@pranshulsoni/flowwatch 1.0.0
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 +442 -0
- package/dist/ai/groqInsightService.d.ts +39 -0
- package/dist/ai/groqInsightService.js +230 -0
- package/dist/createFlowwatch.d.ts +17 -0
- package/dist/createFlowwatch.js +90 -0
- package/dist/dashboard/routes/dashboardResponse.d.ts +204 -0
- package/dist/dashboard/routes/dashboardResponse.js +248 -0
- package/dist/dashboard/routes/router.d.ts +13 -0
- package/dist/dashboard/routes/router.js +708 -0
- package/dist/dashboard/static/dashboard.html +6061 -0
- package/dist/engine/background/queues/workflowQueue.d.ts +6 -0
- package/dist/engine/background/queues/workflowQueue.js +14 -0
- package/dist/engine/background/workers/workflowWorker.d.ts +15 -0
- package/dist/engine/background/workers/workflowWorker.js +98 -0
- package/dist/engine/errors/errorEngine.d.ts +27 -0
- package/dist/engine/errors/errorEngine.js +115 -0
- package/dist/engine/flags/evaluateFlag.d.ts +3 -0
- package/dist/engine/flags/evaluateFlag.js +50 -0
- package/dist/engine/flags/flagEngine.d.ts +9 -0
- package/dist/engine/flags/flagEngine.js +52 -0
- package/dist/engine/flags/hashRollout.d.ts +1 -0
- package/dist/engine/flags/hashRollout.js +9 -0
- package/dist/engine/flags/types.d.ts +7 -0
- package/dist/engine/flags/types.js +1 -0
- package/dist/engine/trace/traceEngine.d.ts +26 -0
- package/dist/engine/trace/traceEngine.js +76 -0
- package/dist/engine/workflows/types.d.ts +28 -0
- package/dist/engine/workflows/types.js +1 -0
- package/dist/engine/workflows/workflowEngine.d.ts +15 -0
- package/dist/engine/workflows/workflowEngine.js +112 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +3 -0
- package/dist/persistence/cache/redisClient.d.ts +2 -0
- package/dist/persistence/cache/redisClient.js +4 -0
- package/dist/persistence/db/postgres.d.ts +3 -0
- package/dist/persistence/db/postgres.js +4 -0
- package/dist/persistence/migrations/migrationRunner.d.ts +3 -0
- package/dist/persistence/migrations/migrationRunner.js +46 -0
- package/dist/persistence/migrations/migrations.d.ts +5 -0
- package/dist/persistence/migrations/migrations.js +191 -0
- package/dist/persistence/repositories/errors/errorRepository.d.ts +38 -0
- package/dist/persistence/repositories/errors/errorRepository.js +63 -0
- package/dist/persistence/repositories/flags/flagRepository.d.ts +72 -0
- package/dist/persistence/repositories/flags/flagRepository.js +245 -0
- package/dist/persistence/repositories/traces/traceRepository.d.ts +64 -0
- package/dist/persistence/repositories/traces/traceRepository.js +110 -0
- package/dist/persistence/repositories/workflows/workflowRepository.d.ts +93 -0
- package/dist/persistence/repositories/workflows/workflowRepository.js +260 -0
- package/dist/persistence/transaction.d.ts +2 -0
- package/dist/persistence/transaction.js +16 -0
- package/dist/runtime/config/normalizeConfig.d.ts +2 -0
- package/dist/runtime/config/normalizeConfig.js +46 -0
- package/dist/runtime/config/validationConfig.d.ts +2 -0
- package/dist/runtime/config/validationConfig.js +119 -0
- package/dist/runtime/health/healthService.d.ts +30 -0
- package/dist/runtime/health/healthService.js +54 -0
- package/dist/runtime/tracing/traceContext.d.ts +12 -0
- package/dist/runtime/tracing/traceContext.js +28 -0
- package/dist/runtime/tracing/tracingMiddleware.d.ts +3 -0
- package/dist/runtime/tracing/tracingMiddleware.js +46 -0
- package/dist/search/elasticsearch/client.d.ts +2 -0
- package/dist/search/elasticsearch/client.js +4 -0
- package/dist/search/elasticsearch/indexSetup.d.ts +3 -0
- package/dist/search/elasticsearch/indexSetup.js +43 -0
- package/dist/search/elasticsearch/indexer.d.ts +9 -0
- package/dist/search/elasticsearch/indexer.js +86 -0
- package/dist/search/elasticsearch/mappingChecker.d.ts +2 -0
- package/dist/search/elasticsearch/mappingChecker.js +28 -0
- package/dist/types/index.d.ts +48 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/flowwatchEnvStore.d.ts +27 -0
- package/dist/utils/flowwatchEnvStore.js +145 -0
- package/package.json +63 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export async function createErrorMapping(client) {
|
|
2
|
+
await client.indices.create({
|
|
3
|
+
index: "flowwatch_errors",
|
|
4
|
+
mappings: {
|
|
5
|
+
properties: {
|
|
6
|
+
id: { type: "keyword" },
|
|
7
|
+
traceId: { type: "keyword" },
|
|
8
|
+
spanId: { type: "keyword" },
|
|
9
|
+
source: { type: "keyword" },
|
|
10
|
+
category: { type: "keyword" },
|
|
11
|
+
level: { type: "keyword" },
|
|
12
|
+
name: { type: "keyword" },
|
|
13
|
+
fingerprint: { type: "keyword" },
|
|
14
|
+
message: { type: "text" },
|
|
15
|
+
stack: { type: "text" },
|
|
16
|
+
statusCode: { type: "integer" },
|
|
17
|
+
metadata: { type: "object", enabled: true },
|
|
18
|
+
occurredAt: { type: "date" },
|
|
19
|
+
createdAt: { type: "date" },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export async function createTraceMapping(client) {
|
|
25
|
+
await client.indices.create({
|
|
26
|
+
index: "flowwatch_trace_spans",
|
|
27
|
+
mappings: {
|
|
28
|
+
properties: {
|
|
29
|
+
id: { type: "keyword" },
|
|
30
|
+
traceId: { type: "keyword" },
|
|
31
|
+
parentSpanId: { type: "keyword" },
|
|
32
|
+
name: { type: "text" },
|
|
33
|
+
type: { type: "keyword" },
|
|
34
|
+
status: { type: "keyword" },
|
|
35
|
+
durationMs: { type: "integer" },
|
|
36
|
+
metadata: { type: "object", enabled: true },
|
|
37
|
+
startedAt: { type: "date" },
|
|
38
|
+
endedAt: { type: "date" },
|
|
39
|
+
createdAt: { type: "date" },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Client } from "@elastic/elasticsearch";
|
|
2
|
+
import type { ErrorRow } from "../../persistence/repositories/errors/errorRepository.js";
|
|
3
|
+
import type { TraceSpanRow } from "../../persistence/repositories/traces/traceRepository.js";
|
|
4
|
+
export declare const errorIndex = "flowwatch_errors";
|
|
5
|
+
export declare const traceSpanIndex = "flowwatch_trace_spans";
|
|
6
|
+
export declare function indexError(client: Client, error: ErrorRow): Promise<void>;
|
|
7
|
+
export declare function indexTraceSpan(client: Client, span: TraceSpanRow): Promise<void>;
|
|
8
|
+
export declare function createErrorMapping(client: Client): Promise<void>;
|
|
9
|
+
export declare function createTraceMapping(client: Client): Promise<void>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export const errorIndex = "flowwatch_errors";
|
|
2
|
+
export const traceSpanIndex = "flowwatch_trace_spans";
|
|
3
|
+
export async function indexError(client, error) {
|
|
4
|
+
await client.index({
|
|
5
|
+
index: errorIndex,
|
|
6
|
+
id: error.id,
|
|
7
|
+
document: {
|
|
8
|
+
id: error.id,
|
|
9
|
+
traceId: error.trace_id,
|
|
10
|
+
spanId: error.span_id,
|
|
11
|
+
source: error.source,
|
|
12
|
+
category: error.category,
|
|
13
|
+
level: error.level,
|
|
14
|
+
message: error.message,
|
|
15
|
+
stack: error.stack,
|
|
16
|
+
name: error.name,
|
|
17
|
+
statusCode: error.status_code,
|
|
18
|
+
fingerprint: error.fingerprint,
|
|
19
|
+
metadata: error.metadata,
|
|
20
|
+
occurredAt: error.occurred_at,
|
|
21
|
+
createdAt: error.created_at,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export async function indexTraceSpan(client, span) {
|
|
26
|
+
await client.index({
|
|
27
|
+
index: traceSpanIndex,
|
|
28
|
+
id: span.id,
|
|
29
|
+
document: {
|
|
30
|
+
id: span.id,
|
|
31
|
+
traceId: span.trace_id,
|
|
32
|
+
parentSpanId: span.parent_span_id,
|
|
33
|
+
name: span.name,
|
|
34
|
+
type: span.type,
|
|
35
|
+
status: span.status,
|
|
36
|
+
durationMs: span.duration_ms,
|
|
37
|
+
metadata: span.metadata,
|
|
38
|
+
startedAt: span.started_at,
|
|
39
|
+
endedAt: span.ended_at,
|
|
40
|
+
createdAt: span.created_at,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export async function createErrorMapping(client) {
|
|
45
|
+
await client.indices.create({
|
|
46
|
+
index: "flowwatch_errors",
|
|
47
|
+
mappings: {
|
|
48
|
+
properties: {
|
|
49
|
+
id: { type: "keyword" },
|
|
50
|
+
traceId: { type: "keyword" },
|
|
51
|
+
spanId: { type: "keyword" },
|
|
52
|
+
source: { type: "keyword" },
|
|
53
|
+
category: { type: "keyword" },
|
|
54
|
+
level: { type: "keyword" },
|
|
55
|
+
name: { type: "keyword" },
|
|
56
|
+
fingerprint: { type: "keyword" },
|
|
57
|
+
message: { type: "text" },
|
|
58
|
+
stack: { type: "text" },
|
|
59
|
+
statusCode: { type: "integer" },
|
|
60
|
+
metadata: { type: "object", enabled: true },
|
|
61
|
+
occurredAt: { type: "date" },
|
|
62
|
+
createdAt: { type: "date" },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export async function createTraceMapping(client) {
|
|
68
|
+
await client.indices.create({
|
|
69
|
+
index: "flowwatch_trace_spans",
|
|
70
|
+
mappings: {
|
|
71
|
+
properties: {
|
|
72
|
+
id: { type: "keyword" },
|
|
73
|
+
traceId: { type: "keyword" },
|
|
74
|
+
parentSpanId: { type: "keyword" },
|
|
75
|
+
name: { type: "text" },
|
|
76
|
+
type: { type: "keyword" },
|
|
77
|
+
status: { type: "keyword" },
|
|
78
|
+
durationMs: { type: "integer" },
|
|
79
|
+
metadata: { type: "object", enabled: true },
|
|
80
|
+
startedAt: { type: "date" },
|
|
81
|
+
endedAt: { type: "date" },
|
|
82
|
+
createdAt: { type: "date" },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createErrorMapping, createTraceMapping, errorIndex, traceSpanIndex } from "./indexer.js";
|
|
2
|
+
export async function createMissingMappings(client) {
|
|
3
|
+
try {
|
|
4
|
+
await createErrorMappingIfMissing(client);
|
|
5
|
+
await createTraceMappingIfMissing(client);
|
|
6
|
+
}
|
|
7
|
+
catch (err) {
|
|
8
|
+
console.warn(`[Flowwatch] ⚠️ Elasticsearch unavailable on startup (${err?.message ?? err}). Search indexing will be skipped until Elasticsearch is reachable.`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
async function createErrorMappingIfMissing(client) {
|
|
12
|
+
const exists = await client.indices.exists({
|
|
13
|
+
index: errorIndex,
|
|
14
|
+
});
|
|
15
|
+
if (exists) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
await createErrorMapping(client);
|
|
19
|
+
}
|
|
20
|
+
async function createTraceMappingIfMissing(client) {
|
|
21
|
+
const exists = await client.indices.exists({
|
|
22
|
+
index: traceSpanIndex,
|
|
23
|
+
});
|
|
24
|
+
if (exists) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
await createTraceMapping(client);
|
|
28
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { PoolConfig } from "pg";
|
|
2
|
+
import type { Request } from "express";
|
|
3
|
+
export interface FlowwatchConfig {
|
|
4
|
+
db: PoolConfig;
|
|
5
|
+
redis: FlowwatchRedisConfig;
|
|
6
|
+
elasticsearch: FlowwatchElasticsearchConfig;
|
|
7
|
+
dashboard?: FlowwatchDashboardConfig;
|
|
8
|
+
worker?: boolean | FlowwatchWorkerConfig;
|
|
9
|
+
migrations?: FlowwatchMigrationConfig;
|
|
10
|
+
runtime?: FlowwatchRuntimeConfig;
|
|
11
|
+
}
|
|
12
|
+
export interface FlowwatchRedisConfig {
|
|
13
|
+
url: string;
|
|
14
|
+
}
|
|
15
|
+
export interface FlowwatchElasticsearchConfig {
|
|
16
|
+
node: string;
|
|
17
|
+
}
|
|
18
|
+
export interface FlowwatchDashboardConfig {
|
|
19
|
+
path?: string;
|
|
20
|
+
token?: string;
|
|
21
|
+
auth?: (req: Request) => boolean | Promise<boolean>;
|
|
22
|
+
enabled?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface FlowwatchWorkerConfig {
|
|
25
|
+
enabled?: boolean;
|
|
26
|
+
workflowConcurrency?: number;
|
|
27
|
+
errorIndexingConcurrency?: number;
|
|
28
|
+
maintenanceConcurrency?: number;
|
|
29
|
+
queuePrefix?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface FlowwatchMigrationConfig {
|
|
32
|
+
autoRun?: boolean;
|
|
33
|
+
tableName?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface FlowwatchRuntimeConfig {
|
|
36
|
+
environment?: "development" | "test" | "staging" | "production" | string;
|
|
37
|
+
serviceName?: string;
|
|
38
|
+
debug?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface NormalizedFlowwatchConfig {
|
|
41
|
+
db: PoolConfig;
|
|
42
|
+
redis: FlowwatchRedisConfig;
|
|
43
|
+
elasticsearch: FlowwatchElasticsearchConfig;
|
|
44
|
+
dashboard: Required<Pick<FlowwatchDashboardConfig, "path" | "enabled">> & Pick<FlowwatchDashboardConfig, "token" | "auth">;
|
|
45
|
+
worker: Required<FlowwatchWorkerConfig>;
|
|
46
|
+
migrations: Required<FlowwatchMigrationConfig>;
|
|
47
|
+
runtime: Required<FlowwatchRuntimeConfig>;
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
interface FlowwatchEnv {
|
|
2
|
+
groqApiKey?: string;
|
|
3
|
+
groqModel?: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Load AI credentials into the in-memory store on startup.
|
|
7
|
+
*
|
|
8
|
+
* Resolution order (both are checked; process.env fills gaps not in the file):
|
|
9
|
+
* 1. .fw.env at process.cwd() — written by dashboard Settings
|
|
10
|
+
* 2. process.env.FLOWWATCH_GROQ_API_KEY / GROQ_API_KEY — set by consumer
|
|
11
|
+
*
|
|
12
|
+
* If the key is found only in process.env it is immediately persisted to
|
|
13
|
+
* .fw.env so the next restart doesn't need process.env anymore.
|
|
14
|
+
*/
|
|
15
|
+
export declare function loadFlowwatchEnv(): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Persist settings to .fw.env and update the in-memory store.
|
|
18
|
+
* Called from the dashboard Settings API when the user saves their key.
|
|
19
|
+
*/
|
|
20
|
+
export declare function saveFlowwatchEnv(updates: Partial<FlowwatchEnv>): Promise<void>;
|
|
21
|
+
/** API key from store or process.env. Never log or send this to the client. */
|
|
22
|
+
export declare function getGroqApiKey(): string | undefined;
|
|
23
|
+
/** Model from store or process.env. */
|
|
24
|
+
export declare function getGroqModel(): string | undefined;
|
|
25
|
+
/** True if any API key source is available. Safe to send to the client. */
|
|
26
|
+
export declare function isGroqApiKeyConfigured(): boolean;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flowwatch environment store — persists AI credentials to `.fw.env` in the
|
|
3
|
+
* consumer's working directory (process.cwd()), i.e. their project root.
|
|
4
|
+
*
|
|
5
|
+
* Priority order on startup:
|
|
6
|
+
* 1. FLOWWATCH_GROQ_API_KEY / FLOWWATCH_GROQ_MODEL already in process.env
|
|
7
|
+
* 2. .fw.env at process.cwd() written by this module
|
|
8
|
+
* → whichever is found first wins; the result is persisted to .fw.env
|
|
9
|
+
*/
|
|
10
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
const FLOWWATCH_ENV_FILE = ".fw.env";
|
|
13
|
+
/** Single in-memory store for the lifetime of this process. */
|
|
14
|
+
const store = {};
|
|
15
|
+
// ─── Path ────────────────────────────────────────────────────────────────────
|
|
16
|
+
function getFlowwatchEnvPath() {
|
|
17
|
+
return join(process.cwd(), FLOWWATCH_ENV_FILE);
|
|
18
|
+
}
|
|
19
|
+
// ─── File helpers ─────────────────────────────────────────────────────────────
|
|
20
|
+
function parseEnvFile(content) {
|
|
21
|
+
const result = {};
|
|
22
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
23
|
+
const line = raw.trim();
|
|
24
|
+
if (!line || line.startsWith("#"))
|
|
25
|
+
continue;
|
|
26
|
+
const eq = line.indexOf("=");
|
|
27
|
+
if (eq === -1)
|
|
28
|
+
continue;
|
|
29
|
+
const key = line.slice(0, eq).trim();
|
|
30
|
+
const value = line.slice(eq + 1).trim();
|
|
31
|
+
if (key === "FLOWWATCH_GROQ_API_KEY")
|
|
32
|
+
result.groqApiKey = value;
|
|
33
|
+
if (key === "FLOWWATCH_GROQ_MODEL")
|
|
34
|
+
result.groqModel = value;
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
function buildEnvFile(existing, updated) {
|
|
39
|
+
const kept = existing
|
|
40
|
+
.split(/\r?\n/)
|
|
41
|
+
.filter((l) => {
|
|
42
|
+
const t = l.trim();
|
|
43
|
+
return !t.startsWith("FLOWWATCH_GROQ_API_KEY=") && !t.startsWith("FLOWWATCH_GROQ_MODEL=");
|
|
44
|
+
});
|
|
45
|
+
while (kept.length && !kept[kept.length - 1].trim())
|
|
46
|
+
kept.pop();
|
|
47
|
+
if (!kept.length) {
|
|
48
|
+
kept.push("# Flowwatch AI configuration — auto-generated, do not commit to version control");
|
|
49
|
+
}
|
|
50
|
+
if (updated.groqApiKey)
|
|
51
|
+
kept.push(`FLOWWATCH_GROQ_API_KEY=${updated.groqApiKey}`);
|
|
52
|
+
if (updated.groqModel)
|
|
53
|
+
kept.push(`FLOWWATCH_GROQ_MODEL=${updated.groqModel}`);
|
|
54
|
+
kept.push("");
|
|
55
|
+
return kept.join("\n");
|
|
56
|
+
}
|
|
57
|
+
async function readEnvFile() {
|
|
58
|
+
try {
|
|
59
|
+
return await readFile(getFlowwatchEnvPath(), "utf-8");
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
66
|
+
/**
|
|
67
|
+
* Load AI credentials into the in-memory store on startup.
|
|
68
|
+
*
|
|
69
|
+
* Resolution order (both are checked; process.env fills gaps not in the file):
|
|
70
|
+
* 1. .fw.env at process.cwd() — written by dashboard Settings
|
|
71
|
+
* 2. process.env.FLOWWATCH_GROQ_API_KEY / GROQ_API_KEY — set by consumer
|
|
72
|
+
*
|
|
73
|
+
* If the key is found only in process.env it is immediately persisted to
|
|
74
|
+
* .fw.env so the next restart doesn't need process.env anymore.
|
|
75
|
+
*/
|
|
76
|
+
export async function loadFlowwatchEnv() {
|
|
77
|
+
const filePath = getFlowwatchEnvPath();
|
|
78
|
+
// Step 1 — read whatever the file has (may be partial or missing)
|
|
79
|
+
const raw = await readEnvFile();
|
|
80
|
+
const parsed = parseEnvFile(raw);
|
|
81
|
+
if (parsed.groqApiKey)
|
|
82
|
+
store.groqApiKey = parsed.groqApiKey;
|
|
83
|
+
if (parsed.groqModel)
|
|
84
|
+
store.groqModel = parsed.groqModel;
|
|
85
|
+
// Step 2 — fill any gaps from process.env (ALWAYS checked, even if file exists)
|
|
86
|
+
const envKey = process.env.FLOWWATCH_GROQ_API_KEY || process.env.GROQ_API_KEY;
|
|
87
|
+
const envModel = process.env.FLOWWATCH_GROQ_MODEL || process.env.GROQ_MODEL;
|
|
88
|
+
let needsSave = false;
|
|
89
|
+
if (!store.groqApiKey && envKey) {
|
|
90
|
+
store.groqApiKey = envKey;
|
|
91
|
+
needsSave = true;
|
|
92
|
+
}
|
|
93
|
+
if (!store.groqModel && envModel) {
|
|
94
|
+
store.groqModel = envModel;
|
|
95
|
+
needsSave = true;
|
|
96
|
+
}
|
|
97
|
+
if (store.groqApiKey) {
|
|
98
|
+
console.log(`[Flowwatch] ✅ Groq API key loaded (config: ${filePath})`);
|
|
99
|
+
// Persist back so future restarts find the key in the file
|
|
100
|
+
if (needsSave) {
|
|
101
|
+
try {
|
|
102
|
+
await writeFile(filePath, buildEnvFile(raw, store), "utf-8");
|
|
103
|
+
console.log(`[Flowwatch] ✅ API key saved to ${filePath}`);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.warn(`[Flowwatch] ⚠️ Could not write ${filePath}: ${err?.message}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
console.log(`[Flowwatch] ⚠️ No Groq API key found.\n` +
|
|
112
|
+
` • Enter it in the dashboard → Settings → AI Configuration\n` +
|
|
113
|
+
` • OR set FLOWWATCH_GROQ_API_KEY in your environment\n` +
|
|
114
|
+
` • Config will be saved to: ${filePath}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Persist settings to .fw.env and update the in-memory store.
|
|
119
|
+
* Called from the dashboard Settings API when the user saves their key.
|
|
120
|
+
*/
|
|
121
|
+
export async function saveFlowwatchEnv(updates) {
|
|
122
|
+
if (updates.groqApiKey !== undefined)
|
|
123
|
+
store.groqApiKey = updates.groqApiKey || undefined;
|
|
124
|
+
if (updates.groqModel !== undefined)
|
|
125
|
+
store.groqModel = updates.groqModel || undefined;
|
|
126
|
+
const existing = await readEnvFile();
|
|
127
|
+
await writeFile(getFlowwatchEnvPath(), buildEnvFile(existing, store), "utf-8");
|
|
128
|
+
}
|
|
129
|
+
/** API key from store or process.env. Never log or send this to the client. */
|
|
130
|
+
export function getGroqApiKey() {
|
|
131
|
+
return (store.groqApiKey ||
|
|
132
|
+
process.env.FLOWWATCH_GROQ_API_KEY ||
|
|
133
|
+
process.env.GROQ_API_KEY);
|
|
134
|
+
}
|
|
135
|
+
/** Model from store or process.env. */
|
|
136
|
+
export function getGroqModel() {
|
|
137
|
+
return (store.groqModel ||
|
|
138
|
+
process.env.FLOWWATCH_GROQ_MODEL ||
|
|
139
|
+
process.env.GROQ_MODEL ||
|
|
140
|
+
undefined);
|
|
141
|
+
}
|
|
142
|
+
/** True if any API key source is available. Safe to send to the client. */
|
|
143
|
+
export function isGroqApiKeyConfigured() {
|
|
144
|
+
return Boolean(getGroqApiKey());
|
|
145
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pranshulsoni/flowwatch",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Embedded operations dashboard, workflow runner, feature flags, tracing, error capture, health checks, and AI diagnostics for Express backends.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"express",
|
|
19
|
+
"backend",
|
|
20
|
+
"observability",
|
|
21
|
+
"dashboard",
|
|
22
|
+
"workflow",
|
|
23
|
+
"workflows",
|
|
24
|
+
"feature-flags",
|
|
25
|
+
"tracing",
|
|
26
|
+
"error-monitoring",
|
|
27
|
+
"health-checks",
|
|
28
|
+
"bullmq",
|
|
29
|
+
"postgres",
|
|
30
|
+
"redis",
|
|
31
|
+
"elasticsearch",
|
|
32
|
+
"groq",
|
|
33
|
+
"ai-diagnostics"
|
|
34
|
+
],
|
|
35
|
+
"author": "Pranshul Soni",
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -p tsconfig.json && node scripts/copy-dashboard-static.cjs"
|
|
38
|
+
},
|
|
39
|
+
"license": "ISC",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/PranshulSoni/FlowWatch.git",
|
|
43
|
+
"directory": "packages/flowwatch"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/PranshulSoni/FlowWatch/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/PranshulSoni/FlowWatch#readme",
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20.6.0"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@elastic/elasticsearch": "^8.19.1",
|
|
57
|
+
"bullmq": "^5.78.1",
|
|
58
|
+
"express": "^5.2.1",
|
|
59
|
+
"ioredis": "^5.11.1",
|
|
60
|
+
"pg": "^8.21.0",
|
|
61
|
+
"zod": "^4.4.3"
|
|
62
|
+
}
|
|
63
|
+
}
|