@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.
@@ -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';
@@ -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 setup.` };
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();
@@ -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 setup first' };
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 setup or brew install dicklesworthstone/tap/cm' });
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
- // We can't use fetch against Postgres (it's not HTTP), so just check pg_isready via docker
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', [
@@ -1,5 +1,5 @@
1
1
  /**
2
- * lib/setup-prompts.mjs — yes/no consent helper for `construct setup`.
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 setup` don't re-prompt for the same
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 setup` and on first init.
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 setup [--yes] [--no-docker]
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 setup --yes');
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 setup` };
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' || status === 'configured') return '⚠';
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 setup → restoreConstructDb (reload after fresh container start)
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 setup`');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geraldmaron/construct",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "description": "Construct — agent orchestration layer for OpenCode, Claude Code, and other coding surfaces",
6
6
  "bin": {
@@ -80,7 +80,7 @@ construct doctor # verify everything's healthy
80
80
 
81
81
  ## Local services
82
82
 
83
- If you ran `construct setup` 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.):
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 setup` | One-time per-machine: spins up local services, writes config |
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 setup
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