@geraldmaron/construct 1.0.2 → 1.0.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/.env.example +1 -1
- package/README.md +4 -4
- package/agents/prompts/cx-ai-engineer.md +6 -26
- package/agents/prompts/cx-architect.md +1 -0
- package/agents/prompts/cx-business-strategist.md +2 -0
- package/agents/prompts/cx-data-analyst.md +6 -26
- package/agents/prompts/cx-docs-keeper.md +1 -31
- package/agents/prompts/cx-explorer.md +1 -0
- package/agents/prompts/cx-orchestrator.md +40 -112
- package/agents/prompts/cx-platform-engineer.md +2 -22
- package/agents/prompts/cx-product-manager.md +2 -1
- package/agents/prompts/cx-qa.md +0 -20
- package/agents/prompts/cx-rd-lead.md +2 -0
- package/agents/prompts/cx-researcher.md +77 -31
- package/agents/prompts/cx-security.md +11 -49
- package/agents/prompts/cx-sre.md +9 -43
- package/agents/prompts/cx-ux-researcher.md +1 -0
- package/agents/role-manifests.json +4 -4
- package/bin/construct +72 -11
- package/bin/construct-postinstall.mjs +1 -1
- package/db/schema/004_recommendations.sql +46 -0
- package/db/schema/005_strategy.sql +21 -0
- package/lib/auto-docs.mjs +1 -2
- package/lib/beads-automation.mjs +16 -7
- package/lib/bootstrap/resources.mjs +2 -2
- package/lib/cli-commands.mjs +8 -2
- package/lib/document-ingest.mjs +6 -5
- package/lib/embed/cli.mjs +16 -3
- package/lib/embed/conflict-detection.mjs +26 -9
- package/lib/embed/customer-profiles.mjs +38 -18
- package/lib/embed/daemon.mjs +59 -50
- package/lib/embed/inbox.mjs +30 -0
- package/lib/embed/recommendation-store.mjs +214 -15
- package/lib/embed/workspaces.mjs +53 -18
- package/lib/evaluator-optimizer.mjs +0 -2
- package/lib/features.mjs +11 -0
- package/lib/gates-audit.mjs +3 -3
- package/lib/health-check.mjs +3 -5
- package/lib/hooks/pre-compact.mjs +3 -0
- package/lib/hooks/read-tracker.mjs +10 -101
- package/lib/host-capabilities.mjs +90 -1
- package/lib/init-unified.mjs +119 -3
- package/lib/init-update.mjs +246 -131
- package/lib/install/first-invocation.mjs +4 -4
- package/lib/intake/queue.mjs +1 -1
- package/lib/integrations/intake-integrations.mjs +4 -5
- package/lib/intent-classifier.mjs +1 -0
- package/lib/knowledge/layout.mjs +10 -0
- package/lib/knowledge/rag.mjs +16 -0
- package/lib/mcp/tools/telemetry.mjs +30 -78
- package/lib/model-cheapest-provider.mjs +231 -0
- package/lib/model-router.mjs +68 -9
- package/lib/ollama-manager.mjs +1 -1
- package/lib/opencode-telemetry.mjs +4 -5
- package/lib/orchestration-policy.mjs +9 -0
- package/lib/parity.mjs +124 -21
- package/lib/project-profile.mjs +1 -1
- package/lib/prompt-composer.js +106 -29
- package/lib/read-tracker-store.mjs +149 -0
- package/lib/roles/catalog.mjs +133 -0
- package/lib/roles/preference.mjs +74 -0
- package/lib/server/index.mjs +109 -47
- package/lib/server/insights.mjs +1 -1
- package/lib/server/telemetry-login.mjs +17 -25
- package/lib/service-manager.mjs +32 -24
- package/lib/services/local-postgres.mjs +15 -0
- package/lib/services/telemetry-backend.mjs +2 -3
- package/lib/setup-prompts.mjs +2 -2
- package/lib/setup.mjs +55 -46
- package/lib/status.mjs +56 -8
- package/lib/storage/backend.mjs +12 -2
- package/lib/storage/postgres-backup.mjs +1 -1
- package/lib/strategy-store.mjs +371 -0
- package/lib/telemetry/backends/local.mjs +6 -4
- package/lib/telemetry/client.mjs +185 -0
- package/lib/telemetry/ingest.mjs +13 -5
- package/lib/telemetry/team-rollup.mjs +9 -2
- package/lib/uninstall/uninstall.mjs +1 -1
- package/lib/worker/trace.mjs +17 -27
- package/package.json +5 -2
- package/rules/common/research.md +44 -12
- package/skills/docs/backlog-proposal-workflow.md +2 -2
- package/skills/docs/customer-profile-workflow.md +1 -1
- package/skills/docs/evidence-ingest-workflow.md +5 -5
- package/skills/docs/prfaq-workflow.md +1 -1
- package/skills/docs/product-intelligence-review.md +1 -1
- package/skills/docs/product-intelligence-workflow.md +3 -3
- package/skills/docs/product-signal-workflow.md +48 -18
- package/skills/docs/research-workflow.md +26 -14
- package/skills/docs/strategy-workflow.md +36 -0
- package/skills/roles/data-analyst.product-intelligence.md +1 -1
- package/skills/roles/researcher.md +28 -15
- package/skills/routing.md +8 -1
- package/templates/docs/construct_guide.md +2 -2
- package/templates/docs/research-brief.md +63 -9
- package/templates/docs/strategy.md +36 -0
- package/templates/homebrew/construct.rb +7 -7
package/lib/setup.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Guides users through provider selection, API key entry, and model tier
|
|
6
6
|
* assignment. Writes the resulting config to ~/.cx/env and optionally to
|
|
7
|
-
* project-level .env. Invoked by `construct
|
|
7
|
+
* project-level .env. Invoked by `construct init` and on first init.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import fs from 'node:fs';
|
|
@@ -25,10 +25,6 @@ import { createSqlClient, closeSqlClient } from './storage/backend.mjs';
|
|
|
25
25
|
import { getEmbeddingModelInfo, warmupEmbeddingModel } from './storage/embeddings-engine.mjs';
|
|
26
26
|
import { restoreConstructDb } from './storage/postgres-backup.mjs';
|
|
27
27
|
import { buildPressureGuardValues, installPressureGuardLaunchAgent, loadPressureGuardLaunchAgent } from './runtime-pressure.mjs';
|
|
28
|
-
import {
|
|
29
|
-
isRemoteTelemetry,
|
|
30
|
-
startManagedServices,
|
|
31
|
-
} from './services/telemetry-backend.mjs';
|
|
32
28
|
import { consentToInstall } from './setup-prompts.mjs';
|
|
33
29
|
|
|
34
30
|
const ROOT_DIR = path.resolve(import.meta.dirname, '..');
|
|
@@ -43,7 +39,7 @@ function printHelp() {
|
|
|
43
39
|
console.log(`Construct setup
|
|
44
40
|
|
|
45
41
|
Usage:
|
|
46
|
-
construct
|
|
42
|
+
construct init [--yes] [--no-docker]
|
|
47
43
|
|
|
48
44
|
What it does:
|
|
49
45
|
- creates ~/.construct/config.env
|
|
@@ -86,9 +82,8 @@ export function defaultVectorIndexPath(homeDir = HOME) {
|
|
|
86
82
|
export async function buildManagedSetupValues({ homeDir = HOME, env = process.env, databaseUrl = '' } = {}) {
|
|
87
83
|
const modelInfo = await getEmbeddingModelInfo({ env });
|
|
88
84
|
|
|
89
|
-
// Local telemetry backend
|
|
90
85
|
const values = {
|
|
91
|
-
CONSTRUCT_TRACE_BACKEND: env.CONSTRUCT_TRACE_BACKEND || '
|
|
86
|
+
CONSTRUCT_TRACE_BACKEND: env.CONSTRUCT_TRACE_BACKEND || 'local',
|
|
92
87
|
CONSTRUCT_VECTOR_INDEX_PATH: env.CONSTRUCT_VECTOR_INDEX_PATH || defaultVectorIndexPath(homeDir),
|
|
93
88
|
CONSTRUCT_VECTOR_MODEL: env.CONSTRUCT_VECTOR_MODEL || modelInfo.model,
|
|
94
89
|
...buildPressureGuardValues({ env }),
|
|
@@ -533,37 +528,10 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
533
528
|
console.log(` ${dockerInstallHint()}`);
|
|
534
529
|
}
|
|
535
530
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
if (dockerAvailable) {
|
|
541
|
-
const svcConsent = await consentToInstall({
|
|
542
|
-
name: 'telemetry',
|
|
543
|
-
isYes,
|
|
544
|
-
alreadyConfigured: isRemoteTelemetry(process.env.CONSTRUCT_TELEMETRY_URL || ''),
|
|
545
|
-
alreadyConfiguredNote: 'CONSTRUCT_TELEMETRY_URL points to a remote instance — local stack not started.',
|
|
546
|
-
envPath,
|
|
547
|
-
});
|
|
548
|
-
if (svcConsent.decision) {
|
|
549
|
-
try {
|
|
550
|
-
telemetryResult = await startManagedServices({
|
|
551
|
-
rootDir,
|
|
552
|
-
homeDir,
|
|
553
|
-
env: process.env,
|
|
554
|
-
composeRunner: dockerRunner,
|
|
555
|
-
spawnDetached: spawnDetachedForSetup,
|
|
556
|
-
});
|
|
557
|
-
console.log(`Telemetry: ${telemetryResult.status} — ${telemetryResult.note || ''}`);
|
|
558
|
-
} catch (err) {
|
|
559
|
-
telemetryResult = { status: 'error', note: err?.message || 'spin-up failed' };
|
|
560
|
-
console.log(`Telemetry: error — ${telemetryResult.note}`);
|
|
561
|
-
}
|
|
562
|
-
} else {
|
|
563
|
-
telemetryResult = { status: 'skipped', note: svcConsent.note };
|
|
564
|
-
console.log(`Telemetry: skipped (${svcConsent.note})`);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
531
|
+
const telemetryResult = process.env.CONSTRUCT_TELEMETRY_URL
|
|
532
|
+
? { status: 'configured', note: `remote export configured (${process.env.CONSTRUCT_TELEMETRY_URL})` }
|
|
533
|
+
: { status: 'local', note: 'local JSONL traces in .cx/traces; remote export optional' };
|
|
534
|
+
console.log(`Telemetry: ${telemetryResult.note}`);
|
|
567
535
|
|
|
568
536
|
fs.mkdirSync(path.dirname(defaultVectorIndexPath(homeDir)), { recursive: true });
|
|
569
537
|
|
|
@@ -585,6 +553,50 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
585
553
|
console.log(`Embeddings: warmup skipped (${err?.message || 'unknown error'}) — model will load on first use`);
|
|
586
554
|
}
|
|
587
555
|
|
|
556
|
+
// Cheapest provider selection — opt-out by default. When user consents,
|
|
557
|
+
// evaluate all configured providers, pick the lowest-cost model per tier,
|
|
558
|
+
// and write to config.env. On subsequent runs the preference is persisted
|
|
559
|
+
// so the prompt is skipped.
|
|
560
|
+
|
|
561
|
+
const { isCheapestProviderEnabled, selectCheapestForAllTiers, setCheapestProviderPreference, formatCheapestProviderMessage } =
|
|
562
|
+
await import('./model-cheapest-provider.mjs');
|
|
563
|
+
const cheapestAlreadyEnabled = isCheapestProviderEnabled(envPath, { env: process.env });
|
|
564
|
+
if (!cheapestAlreadyEnabled) {
|
|
565
|
+
const cheapestConsent = await consentToInstall({
|
|
566
|
+
name: 'cheapest-provider',
|
|
567
|
+
isYes,
|
|
568
|
+
alreadyConfigured: false,
|
|
569
|
+
envPath,
|
|
570
|
+
defaultYes: false,
|
|
571
|
+
});
|
|
572
|
+
if (cheapestConsent.decision) {
|
|
573
|
+
try {
|
|
574
|
+
const selections = await selectCheapestForAllTiers({ env: process.env });
|
|
575
|
+
const applied = {};
|
|
576
|
+
for (const tier of ['reasoning', 'standard', 'fast']) {
|
|
577
|
+
if (selections[tier]?.modelId) applied[tier] = selections[tier].modelId;
|
|
578
|
+
}
|
|
579
|
+
if (Object.keys(applied).length > 0) {
|
|
580
|
+
applyToEnv(envPath, applied);
|
|
581
|
+
console.log('\nCheapest providers applied:');
|
|
582
|
+
for (const [tier, model] of Object.entries(applied)) {
|
|
583
|
+
const label = selections[tier]?.providerLabel || '';
|
|
584
|
+
console.log(` ${tier.padEnd(11)} ${model} (${label})`);
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
console.log('\nCheapest provider: no configured providers found — nothing to apply.');
|
|
588
|
+
}
|
|
589
|
+
setCheapestProviderPreference(envPath, true);
|
|
590
|
+
} catch (err) {
|
|
591
|
+
console.log(`Cheapest provider: skipped (${err?.message || 'unknown error'})`);
|
|
592
|
+
}
|
|
593
|
+
} else {
|
|
594
|
+
console.log(`Cheapest provider: skipped (${cheapestConsent.note})`);
|
|
595
|
+
}
|
|
596
|
+
} else {
|
|
597
|
+
console.log('Cheapest provider: already enabled — skipping prompt.');
|
|
598
|
+
}
|
|
599
|
+
|
|
588
600
|
const hooksResult = ensureGitHooksPath({ cwd: process.cwd() });
|
|
589
601
|
if (hooksResult.status === 'set') console.log(`Git hooks: ${hooksResult.message}`);
|
|
590
602
|
else if (hooksResult.status === 'ok') console.log('Git hooks: wired');
|
|
@@ -705,7 +717,7 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
705
717
|
console.log(` Trace backend: ${managedValues.CONSTRUCT_TRACE_BACKEND}${managedValues.CONSTRUCT_TELEMETRY_URL ? ` (${managedValues.CONSTRUCT_TELEMETRY_URL})` : ''}`);
|
|
706
718
|
console.log(` Pressure guard: swap ${managedValues.CONSTRUCT_PRESSURE_GUARD_SWAP_GB} GiB, opencode max ${managedValues.CONSTRUCT_PRESSURE_GUARD_MAX_OPENCODE}`);
|
|
707
719
|
console.log('\nFor unattended setup, including local Postgres when Docker is running:');
|
|
708
|
-
console.log(' construct
|
|
720
|
+
console.log(' construct init --yes');
|
|
709
721
|
}
|
|
710
722
|
|
|
711
723
|
runConstruct(['sync']);
|
|
@@ -746,18 +758,15 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
746
758
|
// Local-service summary — surfaces running local services. Same pattern
|
|
747
759
|
// as `supabase start`, which prints all local URLs + keys after spin-up.
|
|
748
760
|
|
|
749
|
-
const hasLocalTelemetry = telemetryResult?.status === 'started' || telemetryResult?.status === 'degraded';
|
|
750
761
|
const hasLocalPostgres = serviceResult?.status === 'ok' && serviceResult?.databaseUrl?.includes('127.0.0.1');
|
|
751
762
|
const vectorBackend = hasLocalPostgres
|
|
752
763
|
? { label: 'Postgres + pgvector', detail: `${serviceResult.databaseUrl} (384d embeddings, ${managedValues.CONSTRUCT_VECTOR_MODEL})` }
|
|
753
764
|
: { label: 'JSON fallback', detail: `${managedValues.CONSTRUCT_VECTOR_INDEX_PATH} (${managedValues.CONSTRUCT_VECTOR_MODEL})` };
|
|
754
765
|
|
|
755
766
|
console.log('\nLocal services:');
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
console.log(` Telemetry: not started`);
|
|
760
|
-
}
|
|
767
|
+
console.log(' Traces: local JSONL (.cx/traces)');
|
|
768
|
+
if (process.env.CONSTRUCT_TELEMETRY_URL) console.log(` Telemetry: remote export (${process.env.CONSTRUCT_TELEMETRY_URL})`);
|
|
769
|
+
else console.log(' Telemetry: remote export not configured');
|
|
761
770
|
if (hasLocalPostgres) {
|
|
762
771
|
console.log(` Postgres: ${serviceResult.databaseUrl}`);
|
|
763
772
|
} else if (process.env.DATABASE_URL) {
|
package/lib/status.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { describeSqlStore, sqlStoreHealth } from './storage/sql-store.mjs';
|
|
|
24
24
|
import { describeVectorStore } from './storage/vector-store.mjs';
|
|
25
25
|
import { describeSqlStoreHealth } from './storage/sql-store.mjs';
|
|
26
26
|
import { triggerAutoBackfillIfSparse } from './telemetry/backfill.mjs';
|
|
27
|
+
import { resolveTraceBackend, telemetryProviderLabel } from './telemetry/client.mjs';
|
|
27
28
|
const TOTAL_BYTES_WARNING_THRESHOLD = 750_000;
|
|
28
29
|
|
|
29
30
|
function readJSON(path) {
|
|
@@ -317,20 +318,29 @@ async function defaultProbeService(service) {
|
|
|
317
318
|
}
|
|
318
319
|
|
|
319
320
|
async function fetchTelemetryStatus(env, { timeout = 2500 } = {}) {
|
|
321
|
+
const backend = resolveTraceBackend(env);
|
|
322
|
+
if (backend === 'local') return { status: 'healthy', summary: 'Local JSONL tracing enabled · remote export not configured', backend, provider: telemetryProviderLabel(env) };
|
|
323
|
+
if (backend === 'none') return { status: 'disabled', summary: 'Remote telemetry disabled · local JSONL tracing preserved', backend, provider: telemetryProviderLabel(env) };
|
|
324
|
+
if (backend === 'otel') {
|
|
325
|
+
const endpoint = (env.CONSTRUCT_OTEL_EXPORTER_OTLP_ENDPOINT ?? '').replace(/\/$/, '');
|
|
326
|
+
if (!endpoint) return { status: 'unavailable', summary: 'OTLP endpoint not configured', backend, provider: telemetryProviderLabel(env) };
|
|
327
|
+
return { status: 'configured', summary: `OTLP export configured · ${endpoint}`, backend, provider: telemetryProviderLabel(env) };
|
|
328
|
+
}
|
|
320
329
|
const baseUrl = (env.CONSTRUCT_TELEMETRY_URL ?? '').replace(/\/$/, '');
|
|
321
330
|
const key = env.CONSTRUCT_TELEMETRY_PUBLIC_KEY;
|
|
322
331
|
const secret = env.CONSTRUCT_TELEMETRY_SECRET_KEY;
|
|
323
|
-
if (!key || !secret) return { status: 'unavailable', summary: '
|
|
332
|
+
if (backend === 'langfuse' && (!key || !secret)) return { status: 'unavailable', summary: 'Langfuse credentials not configured', backend, provider: telemetryProviderLabel(env) };
|
|
324
333
|
if (!baseUrl) return { status: 'unavailable', summary: 'CONSTRUCT_TELEMETRY_URL not set' };
|
|
325
334
|
const headers = {
|
|
326
|
-
Authorization: `Basic ${Buffer.from(`${key}:${secret}`).toString('base64')}
|
|
335
|
+
...(key && secret ? { Authorization: `Basic ${Buffer.from(`${key}:${secret}`).toString('base64')}` } : {}),
|
|
327
336
|
};
|
|
328
337
|
const controller = new AbortController();
|
|
329
338
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
330
339
|
try {
|
|
331
|
-
const
|
|
340
|
+
const tracesPath = backend === 'http' ? '/traces?limit=25' : '/api/public/traces?limit=25';
|
|
341
|
+
const res = await fetch(`${baseUrl}${tracesPath}`, { headers, signal: controller.signal });
|
|
332
342
|
if (res.status === 401 || res.status === 403) {
|
|
333
|
-
return { status: 'credentials-invalid', summary: `Telemetry credentials rejected (HTTP ${res.status}) — run: construct
|
|
343
|
+
return { status: 'credentials-invalid', summary: `Telemetry credentials rejected (HTTP ${res.status}) — run: construct init` };
|
|
334
344
|
}
|
|
335
345
|
if (!res.ok) return { status: 'degraded', summary: `Telemetry HTTP ${res.status}` };
|
|
336
346
|
const json = await res.json().catch(() => ({}));
|
|
@@ -359,6 +369,8 @@ async function fetchTelemetryStatus(env, { timeout = 2500 } = {}) {
|
|
|
359
369
|
|
|
360
370
|
return {
|
|
361
371
|
status,
|
|
372
|
+
backend,
|
|
373
|
+
provider: telemetryProviderLabel(env),
|
|
362
374
|
total,
|
|
363
375
|
rich: counts.rich,
|
|
364
376
|
partial: counts.partial,
|
|
@@ -478,6 +490,40 @@ export function buildPublicHealthSurface({
|
|
|
478
490
|
}
|
|
479
491
|
|
|
480
492
|
function traceBackendDefinition(env) {
|
|
493
|
+
const backend = resolveTraceBackend(env);
|
|
494
|
+
const provider = telemetryProviderLabel(env);
|
|
495
|
+
if (backend === 'local' || backend === 'none') {
|
|
496
|
+
return {
|
|
497
|
+
id: 'telemetry',
|
|
498
|
+
name: 'Telemetry',
|
|
499
|
+
url: 'local://.cx/traces',
|
|
500
|
+
runtime: 'local',
|
|
501
|
+
note: backend === 'none' ? 'Remote disabled; local JSONL preserved' : 'Local JSONL traces',
|
|
502
|
+
healthyMessage: 'Local trace capture enabled',
|
|
503
|
+
impactsOverall: false,
|
|
504
|
+
selfCheck: {
|
|
505
|
+
status: backend === 'none' ? 'disabled' : 'healthy',
|
|
506
|
+
message: backend === 'none' ? 'Remote telemetry disabled' : 'Writing .cx/traces/*.jsonl',
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
if (backend === 'otel') {
|
|
511
|
+
const endpoint = (env.CONSTRUCT_OTEL_EXPORTER_OTLP_ENDPOINT ?? '').replace(/\/$/, '');
|
|
512
|
+
return {
|
|
513
|
+
id: 'telemetry',
|
|
514
|
+
name: 'Telemetry',
|
|
515
|
+
url: endpoint || '(not configured)',
|
|
516
|
+
probeUrl: '',
|
|
517
|
+
runtime: 'remote',
|
|
518
|
+
note: `${provider} trace export`,
|
|
519
|
+
healthyMessage: 'Configured',
|
|
520
|
+
impactsOverall: false,
|
|
521
|
+
selfCheck: {
|
|
522
|
+
status: endpoint ? 'configured' : 'unavailable',
|
|
523
|
+
message: endpoint ? `OTLP export configured: ${endpoint}` : 'CONSTRUCT_OTEL_EXPORTER_OTLP_ENDPOINT not set',
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
}
|
|
481
527
|
const url = (env.CONSTRUCT_TELEMETRY_URL ?? '').replace(/\/$/, '');
|
|
482
528
|
const pubKey = env.CONSTRUCT_TELEMETRY_PUBLIC_KEY || '';
|
|
483
529
|
const secKey = env.CONSTRUCT_TELEMETRY_SECRET_KEY || '';
|
|
@@ -486,11 +532,12 @@ function traceBackendDefinition(env) {
|
|
|
486
532
|
id: 'telemetry',
|
|
487
533
|
name: 'Telemetry',
|
|
488
534
|
url: url || '(not configured)',
|
|
489
|
-
probeUrl: url ? `${url}/api/public/traces?limit=1` : '',
|
|
535
|
+
probeUrl: url ? `${url}${backend === 'http' ? '/traces?limit=1' : '/api/public/traces?limit=1'}` : '',
|
|
490
536
|
probeHeaders: auth ? { Authorization: auth } : undefined,
|
|
491
537
|
runtime: 'live',
|
|
492
|
-
note:
|
|
538
|
+
note: `${provider} trace export`,
|
|
493
539
|
healthyMessage: 'Reachable',
|
|
540
|
+
impactsOverall: false,
|
|
494
541
|
};
|
|
495
542
|
}
|
|
496
543
|
|
|
@@ -588,6 +635,7 @@ function serviceDefinitions(env, dashboardPort, selfDashboard) {
|
|
|
588
635
|
runtime: 'live',
|
|
589
636
|
note: 'MCP-managed',
|
|
590
637
|
healthyMessage: 'Reachable',
|
|
638
|
+
impactsOverall: false,
|
|
591
639
|
},
|
|
592
640
|
{
|
|
593
641
|
id: 'opencode',
|
|
@@ -772,8 +820,8 @@ export async function buildStatus({
|
|
|
772
820
|
}
|
|
773
821
|
|
|
774
822
|
function serviceIcon(status) {
|
|
775
|
-
if (status === 'healthy') return '✓';
|
|
776
|
-
if (status === 'degraded'
|
|
823
|
+
if (status === 'healthy' || status === 'configured') return '✓';
|
|
824
|
+
if (status === 'degraded') return '⚠';
|
|
777
825
|
return '✗';
|
|
778
826
|
}
|
|
779
827
|
|
package/lib/storage/backend.mjs
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* lib/storage/backend.mjs — shared storage backend helpers for SQL/vector wiring.
|
|
4
|
+
*
|
|
5
|
+
* postgres is an optional peer dep. Top-level await catches the case where it is not
|
|
6
|
+
* installed (fresh installs, test temp dirs) so the module loads cleanly everywhere.
|
|
4
7
|
*/
|
|
5
|
-
import postgres from 'postgres';
|
|
6
8
|
import { resolveDatabaseUrl } from '../env-config.mjs';
|
|
7
9
|
|
|
10
|
+
let postgres = null;
|
|
11
|
+
try {
|
|
12
|
+
const mod = await import('postgres');
|
|
13
|
+
postgres = mod.default ?? mod;
|
|
14
|
+
} catch {
|
|
15
|
+
// postgres not installed — SQL features unavailable; createSqlClient returns null
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
function cleanUrl(url) {
|
|
9
19
|
return String(url || '').trim();
|
|
10
20
|
}
|
|
11
21
|
|
|
12
22
|
export function createSqlClient(env = process.env) {
|
|
13
23
|
const databaseUrl = cleanUrl(resolveDatabaseUrl(env));
|
|
14
|
-
if (!databaseUrl) return null;
|
|
24
|
+
if (!databaseUrl || !postgres) return null;
|
|
15
25
|
return postgres(databaseUrl, {
|
|
16
26
|
max: Number.parseInt(env.CONSTRUCT_DB_POOL_SIZE || '5', 10),
|
|
17
27
|
idle_timeout: Number.parseInt(env.CONSTRUCT_DB_IDLE_TIMEOUT_MS || '30000', 10),
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Mirrors the telemetry backup pattern in service-manager.mjs. Entry points:
|
|
6
6
|
* - construct down → stashConstructDb (dump before container stop)
|
|
7
|
-
* - construct
|
|
7
|
+
* - construct init → restoreConstructDb (reload after fresh container start)
|
|
8
8
|
*
|
|
9
9
|
* Dumps are stored in ~/.construct/backups/postgres/ as pg_dump custom-format
|
|
10
10
|
* files. The N most recent are kept; older ones are pruned automatically.
|