@geraldmaron/construct 1.2.0 → 1.2.2

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.
Files changed (214) hide show
  1. package/README.md +8 -7
  2. package/bin/construct +206 -62
  3. package/commands/plan/decide.md +1 -1
  4. package/commands/remember/runbook.md +1 -1
  5. package/examples/README.md +1 -1
  6. package/examples/distribution/README.md +1 -1
  7. package/examples/distribution/sources/adr.md +2 -2
  8. package/examples/distribution/sources/deck-one-pager.md +1 -1
  9. package/examples/distribution/sources/prd-platform.md +1 -1
  10. package/examples/distribution/sources/research-brief.md +2 -2
  11. package/examples/distribution/sources/rfc-platform.md +1 -1
  12. package/examples/distribution/sources/runbook.md +2 -2
  13. package/examples/distribution/sources/strategy.md +1 -1
  14. package/lib/agent-instructions/inject.mjs +1 -1
  15. package/lib/artifact-manifest.mjs +150 -4
  16. package/lib/artifact-type-from-path.mjs +9 -5
  17. package/lib/artifact-workflow.mjs +252 -0
  18. package/lib/audit-specialists.mjs +1 -1
  19. package/lib/audit-trail.mjs +3 -3
  20. package/lib/auto-docs.mjs +13 -11
  21. package/lib/bootstrap/built-ins.mjs +3 -3
  22. package/lib/boundary.mjs +3 -2
  23. package/lib/brand-prose.mjs +1 -1
  24. package/lib/cache-governor.js +4 -4
  25. package/lib/certification/document-io-fixtures.mjs +1 -1
  26. package/lib/chat/config.mjs +2 -2
  27. package/lib/chat/desktop-binary.mjs +5 -3
  28. package/lib/chat/present.mjs +1 -0
  29. package/lib/cli-commands.mjs +23 -6
  30. package/lib/cli-service-inventory.mjs +42 -0
  31. package/lib/comment-lint.mjs +9 -5
  32. package/lib/config/alias.mjs +3 -2
  33. package/lib/config/intake-policy.mjs +139 -0
  34. package/lib/config/project-config.mjs +106 -7
  35. package/lib/config/schema.mjs +45 -6
  36. package/lib/config/source-targets.mjs +311 -0
  37. package/lib/config/xdg.mjs +63 -0
  38. package/lib/contracts/violation-log.mjs +2 -2
  39. package/lib/cost-ledger.mjs +4 -3
  40. package/lib/cost.mjs +3 -2
  41. package/lib/decisions/registry.mjs +3 -3
  42. package/lib/distill.mjs +2 -2
  43. package/lib/docs-verify.mjs +2 -0
  44. package/lib/doctor/audit.mjs +4 -3
  45. package/lib/doctor/cli.mjs +0 -1
  46. package/lib/doctor/index.mjs +4 -4
  47. package/lib/doctor/report.mjs +3 -3
  48. package/lib/doctor/source-checkout.mjs +16 -0
  49. package/lib/doctor/watchers/bd-watch.mjs +5 -5
  50. package/lib/doctor/watchers/cost.mjs +4 -4
  51. package/lib/doctor/watchers/disk.mjs +4 -3
  52. package/lib/doctor/watchers/service-health.mjs +5 -5
  53. package/lib/document-export.mjs +39 -17
  54. package/lib/document-extract.mjs +3 -3
  55. package/lib/document-ingest.mjs +13 -3
  56. package/lib/efficiency.mjs +2 -1
  57. package/lib/embed/approval-queue.mjs +3 -2
  58. package/lib/embed/auto-sources.mjs +44 -0
  59. package/lib/embed/cli.mjs +9 -6
  60. package/lib/embed/config.mjs +6 -5
  61. package/lib/embed/daemon.mjs +6 -5
  62. package/lib/embed/demand-fetch.mjs +50 -19
  63. package/lib/embed/inbox-live-watcher.mjs +1 -1
  64. package/lib/embed/inbox.mjs +52 -41
  65. package/lib/embed/intake-metrics.mjs +3 -2
  66. package/lib/embed/reconcile.mjs +1 -1
  67. package/lib/embed/semantic.mjs +3 -2
  68. package/lib/embed/supervision.mjs +4 -2
  69. package/lib/embed/worker.mjs +2 -1
  70. package/lib/embedded-contract/index.mjs +11 -0
  71. package/lib/engine/registry.mjs +3 -3
  72. package/lib/env-config.mjs +34 -12
  73. package/lib/export-branding.mjs +32 -0
  74. package/lib/features.mjs +4 -3
  75. package/lib/health-check.mjs +7 -6
  76. package/lib/hook-health.mjs +5 -4
  77. package/lib/hooks/_lib/log.mjs +4 -3
  78. package/lib/hooks/_lib/output-mode.mjs +2 -1
  79. package/lib/hooks/agent-tracker.mjs +3 -4
  80. package/lib/hooks/audit-reads.mjs +2 -2
  81. package/lib/hooks/audit-trail.mjs +2 -2
  82. package/lib/hooks/bash-output-logger.mjs +3 -3
  83. package/lib/hooks/brand-prose-lint.mjs +1 -1
  84. package/lib/hooks/ci-status-check.mjs +3 -2
  85. package/lib/hooks/context-watch.mjs +2 -2
  86. package/lib/hooks/context-window-recovery.mjs +5 -5
  87. package/lib/hooks/dep-audit.mjs +2 -2
  88. package/lib/hooks/doc-coupling-check.mjs +3 -2
  89. package/lib/hooks/edit-accumulator.mjs +3 -2
  90. package/lib/hooks/edit-guard.mjs +3 -3
  91. package/lib/hooks/guard-bash.mjs +2 -2
  92. package/lib/hooks/model-fallback.mjs +2 -1
  93. package/lib/hooks/policy-engine.mjs +4 -3
  94. package/lib/hooks/pre-compact.mjs +7 -7
  95. package/lib/hooks/readme-age-check.mjs +2 -2
  96. package/lib/hooks/session-optimize.mjs +4 -3
  97. package/lib/hooks/session-start.mjs +20 -16
  98. package/lib/hooks/stop-notify.mjs +9 -8
  99. package/lib/hooks/stop-typecheck.mjs +4 -3
  100. package/lib/hooks/test-watch.mjs +2 -2
  101. package/lib/host-disposition.mjs +1 -1
  102. package/lib/ingest/provider-extract.mjs +2 -1
  103. package/lib/init/detect-existing-structure.mjs +2 -2
  104. package/lib/init/doc-lanes.mjs +3 -10
  105. package/lib/init-docs.mjs +12 -54
  106. package/lib/init-unified.mjs +47 -92
  107. package/lib/init.mjs +7 -5
  108. package/lib/install/desktop-binary-download.mjs +5 -2
  109. package/lib/intake/constants.mjs +29 -0
  110. package/lib/intake/daemon.mjs +31 -8
  111. package/lib/intake/intake-config.mjs +32 -112
  112. package/lib/integrations/intake-integrations.mjs +8 -7
  113. package/lib/knowledge/rag.mjs +2 -2
  114. package/lib/knowledge/search.mjs +15 -15
  115. package/lib/maintenance/cleanup.mjs +14 -10
  116. package/lib/mcp/server.mjs +20 -2
  117. package/lib/mcp/tools/embedded-contract.mjs +14 -0
  118. package/lib/mcp/tools/project.mjs +2 -1
  119. package/lib/model-cheapest-provider.mjs +4 -3
  120. package/lib/model-pricing.mjs +3 -2
  121. package/lib/model-router.mjs +4 -3
  122. package/lib/models/catalog.mjs +2 -1
  123. package/lib/models/execution-capability-profile.mjs +2 -1
  124. package/lib/models/provider-poll.mjs +2 -1
  125. package/lib/ollama-manager.mjs +0 -1
  126. package/lib/op-log.mjs +2 -1
  127. package/lib/opencode-runtime-plugin.mjs +3 -2
  128. package/lib/oracle/artifact-gate.mjs +3 -0
  129. package/lib/oracle/cli.mjs +2 -1
  130. package/lib/oracle/execute.mjs +3 -2
  131. package/lib/oracle/index.mjs +2 -1
  132. package/lib/oracle/read-model.mjs +3 -2
  133. package/lib/performance/generate.mjs +4 -3
  134. package/lib/platforms/capabilities.mjs +1 -1
  135. package/lib/playwright-demo.mjs +2 -0
  136. package/lib/plugin-registry.mjs +2 -1
  137. package/lib/profiles/lifecycle.mjs +3 -3
  138. package/lib/project-profile.mjs +2 -1
  139. package/lib/project-root.mjs +9 -7
  140. package/lib/provider-capabilities.js +4 -3
  141. package/lib/providers/auth-manager.mjs +2 -1
  142. package/lib/providers/copilot-auth.mjs +3 -2
  143. package/lib/providers/creds.mjs +3 -2
  144. package/lib/providers/registry.mjs +3 -3
  145. package/lib/providers/secret-resolver.mjs +3 -2
  146. package/lib/read-tracker-store.mjs +2 -1
  147. package/lib/reconcile/mcp-entry-reconcile.mjs +2 -2
  148. package/lib/reflect.mjs +2 -1
  149. package/lib/registry/generate-docs.mjs +2 -2
  150. package/lib/roles/approval-surface.mjs +5 -4
  151. package/lib/roles/event-bus.mjs +0 -1
  152. package/lib/roles/gateway.mjs +6 -2
  153. package/lib/rules-delivery.mjs +1 -1
  154. package/lib/runtime-pressure.mjs +5 -3
  155. package/lib/sandbox.mjs +3 -2
  156. package/lib/scheduler/solo.mjs +6 -4
  157. package/lib/server/auth.mjs +4 -3
  158. package/lib/server/index.mjs +53 -26
  159. package/lib/server/insights.mjs +5 -4
  160. package/lib/server/rate-limit.mjs +5 -3
  161. package/lib/server/webhook.mjs +5 -3
  162. package/lib/service-manager.mjs +5 -3
  163. package/lib/setup.mjs +9 -8
  164. package/lib/status.mjs +6 -5
  165. package/lib/storage/embeddings-local.mjs +3 -2
  166. package/lib/storage/sync.mjs +2 -2
  167. package/lib/telemetry/client.mjs +0 -1
  168. package/lib/telemetry/intent-verifications.mjs +6 -6
  169. package/lib/telemetry/model-pricing-catalog.mjs +4 -3
  170. package/lib/telemetry/rule-calls.mjs +3 -3
  171. package/lib/telemetry/skill-calls.mjs +3 -3
  172. package/lib/template-registry.mjs +1 -0
  173. package/lib/templates/visual-requirements.mjs +1 -1
  174. package/lib/test-corpus-inventory.mjs +1 -1
  175. package/lib/uninstall/uninstall.mjs +12 -10
  176. package/package.json +3 -3
  177. package/platforms/claude/settings.template.json +43 -43
  178. package/rules/common/no-fabrication.md +1 -1
  179. package/rules/common/release-gates.md +1 -1
  180. package/rules/common/research.md +1 -1
  181. package/rules/common/review-before-change.md +1 -1
  182. package/scripts/sync-specialists.mjs +11 -8
  183. package/skills/docs/adr-workflow.md +3 -3
  184. package/skills/docs/init-docs.md +9 -9
  185. package/skills/docs/prd-workflow.md +5 -5
  186. package/skills/docs/product-intelligence-review.md +1 -1
  187. package/skills/docs/product-intelligence-workflow.md +2 -2
  188. package/skills/docs/product-signal-workflow.md +1 -1
  189. package/skills/docs/runbook-workflow.md +4 -4
  190. package/skills/exploration/unknown-codebase-onboarding.md +1 -1
  191. package/skills/operating/orchestration-reference.md +1 -1
  192. package/skills/routing.md +3 -3
  193. package/specialists/artifact-manifest.json +11 -2
  194. package/specialists/artifact-manifest.schema.json +31 -1
  195. package/specialists/prompts/cx-architect.md +1 -1
  196. package/specialists/prompts/cx-docs-keeper.md +1 -1
  197. package/specialists/prompts/cx-researcher.md +1 -1
  198. package/specialists/prompts/cx-sre.md +1 -1
  199. package/specialists/role-manifests.json +6 -6
  200. package/templates/distribution/construct-reference.docx +0 -0
  201. package/templates/docs/README.md +125 -0
  202. package/templates/docs/construct_guide.md +3 -4
  203. package/templates/docs/persona-artifact.md +1 -1
  204. package/templates/docs/prds/README.md +25 -0
  205. package/templates/docs/prds/templates/_template.md +206 -0
  206. package/templates/docs/prds/templates/meta-prd.template.md +177 -0
  207. package/templates/docs/prds/templates/prd-business.template.md +61 -0
  208. package/templates/docs/prds/templates/prd-platform.template.md +81 -0
  209. package/templates/docs/prds/templates/prfaq.template.md +38 -0
  210. package/templates/docs/rfcs/README.md +22 -0
  211. package/templates/docs/rfcs/templates/_template.md +58 -0
  212. package/templates/docs/rfcs/templates/rfc-platform.template.md +72 -0
  213. package/templates/homebrew/construct.rb +1 -1
  214. package/templates/workflows/new-feature.yml +8 -8
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * When a persona-tagged session hits an action that lands in the fence's
5
5
  * `approvalRequired` bucket (commit, push, out-of-fence edit, etc.), this
6
- * module appends a marker to ~/.cx/approval-pending.jsonl and emits an SSE
6
+ * module appends a marker to <doctorRoot>/approval-pending.jsonl and emits an SSE
7
7
  * toast through the embed notification bus. The user sees a dashboard toast
8
8
  * instead of having to grep stderr.
9
9
  *
@@ -12,13 +12,14 @@
12
12
  */
13
13
 
14
14
  import { existsSync, appendFileSync, mkdirSync } from 'node:fs';
15
- import { homedir } from 'node:os';
16
15
  import { join } from 'node:path';
17
16
 
18
- const PENDING_PATH = join(homedir(), '.cx', 'approval-pending.jsonl');
17
+ import { doctorRoot } from '../config/xdg.mjs';
18
+
19
+ const PENDING_PATH = join(doctorRoot(), 'approval-pending.jsonl');
19
20
 
20
21
  function ensureDir() {
21
- const dir = join(homedir(), '.cx');
22
+ const dir = doctorRoot();
22
23
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
23
24
  }
24
25
 
@@ -14,7 +14,6 @@
14
14
 
15
15
  import { createHash } from 'node:crypto';
16
16
  import { existsSync, readFileSync, appendFileSync, writeFileSync, mkdirSync } from 'node:fs';
17
- import { homedir } from 'node:os';
18
17
  import { dirname, join } from 'node:path';
19
18
  import { resolveProjectScopedPath } from '../project-root.mjs';
20
19
 
@@ -11,15 +11,19 @@
11
11
  */
12
12
 
13
13
  import { existsSync, readFileSync, appendFileSync, writeFileSync, mkdirSync } from 'node:fs';
14
- import { homedir, tmpdir } from 'node:os';
14
+ import { tmpdir } from 'node:os';
15
15
  import { join } from 'node:path';
16
16
 
17
17
  import { emit, _paths } from './event-bus.mjs';
18
18
  import { route as routeEvent } from './router.mjs';
19
19
  import { runBd } from '../beads-client.mjs';
20
+ import { doctorRoot } from '../config/xdg.mjs';
21
+
22
+ // CONSTRUCT_ROLES_ROOT still wins for the roles event ecosystem's test
23
+ // isolation; the literal fallback follows the global doctor root instead of ~/.cx.
20
24
 
21
25
  function rootDir() {
22
- return process.env.CONSTRUCT_ROLES_ROOT || join(homedir(), '.cx');
26
+ return process.env.CONSTRUCT_ROLES_ROOT || doctorRoot();
23
27
  }
24
28
  function pendingPath() {
25
29
  return join(rootDir(), 'role-pending.jsonl');
@@ -10,7 +10,7 @@
10
10
  * carry the `construct-` prefix and are swept when their rule stops matching, so
11
11
  * user-authored .mdc files are never touched (ADR-0027 ownership contract).
12
12
  * Claude/Codex/OpenCode have no per-rule glob mechanism; for them rules remain
13
- * reference-delivered (cited by path in prose) — see docs/concepts/rules-delivery.md.
13
+ * reference-delivered (cited by path in prose) — see docs/guides/concepts/rules-delivery.md.
14
14
  */
15
15
  import fs from 'node:fs';
16
16
  import path from 'node:path';
@@ -10,6 +10,8 @@ import os from 'node:os';
10
10
  import path from 'node:path';
11
11
  import { spawnSync } from 'node:child_process';
12
12
 
13
+ import { stateDir } from './config/xdg.mjs';
14
+
13
15
  const DEFAULTS = Object.freeze({
14
16
  enabled: '1',
15
17
  intervalSeconds: '300',
@@ -115,7 +117,7 @@ function summarizeTermination(entries) {
115
117
 
116
118
  function warningStatePath(env = process.env) {
117
119
  const homeDir = env.HOME || os.homedir();
118
- return path.join(homeDir, '.construct', 'runtime', 'process-pressure-warnings.json');
120
+ return path.join(stateDir(homeDir), 'runtime', 'process-pressure-warnings.json');
119
121
  }
120
122
 
121
123
  function readWarningState(env = process.env) {
@@ -374,9 +376,9 @@ ${programArgs.map((arg) => ` <string>${plistEscape(arg)}</string>`).join('\
374
376
  <key>StartInterval</key>
375
377
  <integer>${intervalSeconds}</integer>
376
378
  <key>StandardOutPath</key>
377
- <string>${plistEscape(path.join(homeDir, '.construct', 'runtime', 'pressure-release.log'))}</string>
379
+ <string>${plistEscape(path.join(stateDir(homeDir), 'runtime', 'pressure-release.log'))}</string>
378
380
  <key>StandardErrorPath</key>
379
- <string>${plistEscape(path.join(homeDir, '.construct', 'runtime', 'pressure-release.log'))}</string>
381
+ <string>${plistEscape(path.join(stateDir(homeDir), 'runtime', 'pressure-release.log'))}</string>
380
382
  </dict>
381
383
  </plist>
382
384
  `;
package/lib/sandbox.mjs CHANGED
@@ -18,11 +18,12 @@
18
18
  * lives under `~/.cx/sandboxes/<id>/` so they survive across CLI sessions.
19
19
  */
20
20
  import fs from 'node:fs';
21
- import os from 'node:os';
22
21
  import path from 'node:path';
23
22
  import { spawnSync } from 'node:child_process';
24
23
 
25
- const ROOT = path.join(os.homedir(), '.cx', 'sandboxes');
24
+ import { doctorRoot } from './config/xdg.mjs';
25
+
26
+ const ROOT = path.join(doctorRoot(), 'sandboxes');
26
27
 
27
28
  function ensureRoot() {
28
29
  fs.mkdirSync(ROOT, { recursive: true });
@@ -20,6 +20,8 @@ import fs from 'node:fs';
20
20
  import path from 'node:path';
21
21
  import os from 'node:os';
22
22
 
23
+ import { doctorRoot } from '../config/xdg.mjs';
24
+
23
25
  // ---------------------------------------------------------------------------
24
26
  // Cron parsing helpers — minimal support for standard 5-field cron
25
27
  // ---------------------------------------------------------------------------
@@ -82,9 +84,9 @@ function writeLaunchdPlist(id, schedule, cwd) {
82
84
  ${calendarEntries}
83
85
  </dict>
84
86
  <key>StandardOutPath</key>
85
- <string>${path.join(os.homedir(), '.cx', 'scheduler', 'logs', `${id}.stdout.log`)}</string>
87
+ <string>${path.join(doctorRoot(), 'scheduler', 'logs', `${id}.stdout.log`)}</string>
86
88
  <key>StandardErrorPath</key>
87
- <string>${path.join(os.homedir(), '.cx', 'scheduler', 'logs', `${id}.stderr.log`)}</string>
89
+ <string>${path.join(doctorRoot(), 'scheduler', 'logs', `${id}.stderr.log`)}</string>
88
90
  <key>RunAtLoad</key>
89
91
  <false/>
90
92
  </dict>
@@ -120,8 +122,8 @@ Description=Construct job: ${id}
120
122
  Type=oneshot
121
123
  WorkingDirectory=${cwd}
122
124
  ExecStart=${constructBin} scheduler run ${id}
123
- StandardOutput=append:${path.join(os.homedir(), '.cx', 'scheduler', 'logs', `${id}.stdout.log`)}
124
- StandardError=append:${path.join(os.homedir(), '.cx', 'scheduler', 'logs', `${id}.stderr.log`)}
125
+ StandardOutput=append:${path.join(doctorRoot(), 'scheduler', 'logs', `${id}.stdout.log`)}
126
+ StandardError=append:${path.join(doctorRoot(), 'scheduler', 'logs', `${id}.stderr.log`)}
125
127
  `;
126
128
 
127
129
  fs.mkdirSync(unitDir, { recursive: true });
@@ -2,7 +2,7 @@
2
2
  * lib/server/auth.mjs — Dashboard authentication middleware.
3
3
  *
4
4
  * Token-based auth for the Construct dashboard. A single shared bearer token
5
- * is stored at ~/.construct/config.env under CONSTRUCT_DASHBOARD_TOKEN. If no
5
+ * is stored at config.env in the XDG config dir under CONSTRUCT_DASHBOARD_TOKEN. If no
6
6
  * token is configured the dashboard runs open (localhost-only by design). The
7
7
  * browser receives a short-lived session cookie on successful token exchange so
8
8
  * subsequent requests don't need to re-send the Authorization header.
@@ -16,9 +16,10 @@ import { randomBytes, timingSafeEqual } from 'crypto';
16
16
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
17
17
  import { join } from 'path';
18
18
  import { homedir } from 'os';
19
+ import { configDir } from '../config/xdg.mjs';
19
20
 
20
21
  const HOME = homedir();
21
- const CONFIG_ENV = join(HOME, '.construct', 'config.env');
22
+ const CONFIG_ENV = join(configDir(HOME), 'config.env');
22
23
  const SESSION_COOKIE = 'cx_session';
23
24
  const SESSION_TTL_MS = 8 * 60 * 60 * 1000; // 8 hours
24
25
  const SUPPORTED_OAUTH_PROVIDERS = new Set(['github', 'google']);
@@ -46,7 +47,7 @@ export function getDashboardToken() {
46
47
  }
47
48
 
48
49
  export function setDashboardToken(token) {
49
- mkdirSync(join(HOME, '.construct'), { recursive: true });
50
+ mkdirSync(configDir(HOME), { recursive: true });
50
51
  const env = existsSync(CONFIG_ENV) ? readFileSync(CONFIG_ENV, 'utf8') : '';
51
52
  const lines = env.split('\n').filter(l => !l.startsWith('CONSTRUCT_DASHBOARD_TOKEN='));
52
53
  lines.push(`CONSTRUCT_DASHBOARD_TOKEN=${token}`);
@@ -20,6 +20,7 @@ import { generateArtifact, listArtifacts } from '../embed/artifact.mjs';
20
20
  import { ApprovalQueue } from '../embed/approval-queue.mjs';
21
21
  import { resolveEmbedStatus } from '../embed/cli.mjs';
22
22
  import { loadConstructEnv, prepareConstructEnv } from '../runtime-env.mjs';
23
+ import { configDir, doctorRoot } from '../config/xdg.mjs';
23
24
  import {
24
25
  isAuthConfigured, isAuthenticated, rejectUnauthorized,
25
26
  validateToken, createSession, sessionCookieHeader, clearSessionCookieHeader,
@@ -71,15 +72,15 @@ function assertSafeBind() {
71
72
 
72
73
  const STATIC_DIR = join(__dirname, 'static');
73
74
  const REGISTRY_FILE = join(ROOT_DIR, 'specialists', 'registry.json');
74
- const FEATURES_FILE = join(HOME, '.construct', 'features.json');
75
+ const FEATURES_FILE = join(configDir(HOME), 'features.json');
75
76
  const WORKFLOW_FILE = join(ROOT_DIR, 'plan.md');
76
77
  const SKILLS_DIR = join(ROOT_DIR, 'skills');
77
78
  const COMMANDS_DIR = join(ROOT_DIR, 'commands');
78
- const SNAPSHOTS_FILE = join(HOME, '.cx', 'snapshots.jsonl');
79
- const APPROVAL_QUEUE_FILE = join(HOME, '.cx', 'approval-queue.jsonl');
80
- const CONFIG_ENV_FILE = join(HOME, '.construct', 'config.env');
81
- const EMBED_YAML_FILE = join(HOME, '.construct', 'embed.yaml');
82
- const CREDENTIAL_AUDIT_FILE = join(HOME, '.cx', 'credential-audit.jsonl');
79
+ const SNAPSHOTS_FILE = join(doctorRoot(), 'snapshots.jsonl');
80
+ const APPROVAL_QUEUE_FILE = join(doctorRoot(), 'approval-queue.jsonl');
81
+ const CONFIG_ENV_FILE = join(configDir(HOME), 'config.env');
82
+ const EMBED_YAML_FILE = join(configDir(HOME), 'embed.yaml');
83
+ const CREDENTIAL_AUDIT_FILE = join(doctorRoot(), 'credential-audit.jsonl');
83
84
 
84
85
  // Provider → expected env vars. Source of truth for the credentials surface,
85
86
  // the per-provider health classification ("not configured" vs "unhealthy"),
@@ -97,7 +98,7 @@ const BUILTIN_CREDENTIAL_MAP = [
97
98
  { provider: 'salesforce', label: 'Salesforce', kind: 'integration', envVars: ['SALESFORCE_INSTANCE_URL', 'SALESFORCE_ACCESS_TOKEN'] },
98
99
  ];
99
100
 
100
- const CUSTOM_CREDENTIALS_FILE = join(HOME, '.construct', 'custom-credentials.json');
101
+ const CUSTOM_CREDENTIALS_FILE = join(configDir(HOME), 'custom-credentials.json');
101
102
 
102
103
  // Env-var name validator: must match POSIX shell variable syntax and avoid
103
104
  // stomping built-in entries. Refusing $PATH / $HOME / OS-special prefixes
@@ -182,7 +183,7 @@ function resolveCredential(varName, env) {
182
183
  } catch { /* not available */ }
183
184
  }
184
185
 
185
- const paths = [join(homeDir, '.construct', 'config.env'), join(homeDir, '.env')];
186
+ const paths = [join(configDir(homeDir), 'config.env'), join(homeDir, '.env')];
186
187
  for (const p of paths) {
187
188
  try {
188
189
  if (existsSync(p)) {
@@ -588,7 +589,7 @@ function handleConfig(req, res) {
588
589
  const { type, content } = JSON.parse(body || '{}');
589
590
  if (type !== 'env' && type !== 'embed') throw new Error('type must be env or embed');
590
591
  if (typeof content !== 'string') throw new Error('content must be a string');
591
- mkdirSync(join(HOME, '.construct'), { recursive: true });
592
+ mkdirSync(configDir(HOME), { recursive: true });
592
593
  const target = type === 'env' ? CONFIG_ENV_FILE : EMBED_YAML_FILE;
593
594
  writeFileSync(target, content, 'utf8');
594
595
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -704,7 +705,9 @@ const server = createServer(async (req, res) => {
704
705
  const rateTier = url.pathname === '/api/chat/stream' || url.pathname === '/api/chat'
705
706
  || url.pathname === '/api/chat/loop/stream'
706
707
  ? 'chat'
707
- : (req.method !== 'GET' && req.method !== 'HEAD' ? 'write' : 'read');
708
+ : url.pathname === '/api/chat/loop/command'
709
+ ? 'command'
710
+ : (req.method !== 'GET' && req.method !== 'HEAD' ? 'write' : 'read');
708
711
  if (checkRateLimit) {
709
712
  const limit = checkRateLimit(req, rateTier, { env: process.env });
710
713
  if (!limit.allowed) {
@@ -1198,7 +1201,7 @@ const server = createServer(async (req, res) => {
1198
1201
 
1199
1202
  if (url.pathname === '/api/performance/reviews' && req.method === 'GET') {
1200
1203
  try {
1201
- const dir = join(HOME, '.cx', 'performance-reviews');
1204
+ const dir = join(doctorRoot(), 'performance-reviews');
1202
1205
  if (!existsSync(dir)) {
1203
1206
  res.writeHead(200, { 'Content-Type': 'application/json' });
1204
1207
  res.end(JSON.stringify({ reviews: [], total: 0, mockFiltered: 0, generatorImplemented: false }));
@@ -1325,7 +1328,7 @@ const server = createServer(async (req, res) => {
1325
1328
  if (url.pathname === '/api/mode' && req.method === 'GET') {
1326
1329
  try {
1327
1330
  const env = loadConstructEnv();
1328
- const embedYamlPath = join(HOME, '.construct', 'embed.yaml');
1331
+ const embedYamlPath = join(configDir(HOME), 'embed.yaml');
1329
1332
  const embedStatus = resolveEmbedStatus(env);
1330
1333
 
1331
1334
  // Determine mode based on embed status and configuration
@@ -1372,7 +1375,7 @@ const server = createServer(async (req, res) => {
1372
1375
  parentUrl,
1373
1376
  isEmbedded,
1374
1377
  embedStatus: embedStatus.level,
1375
- embedConfigExists: existsSync(join(HOME, '.construct', 'embed.yaml')),
1378
+ embedConfigExists: existsSync(join(configDir(HOME), 'embed.yaml')),
1376
1379
  capabilities: {
1377
1380
  modeDetection: true,
1378
1381
  snapshotStatus: true,
@@ -1580,6 +1583,32 @@ const server = createServer(async (req, res) => {
1580
1583
  return;
1581
1584
  }
1582
1585
 
1586
+ if (url.pathname === '/api/sources' && req.method === 'GET') {
1587
+ try {
1588
+ const { loadProjectConfig } = await import('../config/project-config.mjs');
1589
+ const {
1590
+ resolveEffectiveSourceTargetsFromConfig,
1591
+ legacyEnvSourceTargets,
1592
+ normalizeConfigTarget,
1593
+ } = await import('../config/source-targets.mjs');
1594
+ const loaded = loadProjectConfig(ROOT_DIR, process.env);
1595
+ const configTargets = (loaded.config.sources?.targets ?? []).map(normalizeConfigTarget);
1596
+ const envTargets = legacyEnvSourceTargets(process.env);
1597
+ const effective = resolveEffectiveSourceTargetsFromConfig(loaded.config, process.env);
1598
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1599
+ res.end(JSON.stringify({
1600
+ path: loaded.path,
1601
+ configTargets,
1602
+ envTargets,
1603
+ effective,
1604
+ }));
1605
+ } catch (err) {
1606
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1607
+ res.end(JSON.stringify({ error: err.message }));
1608
+ }
1609
+ return;
1610
+ }
1611
+
1583
1612
  if (url.pathname === '/api/intake/config' && req.method === 'GET') {
1584
1613
  try {
1585
1614
  const { loadIntakeConfig, INTAKE_DEPTH_GUIDANCE, INTAKE_HARD_MAX_DEPTH } = await import('../intake/intake-config.mjs');
@@ -1592,7 +1621,7 @@ const server = createServer(async (req, res) => {
1592
1621
  rootDir: ROOT_DIR,
1593
1622
  guidance: INTAKE_DEPTH_GUIDANCE,
1594
1623
  hardMaxDepth: INTAKE_HARD_MAX_DEPTH,
1595
- defaults: { projectInbox: '.cx/inbox', docsIntake: 'docs/intake' },
1624
+ defaults: { rootInbox: 'inbox/' },
1596
1625
  label: rebrand.intakeQueueLabel,
1597
1626
  itemNoun: rebrand.signalNoun,
1598
1627
  }));
@@ -1752,7 +1781,7 @@ const server = createServer(async (req, res) => {
1752
1781
 
1753
1782
  if (url.pathname === '/api/providers/subscriptions' && req.method === 'GET') {
1754
1783
  try {
1755
- const file = join(HOME, '.construct', 'provider-subscriptions.json');
1784
+ const file = join(configDir(HOME), 'provider-subscriptions.json');
1756
1785
  let data = { subscriptions: [] };
1757
1786
  if (existsSync(file)) {
1758
1787
  try { data = JSON.parse(readFileSync(file, 'utf8')); } catch { /* fall back to empty */ }
@@ -1782,7 +1811,7 @@ const server = createServer(async (req, res) => {
1782
1811
  res.writeHead(200, { 'Content-Type': 'application/json' });
1783
1812
  res.end(JSON.stringify({
1784
1813
  envPath: CONFIG_ENV_FILE,
1785
- overridesPath: join(HOME, '.construct', 'providers.json'),
1814
+ overridesPath: join(configDir(HOME), 'providers.json'),
1786
1815
  }));
1787
1816
  return;
1788
1817
  }
@@ -2270,7 +2299,7 @@ const server = createServer(async (req, res) => {
2270
2299
  if (!resolved) throw new Error('op read returned an empty value');
2271
2300
 
2272
2301
  const { writeEnvValues, loadConstructEnv } = await import('../env-config.mjs');
2273
- mkdirSync(join(HOME, '.construct'), { recursive: true });
2302
+ mkdirSync(configDir(HOME), { recursive: true });
2274
2303
  writeEnvValues(CONFIG_ENV_FILE, { [envVar]: resolved });
2275
2304
  process.env[envVar] = resolved;
2276
2305
  try { chmodSync(CONFIG_ENV_FILE, 0o600); } catch { /* perms best-effort */ }
@@ -2282,7 +2311,7 @@ const server = createServer(async (req, res) => {
2282
2311
  }
2283
2312
 
2284
2313
  try {
2285
- mkdirSync(join(HOME, '.cx'), { recursive: true });
2314
+ mkdirSync(doctorRoot(), { recursive: true });
2286
2315
  appendFileSync(CREDENTIAL_AUDIT_FILE, JSON.stringify({ ts: new Date().toISOString(), action: 'set', envVar, source: '1password', opRef }) + '\n');
2287
2316
  } catch { /* audit best-effort */ }
2288
2317
 
@@ -2339,8 +2368,6 @@ const server = createServer(async (req, res) => {
2339
2368
  const next = saveIntakeConfig(ROOT_DIR, {
2340
2369
  parentDirs: Array.isArray(data.parentDirs) ? data.parentDirs : undefined,
2341
2370
  maxDepth: data.maxDepth,
2342
- includeProjectInbox: data.includeProjectInbox,
2343
- includeDocsIntake: data.includeDocsIntake,
2344
2371
  });
2345
2372
  res.writeHead(200, { 'Content-Type': 'application/json' });
2346
2373
  res.end(JSON.stringify({ success: true, config: next }));
@@ -2377,7 +2404,7 @@ const server = createServer(async (req, res) => {
2377
2404
  }
2378
2405
  } else if (url.pathname === '/api/providers/registry') {
2379
2406
  const { BUILT_INS } = await import('../providers/registry.mjs');
2380
- const overridesPath = join(HOME, '.construct', 'providers.json');
2407
+ const overridesPath = join(configDir(HOME), 'providers.json');
2381
2408
  const { action, id, package: pkg, options } = data;
2382
2409
  if (!id || typeof id !== 'string' || !/^[\w-]+$/.test(id)) throw new Error('Invalid provider id');
2383
2410
  if (BUILT_INS.includes(id)) throw new Error(`cannot override built-in provider '${id}'`);
@@ -2403,7 +2430,7 @@ const server = createServer(async (req, res) => {
2403
2430
  throw new Error('action must be save or delete');
2404
2431
  }
2405
2432
 
2406
- mkdirSync(join(HOME, '.construct'), { recursive: true });
2433
+ mkdirSync(configDir(HOME), { recursive: true });
2407
2434
  const tmp = overridesPath + '.tmp';
2408
2435
  writeFileSync(tmp, JSON.stringify(current, null, 2) + '\n');
2409
2436
  renameSync(tmp, overridesPath);
@@ -2431,7 +2458,7 @@ const server = createServer(async (req, res) => {
2431
2458
  if (value !== '' && typeof value !== 'string') throw new Error('value must be a string');
2432
2459
 
2433
2460
  const { writeEnvValues, parseEnvFile, loadConstructEnv } = await import('../env-config.mjs');
2434
- mkdirSync(join(HOME, '.construct'), { recursive: true });
2461
+ mkdirSync(configDir(HOME), { recursive: true });
2435
2462
  const action = value === '' ? 'unset' : 'set';
2436
2463
 
2437
2464
  if (action === 'unset') {
@@ -2451,7 +2478,7 @@ const server = createServer(async (req, res) => {
2451
2478
  }
2452
2479
 
2453
2480
  try {
2454
- mkdirSync(join(HOME, '.cx'), { recursive: true });
2481
+ mkdirSync(doctorRoot(), { recursive: true });
2455
2482
  appendFileSync(CREDENTIAL_AUDIT_FILE, JSON.stringify({ ts: new Date().toISOString(), action, envVar }) + '\n');
2456
2483
  } catch { /* audit append best-effort */ }
2457
2484
 
@@ -2459,7 +2486,7 @@ const server = createServer(async (req, res) => {
2459
2486
  res.end(JSON.stringify({ success: true }));
2460
2487
  notifyClients();
2461
2488
  } else if (url.pathname === '/api/providers/subscriptions') {
2462
- const subsPath = join(HOME, '.construct', 'provider-subscriptions.json');
2489
+ const subsPath = join(configDir(HOME), 'provider-subscriptions.json');
2463
2490
  const { action, id, provider, name, config } = data;
2464
2491
  if (!id || typeof id !== 'string' || !/^[\w.-]+$/.test(id)) throw new Error('Invalid subscription id');
2465
2492
 
@@ -2480,7 +2507,7 @@ const server = createServer(async (req, res) => {
2480
2507
  throw new Error('action must be save or delete');
2481
2508
  }
2482
2509
 
2483
- mkdirSync(join(HOME, '.construct'), { recursive: true });
2510
+ mkdirSync(configDir(HOME), { recursive: true });
2484
2511
  const tmp = subsPath + '.tmp';
2485
2512
  writeFileSync(tmp, JSON.stringify(current, null, 2) + '\n');
2486
2513
  renameSync(tmp, subsPath);
@@ -15,6 +15,7 @@ import path from 'node:path';
15
15
  import { spawnSync } from 'node:child_process';
16
16
  import { readCostLog, summarizeCostData } from '../cost.mjs';
17
17
  import { getRebrand } from '../profiles/rebrand.mjs';
18
+ import { configDir, doctorRoot } from '../config/xdg.mjs';
18
19
 
19
20
  const TELEMETRY_TIMEOUT_MS = 3500;
20
21
 
@@ -82,7 +83,7 @@ async function fetchTelemetrySummary(env, { limit = 100 } = {}) {
82
83
  // Fallback: read from local .cx/traces/ when Telemetry is unreachable or not configured.
83
84
  // The daemon always writes heartbeat, lifecycle, and probe events to local JSONL files.
84
85
  try {
85
- const traceDir = path.join(os.homedir(), '.cx', 'traces');
86
+ const traceDir = path.join(doctorRoot(), 'traces');
86
87
  if (!fs.existsSync(traceDir)) return { state: 'empty', message: 'No local traces. Start the daemon with `construct dev`.' };
87
88
  const files = fs.readdirSync(traceDir).filter(f => f.endsWith('.jsonl')).sort().reverse();
88
89
  if (!files.length) return { state: 'empty', message: 'No local trace files.' };
@@ -412,7 +413,7 @@ function resolveModelCredential(primaryKey, altKey) {
412
413
  } catch { /* not available */ }
413
414
  }
414
415
 
415
- const paths = [path.join(homeDir, '.construct', 'config.env'), path.join(homeDir, '.env')];
416
+ const paths = [path.join(configDir(homeDir), 'config.env'), path.join(homeDir, '.env')];
416
417
  for (const p of paths) {
417
418
  try {
418
419
  if (!fs.existsSync(p)) continue;
@@ -619,7 +620,7 @@ async function summarizeTelemetryLatency(env, { days = 7, limit = 50 } = {}) {
619
620
 
620
621
  function summarizeRetrievalEval(homeDir = os.homedir()) {
621
622
  try {
622
- const cachePath = path.join(homeDir, '.cx', 'evals', 'retrieval-latest.json');
623
+ const cachePath = path.join(doctorRoot(homeDir), 'evals', 'retrieval-latest.json');
623
624
  if (!fs.existsSync(cachePath)) {
624
625
  return {
625
626
  state: 'empty',
@@ -630,7 +631,7 @@ function summarizeRetrievalEval(homeDir = os.homedir()) {
630
631
  const r = cached.report?.summary || cached.report || {};
631
632
  let history = [];
632
633
  try {
633
- const historyPath = path.join(homeDir, '.cx', 'evals', 'retrieval-history.jsonl');
634
+ const historyPath = path.join(doctorRoot(homeDir), 'evals', 'retrieval-history.jsonl');
634
635
  if (fs.existsSync(historyPath)) {
635
636
  const lines = fs.readFileSync(historyPath, 'utf8').split('\n').filter(Boolean);
636
637
  history = lines.slice(-30).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * Three tiers, configurable via env:
5
5
  *
6
- * read 60 req/min/key (dashboard polling, list endpoints)
7
- * chat 10 req/min/key (/api/chat/stream — bursty + expensive)
8
- * write 5 req/min/key (state-mutating endpoints approvals, config)
6
+ * read 60 req/min/key (dashboard polling, list endpoints)
7
+ * chat 10 req/min/key (/api/chat/stream — bursty + expensive)
8
+ * command 120 req/min/key (/api/chat/loop/commandslash cmds, no LLM)
9
+ * write 5 req/min/key (state-mutating endpoints — approvals, config)
9
10
  *
10
11
  * `key` is by default the request's source IP, with the active session id
11
12
  * mixed in when present so two users behind the same NAT get separate
@@ -18,6 +19,7 @@
18
19
  const DEFAULT_TIERS = {
19
20
  read: { capacity: 60, refillPerMs: 60 / 60_000 },
20
21
  chat: { capacity: 10, refillPerMs: 10 / 60_000 },
22
+ command: { capacity: 120, refillPerMs: 120 / 60_000 },
21
23
  write: { capacity: 5, refillPerMs: 5 / 60_000 },
22
24
  };
23
25
 
@@ -38,6 +38,7 @@ import { existsSync, readFileSync } from 'fs';
38
38
  import { join } from 'path';
39
39
  import { fileURLToPath } from 'url';
40
40
  import { homedir as osHomedir } from 'os';
41
+ import { configDir } from '../config/xdg.mjs';
41
42
 
42
43
  const __dirname = fileURLToPath(new URL('.', import.meta.url));
43
44
  const ROOT_DIR = join(__dirname, '..', '..');
@@ -282,7 +283,7 @@ async function postSlackMessage(responseUrl, text, botToken) {
282
283
  * Load config.env values as an object.
283
284
  */
284
285
  function loadConfigEnv() {
285
- const envFile = join(HOME, '.construct', 'config.env');
286
+ const envFile = join(configDir(HOME), 'config.env');
286
287
  if (!existsSync(envFile)) return {};
287
288
  const lines = readFileSync(envFile, 'utf8').split('\n');
288
289
  const out = {};
@@ -306,8 +307,9 @@ async function runSnapshotForSlack() {
306
307
  const { SnapshotEngine, renderMarkdown } = await import('../embed/snapshot.mjs');
307
308
  const { EMPTY_CONFIG } = await import('../embed/config.mjs');
308
309
 
309
- const registry = ProviderRegistry.fromEnv(env);
310
- const sources = registry.autoSources(env);
310
+ const registry = await ProviderRegistry.fromEnv(env);
311
+ const { resolveAutoEmbedSources } = await import('../embed/auto-sources.mjs');
312
+ const sources = resolveAutoEmbedSources({ cwd: process.cwd(), env, registry });
311
313
  if (!sources.length) return '⚠️ No embed sources configured. Add credentials to config.env.';
312
314
 
313
315
  const config = { ...EMPTY_CONFIG, sources, snapshot: { maxItems: 10 } };
@@ -20,13 +20,15 @@ import { resolveTraceBackend, telemetryProviderLabel } from './telemetry/client.
20
20
  import { memoryPort as derivedMemoryPort } from './home-namespace.mjs';
21
21
  import { readHeartbeat } from './daemons/contract.mjs';
22
22
  import { heartbeatPath } from './oracle/index.mjs';
23
+ import { stateDir } from './config/xdg.mjs';
24
+ import { doctorRoot } from './config/xdg.mjs';
23
25
 
24
26
  const DASHBOARD_STATE_FILE = 'dashboard.json';
25
27
 
26
28
  const INSTALL_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
27
29
 
28
30
  function runtimeStateDir(homeDir = os.homedir()) {
29
- return path.join(homeDir, '.construct', 'runtime');
31
+ return path.join(stateDir(homeDir), 'runtime');
30
32
  }
31
33
 
32
34
  function dashboardStatePath(homeDir = os.homedir()) {
@@ -84,7 +86,7 @@ export function buildRuntimeRecoverySummary({ rootDir, homeDir = os.homedir(), r
84
86
  latestHandoff: relativeOrNull(rootDir, latestHandoffPath(rootDir)),
85
87
  beads: fs.existsSync(path.join(rootDir, '.beads', 'metadata.json')) ? '.beads/metadata.json' : null,
86
88
  userConfig: fs.existsSync(getUserEnvPath(homeDir)) ? getUserEnvPath(homeDir) : null,
87
- userDataRoot: env.CX_DATA_DIR || path.join(homeDir, '.cx'),
89
+ userDataRoot: env.CX_DATA_DIR || doctorRoot(homeDir),
88
90
  };
89
91
 
90
92
  return {
@@ -215,7 +217,7 @@ export function stopDashboard(homeDir = os.homedir()) {
215
217
  }
216
218
 
217
219
  function doctorStatePath(homeDir) {
218
- return path.join(homeDir, '.construct', 'doctor.json');
220
+ return path.join(stateDir(homeDir), 'doctor.json');
219
221
  }
220
222
 
221
223
  export function readDoctorState(homeDir = os.homedir()) {
package/lib/setup.mjs CHANGED
@@ -22,6 +22,7 @@ import path from 'node:path';
22
22
  import { spawn, spawnSync } from 'node:child_process';
23
23
 
24
24
  import { ensureUserConfigDir, getUserEnvPath, writeEnvValues } from './env-config.mjs';
25
+ import { configDir, stateDir, doctorRoot } from './config/xdg.mjs';
25
26
  import { DEFAULT_WORKSPACE_PATH, WORKSPACE_DOCS_LANES } from './embed/config.mjs';
26
27
  import {
27
28
  DEFAULT_DEPLOYMENT_MODE,
@@ -97,7 +98,7 @@ function findCommand(command) {
97
98
 
98
99
  export function defaultVectorIndexPath(homeDir = HOME) {
99
100
  // LanceDB storage path
100
- return path.join(homeDir, '.construct', 'vector', 'lancedb');
101
+ return path.join(stateDir(homeDir), 'vector', 'lancedb');
101
102
  }
102
103
 
103
104
  export async function buildManagedSetupValues({ homeDir = HOME, env = process.env } = {}) {
@@ -266,7 +267,7 @@ function ensureOpenCodeConfig() {
266
267
  export { ensureGitHooksPath } from './git-hooks-path.mjs';
267
268
 
268
269
  export function ensureLibSymlink({ homeDir = HOME, rootDir = ROOT_DIR } = {}) {
269
- const target = path.join(homeDir, '.construct', 'lib');
270
+ const target = path.join(configDir(homeDir), 'lib');
270
271
  const source = path.join(rootDir, 'lib');
271
272
  fs.mkdirSync(path.dirname(target), { recursive: true });
272
273
  let stat = null;
@@ -348,12 +349,12 @@ function printInstallPlan({ scope, homeDir, withDocling, noLaunchAgent, cleanupL
348
349
  console.log('\nWould write:');
349
350
  console.log(` · ${getUserEnvPath(homeDir)} (user config, managed defaults)`);
350
351
  console.log(` · ${getCanonicalOpenCodeConfigPath(homeDir)} (OpenCode config)`);
351
- console.log(` · ${path.join(homeDir, '.construct', 'lib')} → ${path.join(ROOT_DIR, 'lib')} (hook lib symlink)`);
352
- console.log(` · ${path.join(homeDir, '.construct', 'workspace')} (workspace docs scaffold)`);
352
+ console.log(` · ${path.join(configDir(homeDir), 'lib')} → ${path.join(ROOT_DIR, 'lib')} (hook lib symlink)`);
353
+ console.log(` · ${path.join(stateDir(homeDir), 'workspace')} (workspace docs scaffold)`);
353
354
  console.log(` · ${path.dirname(defaultVectorIndexPath(homeDir))} (LanceDB vector store dir)`);
354
355
  console.log('\nWould check / install runtime tooling:');
355
356
  console.log(' · cm (memory CLI) and cass (session search) when available on PATH');
356
- console.log(' · construct-chat desktop binary (from GitHub releases → ~/.construct/bin/)');
357
+ console.log(` · construct-chat desktop binary (from GitHub releases → ${path.join(stateDir(homeDir), 'bin')})`);
357
358
  console.log('\nWould reach the network / external services:');
358
359
  console.log(' · embedding model warmup (loads from local cache; offline-safe, no download)');
359
360
  if (withDocling) console.log(' · docling Python venv provisioning via uv (~10 min)');
@@ -587,7 +588,7 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
587
588
  runConstruct(['doctor']);
588
589
 
589
590
  const setupTs = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-');
590
- const setupLogPath = path.join(HOME, '.cx', `setup-${setupTs}.log`);
591
+ const setupLogPath = path.join(doctorRoot(), `setup-${setupTs}.log`);
591
592
  try {
592
593
  fs.mkdirSync(path.dirname(setupLogPath), { recursive: true });
593
594
  const logLines = [
@@ -605,7 +606,7 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
605
606
  console.log(' Traces: local JSONL (.cx/traces)');
606
607
  console.log(` Vector: LanceDB (embedded)`);
607
608
  console.log(` ${managedValues.CONSTRUCT_LANCEDB_PATH}`);
608
- console.log(' Credentials are saved to ~/.construct/config.env for later reference.');
609
+ console.log(` Credentials are saved to ${getUserEnvPath(homeDir)} for later reference.`);
609
610
 
610
611
  console.log('\nNext steps:');
611
612
  console.log(' construct provider add github # Connect GitHub repository data');
@@ -621,7 +622,7 @@ function ensureUserConfig(homeDir = HOME) {
621
622
  }
622
623
 
623
624
  export function ensureWorkspace(homeDir = HOME) {
624
- const wsPath = path.join(homeDir, '.construct', 'workspace');
625
+ const wsPath = path.join(stateDir(homeDir), 'workspace');
625
626
  const docsPath = path.join(wsPath, 'docs');
626
627
  for (const lane of WORKSPACE_DOCS_LANES) {
627
628
  fs.mkdirSync(path.join(docsPath, lane), { recursive: true });