@geraldmaron/construct 1.0.2 → 1.0.3
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/bin/construct +49 -11
- package/bin/construct-postinstall.mjs +1 -1
- package/lib/bootstrap/resources.mjs +2 -2
- package/lib/document-ingest.mjs +6 -5
- package/lib/embed/cli.mjs +16 -3
- package/lib/embed/customer-profiles.mjs +1 -1
- package/lib/embed/daemon.mjs +49 -42
- package/lib/embed/inbox.mjs +30 -0
- package/lib/embed/recommendation-store.mjs +1 -1
- package/lib/evaluator-optimizer.mjs +0 -2
- package/lib/features.mjs +11 -0
- package/lib/health-check.mjs +2 -4
- package/lib/init-unified.mjs +119 -3
- 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/knowledge/rag.mjs +16 -0
- package/lib/model-cheapest-provider.mjs +231 -0
- package/lib/model-router.mjs +7 -8
- package/lib/project-profile.mjs +1 -1
- package/lib/roles/catalog.mjs +133 -0
- package/lib/roles/preference.mjs +74 -0
- package/lib/server/index.mjs +33 -47
- package/lib/server/insights.mjs +1 -1
- package/lib/service-manager.mjs +2 -2
- package/lib/services/telemetry-backend.mjs +1 -1
- package/lib/setup-prompts.mjs +2 -2
- package/lib/setup.mjs +47 -3
- package/lib/status.mjs +5 -3
- package/lib/storage/postgres-backup.mjs +1 -1
- package/lib/uninstall/uninstall.mjs +1 -1
- package/package.json +1 -1
- package/templates/docs/construct_guide.md +2 -2
- package/templates/homebrew/construct.rb +1 -1
package/lib/server/index.mjs
CHANGED
|
@@ -250,6 +250,25 @@ const MIME = {
|
|
|
250
250
|
'.js': 'text/javascript; charset=utf-8',
|
|
251
251
|
'.css': 'text/css; charset=utf-8',
|
|
252
252
|
'.json': 'application/json',
|
|
253
|
+
'.svg': 'image/svg+xml',
|
|
254
|
+
'.ico': 'image/x-icon',
|
|
255
|
+
'.png': 'image/png',
|
|
256
|
+
'.jpg': 'image/jpeg',
|
|
257
|
+
'.jpeg': 'image/jpeg',
|
|
258
|
+
'.gif': 'image/gif',
|
|
259
|
+
'.webp': 'image/webp',
|
|
260
|
+
'.woff': 'font/woff',
|
|
261
|
+
'.woff2': 'font/woff2',
|
|
262
|
+
'.ttf': 'font/ttf',
|
|
263
|
+
'.xml': 'application/xml',
|
|
264
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
265
|
+
'.manifest': 'text/cache-manifest',
|
|
266
|
+
'.map': 'application/json',
|
|
267
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
268
|
+
'.otf': 'font/otf',
|
|
269
|
+
'.mp4': 'video/mp4',
|
|
270
|
+
'.mp3': 'audio/mpeg',
|
|
271
|
+
'.pdf': 'application/pdf',
|
|
253
272
|
};
|
|
254
273
|
|
|
255
274
|
function listCommands() {
|
|
@@ -1062,56 +1081,10 @@ const server = createServer(async (req, res) => {
|
|
|
1062
1081
|
try {
|
|
1063
1082
|
const env = loadConstructEnv();
|
|
1064
1083
|
const instanceId = env.CONSTRUCT_INSTANCE_ID || 'default';
|
|
1065
|
-
|
|
1066
|
-
// Check if we're running inside another Construct instance
|
|
1067
|
-
const parentConstruct = env.CONSTRUCT_PARENT_INSTANCE || null;
|
|
1068
|
-
const parentUrl = env.CONSTRUCT_PARENT_URL || null;
|
|
1069
|
-
|
|
1070
|
-
// Determine embedding boundary status
|
|
1071
|
-
const isEmbedded = !!parentConstruct;
|
|
1072
|
-
const boundaryStatus = isEmbedded ? 'embedded' : 'standalone';
|
|
1073
|
-
|
|
1074
|
-
// Get current embed status
|
|
1075
|
-
const embedStatus = resolveEmbedStatus(env);
|
|
1076
|
-
|
|
1077
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1078
|
-
res.end(JSON.stringify({
|
|
1079
|
-
boundaryStatus,
|
|
1080
|
-
instanceId,
|
|
1081
|
-
parentConstruct,
|
|
1082
|
-
parentUrl,
|
|
1083
|
-
isEmbedded,
|
|
1084
|
-
embedStatus: embedStatus.level,
|
|
1085
|
-
embedConfigExists: existsSync(join(HOME, '.construct', 'embed.yaml')),
|
|
1086
|
-
// Boundary capabilities that could be exposed to parent
|
|
1087
|
-
capabilities: {
|
|
1088
|
-
modeDetection: true,
|
|
1089
|
-
snapshotStatus: true,
|
|
1090
|
-
approvalQueue: true,
|
|
1091
|
-
configManagement: true
|
|
1092
|
-
}
|
|
1093
|
-
}));
|
|
1094
|
-
} catch (err) {
|
|
1095
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1096
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
1097
|
-
}
|
|
1098
|
-
return;
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
if (url.pathname === '/api/embed/boundary' && req.method === 'GET') {
|
|
1102
|
-
try {
|
|
1103
|
-
const env = loadConstructEnv();
|
|
1104
|
-
const instanceId = env.CONSTRUCT_INSTANCE_ID || 'default';
|
|
1105
|
-
|
|
1106
|
-
// Check if we're running inside another Construct instance
|
|
1107
1084
|
const parentConstruct = env.CONSTRUCT_PARENT_INSTANCE || null;
|
|
1108
1085
|
const parentUrl = env.CONSTRUCT_PARENT_URL || null;
|
|
1109
|
-
|
|
1110
|
-
// Determine embedding boundary status
|
|
1111
1086
|
const isEmbedded = !!parentConstruct;
|
|
1112
1087
|
const boundaryStatus = isEmbedded ? 'embedded' : 'standalone';
|
|
1113
|
-
|
|
1114
|
-
// Get current embed status
|
|
1115
1088
|
const embedStatus = resolveEmbedStatus(env);
|
|
1116
1089
|
|
|
1117
1090
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -1123,7 +1096,6 @@ const server = createServer(async (req, res) => {
|
|
|
1123
1096
|
isEmbedded,
|
|
1124
1097
|
embedStatus: embedStatus.level,
|
|
1125
1098
|
embedConfigExists: existsSync(join(HOME, '.construct', 'embed.yaml')),
|
|
1126
|
-
// Boundary capabilities that could be exposed to parent
|
|
1127
1099
|
capabilities: {
|
|
1128
1100
|
modeDetection: true,
|
|
1129
1101
|
snapshotStatus: true,
|
|
@@ -1358,6 +1330,20 @@ const server = createServer(async (req, res) => {
|
|
|
1358
1330
|
return;
|
|
1359
1331
|
}
|
|
1360
1332
|
|
|
1333
|
+
if (url.pathname === '/api/intake/list' && req.method === 'GET') {
|
|
1334
|
+
try {
|
|
1335
|
+
const { createIntakeQueue } = await import('../intake/queue.mjs');
|
|
1336
|
+
const queue = createIntakeQueue(ROOT_DIR, process.env);
|
|
1337
|
+
const items = queue.listPending();
|
|
1338
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1339
|
+
res.end(JSON.stringify({ items, total: items.length }));
|
|
1340
|
+
} catch (err) {
|
|
1341
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1342
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1343
|
+
}
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1361
1347
|
if (url.pathname === '/api/providers' && req.method === 'GET') {
|
|
1362
1348
|
try {
|
|
1363
1349
|
const probe = url.searchParams.get('probe') === '1';
|
package/lib/server/insights.mjs
CHANGED
|
@@ -42,7 +42,7 @@ async function fetchTelemetrySummary(env, { limit = 100 } = {}) {
|
|
|
42
42
|
});
|
|
43
43
|
clearTimeout(timer);
|
|
44
44
|
if (res.status === 401 || res.status === 403) {
|
|
45
|
-
return { state: 'misconfigured', message: `Telemetry credentials rejected (HTTP ${res.status}). Run construct
|
|
45
|
+
return { state: 'misconfigured', message: `Telemetry credentials rejected (HTTP ${res.status}). Run construct init.` };
|
|
46
46
|
}
|
|
47
47
|
if (!res.ok) return { state: 'unreachable', message: `Telemetry HTTP ${res.status}` };
|
|
48
48
|
const json = await res.json();
|
package/lib/service-manager.mjs
CHANGED
|
@@ -379,7 +379,7 @@ function startConstructPostgres({ rootDir, homeDir = os.homedir(), spawnSyncFn =
|
|
|
379
379
|
if (!composeRunner) return { status: 'unavailable', note: 'Docker not available' };
|
|
380
380
|
|
|
381
381
|
const composeFile = constructPgComposePath(rootDir);
|
|
382
|
-
if (!fs.existsSync(composeFile)) return { status: 'unavailable', note: 'Postgres compose file not found — run construct
|
|
382
|
+
if (!fs.existsSync(composeFile)) return { status: 'unavailable', note: 'Postgres compose file not found — run construct init first' };
|
|
383
383
|
|
|
384
384
|
const args = [...composeRunner.argsPrefix, '-p', 'construct-postgres', '-f', composeFile, 'up', '-d'];
|
|
385
385
|
const r = spawnSyncFn(composeRunner.command, args, { stdio: 'pipe', encoding: 'utf8' });
|
|
@@ -563,7 +563,7 @@ export async function startServices({
|
|
|
563
563
|
results.push({ name: 'Memory (cm)', url: `http://127.0.0.1:${ports.memory}`, status: 'started' });
|
|
564
564
|
}
|
|
565
565
|
} else {
|
|
566
|
-
results.push({ name: 'Memory (cm)', status: 'unavailable', note: 'cm not installed — run: construct
|
|
566
|
+
results.push({ name: 'Memory (cm)', status: 'unavailable', note: 'cm not installed — run: construct init or brew install dicklesworthstone/tap/cm' });
|
|
567
567
|
}
|
|
568
568
|
|
|
569
569
|
// OpenCode (optional)
|
|
@@ -53,7 +53,7 @@ export async function verifyPostgresHealth({
|
|
|
53
53
|
maxRetries = 5,
|
|
54
54
|
intervalMs = 2000,
|
|
55
55
|
} = {}) {
|
|
56
|
-
//
|
|
56
|
+
// Postgres is not HTTP — health is checked via pg_isready in docker instead
|
|
57
57
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
58
58
|
try {
|
|
59
59
|
const result = spawnSync('docker', [
|
package/lib/setup-prompts.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/setup-prompts.mjs — yes/no consent helper for `construct
|
|
2
|
+
* lib/setup-prompts.mjs — yes/no consent helper for `construct init`.
|
|
3
3
|
*
|
|
4
4
|
* Centralises the "should we install this service?" decision so Postgres,
|
|
5
5
|
* telemetry, and any future service share the same prompt + consent-caching
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* 5. interactive → prompt [Y/n], cache the answer
|
|
15
15
|
*
|
|
16
16
|
* Consent is stored as BOOTSTRAP_<NAME>=yes|no in ~/.construct/config.env
|
|
17
|
-
* so subsequent runs of `construct
|
|
17
|
+
* so subsequent runs of `construct init` don't re-prompt for the same
|
|
18
18
|
* services. Pass `force = true` to skip the cache and re-prompt.
|
|
19
19
|
*/
|
|
20
20
|
|
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';
|
|
@@ -43,7 +43,7 @@ function printHelp() {
|
|
|
43
43
|
console.log(`Construct setup
|
|
44
44
|
|
|
45
45
|
Usage:
|
|
46
|
-
construct
|
|
46
|
+
construct init [--yes] [--no-docker]
|
|
47
47
|
|
|
48
48
|
What it does:
|
|
49
49
|
- creates ~/.construct/config.env
|
|
@@ -585,6 +585,50 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
585
585
|
console.log(`Embeddings: warmup skipped (${err?.message || 'unknown error'}) — model will load on first use`);
|
|
586
586
|
}
|
|
587
587
|
|
|
588
|
+
// Cheapest provider selection — opt-out by default. When user consents,
|
|
589
|
+
// evaluate all configured providers, pick the lowest-cost model per tier,
|
|
590
|
+
// and write to config.env. On subsequent runs the preference is persisted
|
|
591
|
+
// so the prompt is skipped.
|
|
592
|
+
|
|
593
|
+
const { isCheapestProviderEnabled, selectCheapestForAllTiers, setCheapestProviderPreference, formatCheapestProviderMessage } =
|
|
594
|
+
await import('./model-cheapest-provider.mjs');
|
|
595
|
+
const cheapestAlreadyEnabled = isCheapestProviderEnabled(envPath, { env: process.env });
|
|
596
|
+
if (!cheapestAlreadyEnabled) {
|
|
597
|
+
const cheapestConsent = await consentToInstall({
|
|
598
|
+
name: 'cheapest-provider',
|
|
599
|
+
isYes,
|
|
600
|
+
alreadyConfigured: false,
|
|
601
|
+
envPath,
|
|
602
|
+
defaultYes: false,
|
|
603
|
+
});
|
|
604
|
+
if (cheapestConsent.decision) {
|
|
605
|
+
try {
|
|
606
|
+
const selections = await selectCheapestForAllTiers({ env: process.env });
|
|
607
|
+
const applied = {};
|
|
608
|
+
for (const tier of ['reasoning', 'standard', 'fast']) {
|
|
609
|
+
if (selections[tier]?.modelId) applied[tier] = selections[tier].modelId;
|
|
610
|
+
}
|
|
611
|
+
if (Object.keys(applied).length > 0) {
|
|
612
|
+
applyToEnv(envPath, applied);
|
|
613
|
+
console.log('\nCheapest providers applied:');
|
|
614
|
+
for (const [tier, model] of Object.entries(applied)) {
|
|
615
|
+
const label = selections[tier]?.providerLabel || '';
|
|
616
|
+
console.log(` ${tier.padEnd(11)} ${model} (${label})`);
|
|
617
|
+
}
|
|
618
|
+
} else {
|
|
619
|
+
console.log('\nCheapest provider: no configured providers found — nothing to apply.');
|
|
620
|
+
}
|
|
621
|
+
setCheapestProviderPreference(envPath, true);
|
|
622
|
+
} catch (err) {
|
|
623
|
+
console.log(`Cheapest provider: skipped (${err?.message || 'unknown error'})`);
|
|
624
|
+
}
|
|
625
|
+
} else {
|
|
626
|
+
console.log(`Cheapest provider: skipped (${cheapestConsent.note})`);
|
|
627
|
+
}
|
|
628
|
+
} else {
|
|
629
|
+
console.log('Cheapest provider: already enabled — skipping prompt.');
|
|
630
|
+
}
|
|
631
|
+
|
|
588
632
|
const hooksResult = ensureGitHooksPath({ cwd: process.cwd() });
|
|
589
633
|
if (hooksResult.status === 'set') console.log(`Git hooks: ${hooksResult.message}`);
|
|
590
634
|
else if (hooksResult.status === 'ok') console.log('Git hooks: wired');
|
|
@@ -705,7 +749,7 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
705
749
|
console.log(` Trace backend: ${managedValues.CONSTRUCT_TRACE_BACKEND}${managedValues.CONSTRUCT_TELEMETRY_URL ? ` (${managedValues.CONSTRUCT_TELEMETRY_URL})` : ''}`);
|
|
706
750
|
console.log(` Pressure guard: swap ${managedValues.CONSTRUCT_PRESSURE_GUARD_SWAP_GB} GiB, opencode max ${managedValues.CONSTRUCT_PRESSURE_GUARD_MAX_OPENCODE}`);
|
|
707
751
|
console.log('\nFor unattended setup, including local Postgres when Docker is running:');
|
|
708
|
-
console.log(' construct
|
|
752
|
+
console.log(' construct init --yes');
|
|
709
753
|
}
|
|
710
754
|
|
|
711
755
|
runConstruct(['sync']);
|
package/lib/status.mjs
CHANGED
|
@@ -330,7 +330,7 @@ async function fetchTelemetryStatus(env, { timeout = 2500 } = {}) {
|
|
|
330
330
|
try {
|
|
331
331
|
const res = await fetch(`${baseUrl}/api/public/traces?limit=25`, { headers, signal: controller.signal });
|
|
332
332
|
if (res.status === 401 || res.status === 403) {
|
|
333
|
-
return { status: 'credentials-invalid', summary: `Telemetry credentials rejected (HTTP ${res.status}) — run: construct
|
|
333
|
+
return { status: 'credentials-invalid', summary: `Telemetry credentials rejected (HTTP ${res.status}) — run: construct init` };
|
|
334
334
|
}
|
|
335
335
|
if (!res.ok) return { status: 'degraded', summary: `Telemetry HTTP ${res.status}` };
|
|
336
336
|
const json = await res.json().catch(() => ({}));
|
|
@@ -491,6 +491,7 @@ function traceBackendDefinition(env) {
|
|
|
491
491
|
runtime: 'live',
|
|
492
492
|
note: 'Trace backend',
|
|
493
493
|
healthyMessage: 'Reachable',
|
|
494
|
+
impactsOverall: false,
|
|
494
495
|
};
|
|
495
496
|
}
|
|
496
497
|
|
|
@@ -588,6 +589,7 @@ function serviceDefinitions(env, dashboardPort, selfDashboard) {
|
|
|
588
589
|
runtime: 'live',
|
|
589
590
|
note: 'MCP-managed',
|
|
590
591
|
healthyMessage: 'Reachable',
|
|
592
|
+
impactsOverall: false,
|
|
591
593
|
},
|
|
592
594
|
{
|
|
593
595
|
id: 'opencode',
|
|
@@ -772,8 +774,8 @@ export async function buildStatus({
|
|
|
772
774
|
}
|
|
773
775
|
|
|
774
776
|
function serviceIcon(status) {
|
|
775
|
-
if (status === 'healthy') return '✓';
|
|
776
|
-
if (status === 'degraded'
|
|
777
|
+
if (status === 'healthy' || status === 'configured') return '✓';
|
|
778
|
+
if (status === 'degraded') return '⚠';
|
|
777
779
|
return '✗';
|
|
778
780
|
}
|
|
779
781
|
|
|
@@ -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.
|
|
@@ -312,7 +312,7 @@ function printFollowups() {
|
|
|
312
312
|
console.log('Follow-ups (run by hand if you want):');
|
|
313
313
|
console.log(' npm uninstall @geraldmaron/construct # drop the package itself');
|
|
314
314
|
console.log(' docker rmi pgvector/pgvector:pg16 # remove the cached pgvector image');
|
|
315
|
-
console.log(' brew uninstall cm cass # if installed by `construct
|
|
315
|
+
console.log(' brew uninstall cm cass # if installed by `construct init`');
|
|
316
316
|
}
|
|
317
317
|
|
|
318
318
|
function rel(base, target) {
|
package/package.json
CHANGED
|
@@ -80,7 +80,7 @@ construct doctor # verify everything's healthy
|
|
|
80
80
|
|
|
81
81
|
## Local services
|
|
82
82
|
|
|
83
|
-
If you ran `construct
|
|
83
|
+
If you ran `construct init` and have Docker, you have three things running locally on Construct's reserved port block (`54329-54339`, chosen to avoid colliding with Next.js, Postgres, Redis, etc.):
|
|
84
84
|
|
|
85
85
|
**Telemetry backend** (LLM observability — see your traces, costs, and quality scores)
|
|
86
86
|
|
|
@@ -105,7 +105,7 @@ All ports bind to `127.0.0.1` only; nothing is reachable from other machines on
|
|
|
105
105
|
|
|
106
106
|
| Command | What it does |
|
|
107
107
|
|---|---|
|
|
108
|
-
| `construct
|
|
108
|
+
| `construct init` | One-time per-machine: spins up local services, writes config |
|
|
109
109
|
| `construct config [mode <m>]` | Show active deployment mode (solo / team / enterprise) or set a new one |
|
|
110
110
|
| `construct doctor` | Health check across config, services, agents, hooks |
|
|
111
111
|
| `construct sync` | Regenerate platform adapters (Claude Code, OpenCode, Codex, Cursor) |
|
|
@@ -49,7 +49,7 @@ class Construct < Formula
|
|
|
49
49
|
def caveats
|
|
50
50
|
<<~EOS
|
|
51
51
|
To finish setup on this machine, run:
|
|
52
|
-
construct
|
|
52
|
+
construct init
|
|
53
53
|
|
|
54
54
|
Construct uses a local Postgres container (via Docker) for hybrid
|
|
55
55
|
retrieval. If Docker is not installed, Construct falls back to a JSON
|