@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.
- package/README.md +8 -7
- package/bin/construct +206 -62
- package/commands/plan/decide.md +1 -1
- package/commands/remember/runbook.md +1 -1
- package/examples/README.md +1 -1
- package/examples/distribution/README.md +1 -1
- package/examples/distribution/sources/adr.md +2 -2
- package/examples/distribution/sources/deck-one-pager.md +1 -1
- package/examples/distribution/sources/prd-platform.md +1 -1
- package/examples/distribution/sources/research-brief.md +2 -2
- package/examples/distribution/sources/rfc-platform.md +1 -1
- package/examples/distribution/sources/runbook.md +2 -2
- package/examples/distribution/sources/strategy.md +1 -1
- package/lib/agent-instructions/inject.mjs +1 -1
- package/lib/artifact-manifest.mjs +150 -4
- package/lib/artifact-type-from-path.mjs +9 -5
- package/lib/artifact-workflow.mjs +252 -0
- package/lib/audit-specialists.mjs +1 -1
- package/lib/audit-trail.mjs +3 -3
- package/lib/auto-docs.mjs +13 -11
- package/lib/bootstrap/built-ins.mjs +3 -3
- package/lib/boundary.mjs +3 -2
- package/lib/brand-prose.mjs +1 -1
- package/lib/cache-governor.js +4 -4
- package/lib/certification/document-io-fixtures.mjs +1 -1
- package/lib/chat/config.mjs +2 -2
- package/lib/chat/desktop-binary.mjs +5 -3
- package/lib/chat/present.mjs +1 -0
- package/lib/cli-commands.mjs +23 -6
- package/lib/cli-service-inventory.mjs +42 -0
- package/lib/comment-lint.mjs +9 -5
- package/lib/config/alias.mjs +3 -2
- package/lib/config/intake-policy.mjs +139 -0
- package/lib/config/project-config.mjs +106 -7
- package/lib/config/schema.mjs +45 -6
- package/lib/config/source-targets.mjs +311 -0
- package/lib/config/xdg.mjs +63 -0
- package/lib/contracts/violation-log.mjs +2 -2
- package/lib/cost-ledger.mjs +4 -3
- package/lib/cost.mjs +3 -2
- package/lib/decisions/registry.mjs +3 -3
- package/lib/distill.mjs +2 -2
- package/lib/docs-verify.mjs +2 -0
- package/lib/doctor/audit.mjs +4 -3
- package/lib/doctor/cli.mjs +0 -1
- package/lib/doctor/index.mjs +4 -4
- package/lib/doctor/report.mjs +3 -3
- package/lib/doctor/source-checkout.mjs +16 -0
- package/lib/doctor/watchers/bd-watch.mjs +5 -5
- package/lib/doctor/watchers/cost.mjs +4 -4
- package/lib/doctor/watchers/disk.mjs +4 -3
- package/lib/doctor/watchers/service-health.mjs +5 -5
- package/lib/document-export.mjs +39 -17
- package/lib/document-extract.mjs +3 -3
- package/lib/document-ingest.mjs +13 -3
- package/lib/efficiency.mjs +2 -1
- package/lib/embed/approval-queue.mjs +3 -2
- package/lib/embed/auto-sources.mjs +44 -0
- package/lib/embed/cli.mjs +9 -6
- package/lib/embed/config.mjs +6 -5
- package/lib/embed/daemon.mjs +6 -5
- package/lib/embed/demand-fetch.mjs +50 -19
- package/lib/embed/inbox-live-watcher.mjs +1 -1
- package/lib/embed/inbox.mjs +52 -41
- package/lib/embed/intake-metrics.mjs +3 -2
- package/lib/embed/reconcile.mjs +1 -1
- package/lib/embed/semantic.mjs +3 -2
- package/lib/embed/supervision.mjs +4 -2
- package/lib/embed/worker.mjs +2 -1
- package/lib/embedded-contract/index.mjs +11 -0
- package/lib/engine/registry.mjs +3 -3
- package/lib/env-config.mjs +34 -12
- package/lib/export-branding.mjs +32 -0
- package/lib/features.mjs +4 -3
- package/lib/health-check.mjs +7 -6
- package/lib/hook-health.mjs +5 -4
- package/lib/hooks/_lib/log.mjs +4 -3
- package/lib/hooks/_lib/output-mode.mjs +2 -1
- package/lib/hooks/agent-tracker.mjs +3 -4
- package/lib/hooks/audit-reads.mjs +2 -2
- package/lib/hooks/audit-trail.mjs +2 -2
- package/lib/hooks/bash-output-logger.mjs +3 -3
- package/lib/hooks/brand-prose-lint.mjs +1 -1
- package/lib/hooks/ci-status-check.mjs +3 -2
- package/lib/hooks/context-watch.mjs +2 -2
- package/lib/hooks/context-window-recovery.mjs +5 -5
- package/lib/hooks/dep-audit.mjs +2 -2
- package/lib/hooks/doc-coupling-check.mjs +3 -2
- package/lib/hooks/edit-accumulator.mjs +3 -2
- package/lib/hooks/edit-guard.mjs +3 -3
- package/lib/hooks/guard-bash.mjs +2 -2
- package/lib/hooks/model-fallback.mjs +2 -1
- package/lib/hooks/policy-engine.mjs +4 -3
- package/lib/hooks/pre-compact.mjs +7 -7
- package/lib/hooks/readme-age-check.mjs +2 -2
- package/lib/hooks/session-optimize.mjs +4 -3
- package/lib/hooks/session-start.mjs +20 -16
- package/lib/hooks/stop-notify.mjs +9 -8
- package/lib/hooks/stop-typecheck.mjs +4 -3
- package/lib/hooks/test-watch.mjs +2 -2
- package/lib/host-disposition.mjs +1 -1
- package/lib/ingest/provider-extract.mjs +2 -1
- package/lib/init/detect-existing-structure.mjs +2 -2
- package/lib/init/doc-lanes.mjs +3 -10
- package/lib/init-docs.mjs +12 -54
- package/lib/init-unified.mjs +47 -92
- package/lib/init.mjs +7 -5
- package/lib/install/desktop-binary-download.mjs +5 -2
- package/lib/intake/constants.mjs +29 -0
- package/lib/intake/daemon.mjs +31 -8
- package/lib/intake/intake-config.mjs +32 -112
- package/lib/integrations/intake-integrations.mjs +8 -7
- package/lib/knowledge/rag.mjs +2 -2
- package/lib/knowledge/search.mjs +15 -15
- package/lib/maintenance/cleanup.mjs +14 -10
- package/lib/mcp/server.mjs +20 -2
- package/lib/mcp/tools/embedded-contract.mjs +14 -0
- package/lib/mcp/tools/project.mjs +2 -1
- package/lib/model-cheapest-provider.mjs +4 -3
- package/lib/model-pricing.mjs +3 -2
- package/lib/model-router.mjs +4 -3
- package/lib/models/catalog.mjs +2 -1
- package/lib/models/execution-capability-profile.mjs +2 -1
- package/lib/models/provider-poll.mjs +2 -1
- package/lib/ollama-manager.mjs +0 -1
- package/lib/op-log.mjs +2 -1
- package/lib/opencode-runtime-plugin.mjs +3 -2
- package/lib/oracle/artifact-gate.mjs +3 -0
- package/lib/oracle/cli.mjs +2 -1
- package/lib/oracle/execute.mjs +3 -2
- package/lib/oracle/index.mjs +2 -1
- package/lib/oracle/read-model.mjs +3 -2
- package/lib/performance/generate.mjs +4 -3
- package/lib/platforms/capabilities.mjs +1 -1
- package/lib/playwright-demo.mjs +2 -0
- package/lib/plugin-registry.mjs +2 -1
- package/lib/profiles/lifecycle.mjs +3 -3
- package/lib/project-profile.mjs +2 -1
- package/lib/project-root.mjs +9 -7
- package/lib/provider-capabilities.js +4 -3
- package/lib/providers/auth-manager.mjs +2 -1
- package/lib/providers/copilot-auth.mjs +3 -2
- package/lib/providers/creds.mjs +3 -2
- package/lib/providers/registry.mjs +3 -3
- package/lib/providers/secret-resolver.mjs +3 -2
- package/lib/read-tracker-store.mjs +2 -1
- package/lib/reconcile/mcp-entry-reconcile.mjs +2 -2
- package/lib/reflect.mjs +2 -1
- package/lib/registry/generate-docs.mjs +2 -2
- package/lib/roles/approval-surface.mjs +5 -4
- package/lib/roles/event-bus.mjs +0 -1
- package/lib/roles/gateway.mjs +6 -2
- package/lib/rules-delivery.mjs +1 -1
- package/lib/runtime-pressure.mjs +5 -3
- package/lib/sandbox.mjs +3 -2
- package/lib/scheduler/solo.mjs +6 -4
- package/lib/server/auth.mjs +4 -3
- package/lib/server/index.mjs +53 -26
- package/lib/server/insights.mjs +5 -4
- package/lib/server/rate-limit.mjs +5 -3
- package/lib/server/webhook.mjs +5 -3
- package/lib/service-manager.mjs +5 -3
- package/lib/setup.mjs +9 -8
- package/lib/status.mjs +6 -5
- package/lib/storage/embeddings-local.mjs +3 -2
- package/lib/storage/sync.mjs +2 -2
- package/lib/telemetry/client.mjs +0 -1
- package/lib/telemetry/intent-verifications.mjs +6 -6
- package/lib/telemetry/model-pricing-catalog.mjs +4 -3
- package/lib/telemetry/rule-calls.mjs +3 -3
- package/lib/telemetry/skill-calls.mjs +3 -3
- package/lib/template-registry.mjs +1 -0
- package/lib/templates/visual-requirements.mjs +1 -1
- package/lib/test-corpus-inventory.mjs +1 -1
- package/lib/uninstall/uninstall.mjs +12 -10
- package/package.json +3 -3
- package/platforms/claude/settings.template.json +43 -43
- package/rules/common/no-fabrication.md +1 -1
- package/rules/common/release-gates.md +1 -1
- package/rules/common/research.md +1 -1
- package/rules/common/review-before-change.md +1 -1
- package/scripts/sync-specialists.mjs +11 -8
- package/skills/docs/adr-workflow.md +3 -3
- package/skills/docs/init-docs.md +9 -9
- package/skills/docs/prd-workflow.md +5 -5
- package/skills/docs/product-intelligence-review.md +1 -1
- package/skills/docs/product-intelligence-workflow.md +2 -2
- package/skills/docs/product-signal-workflow.md +1 -1
- package/skills/docs/runbook-workflow.md +4 -4
- package/skills/exploration/unknown-codebase-onboarding.md +1 -1
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/routing.md +3 -3
- package/specialists/artifact-manifest.json +11 -2
- package/specialists/artifact-manifest.schema.json +31 -1
- package/specialists/prompts/cx-architect.md +1 -1
- package/specialists/prompts/cx-docs-keeper.md +1 -1
- package/specialists/prompts/cx-researcher.md +1 -1
- package/specialists/prompts/cx-sre.md +1 -1
- package/specialists/role-manifests.json +6 -6
- package/templates/distribution/construct-reference.docx +0 -0
- package/templates/docs/README.md +125 -0
- package/templates/docs/construct_guide.md +3 -4
- package/templates/docs/persona-artifact.md +1 -1
- package/templates/docs/prds/README.md +25 -0
- package/templates/docs/prds/templates/_template.md +206 -0
- package/templates/docs/prds/templates/meta-prd.template.md +177 -0
- package/templates/docs/prds/templates/prd-business.template.md +61 -0
- package/templates/docs/prds/templates/prd-platform.template.md +81 -0
- package/templates/docs/prds/templates/prfaq.template.md +38 -0
- package/templates/docs/rfcs/README.md +22 -0
- package/templates/docs/rfcs/templates/_template.md +58 -0
- package/templates/docs/rfcs/templates/rfc-platform.template.md +72 -0
- package/templates/homebrew/construct.rb +1 -1
- 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
|
|
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
|
-
|
|
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 =
|
|
22
|
+
const dir = doctorRoot();
|
|
22
23
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
23
24
|
}
|
|
24
25
|
|
package/lib/roles/event-bus.mjs
CHANGED
|
@@ -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
|
|
package/lib/roles/gateway.mjs
CHANGED
|
@@ -11,15 +11,19 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { existsSync, readFileSync, appendFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
14
|
-
import {
|
|
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 ||
|
|
26
|
+
return process.env.CONSTRUCT_ROLES_ROOT || doctorRoot();
|
|
23
27
|
}
|
|
24
28
|
function pendingPath() {
|
|
25
29
|
return join(rootDir(), 'role-pending.jsonl');
|
package/lib/rules-delivery.mjs
CHANGED
|
@@ -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';
|
package/lib/runtime-pressure.mjs
CHANGED
|
@@ -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, '
|
|
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, '
|
|
379
|
+
<string>${plistEscape(path.join(stateDir(homeDir), 'runtime', 'pressure-release.log'))}</string>
|
|
378
380
|
<key>StandardErrorPath</key>
|
|
379
|
-
<string>${plistEscape(path.join(homeDir, '
|
|
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
|
-
|
|
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 });
|
package/lib/scheduler/solo.mjs
CHANGED
|
@@ -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(
|
|
87
|
+
<string>${path.join(doctorRoot(), 'scheduler', 'logs', `${id}.stdout.log`)}</string>
|
|
86
88
|
<key>StandardErrorPath</key>
|
|
87
|
-
<string>${path.join(
|
|
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(
|
|
124
|
-
StandardError=append:${path.join(
|
|
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 });
|
package/lib/server/auth.mjs
CHANGED
|
@@ -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
|
|
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, '
|
|
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(
|
|
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}`);
|
package/lib/server/index.mjs
CHANGED
|
@@ -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, '
|
|
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(
|
|
79
|
-
const APPROVAL_QUEUE_FILE = join(
|
|
80
|
-
const CONFIG_ENV_FILE = join(HOME, '
|
|
81
|
-
const EMBED_YAML_FILE = join(HOME, '
|
|
82
|
-
const CREDENTIAL_AUDIT_FILE = join(
|
|
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, '
|
|
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, '
|
|
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(
|
|
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
|
-
:
|
|
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(
|
|
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, '
|
|
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, '
|
|
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: {
|
|
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, '
|
|
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, '
|
|
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(
|
|
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(
|
|
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, '
|
|
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(
|
|
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(
|
|
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(
|
|
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, '
|
|
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(
|
|
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);
|
package/lib/server/insights.mjs
CHANGED
|
@@ -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(
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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
|
|
7
|
-
* chat
|
|
8
|
-
*
|
|
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/command — slash 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
|
|
package/lib/server/webhook.mjs
CHANGED
|
@@ -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, '
|
|
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
|
|
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 } };
|
package/lib/service-manager.mjs
CHANGED
|
@@ -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, '
|
|
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 ||
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
352
|
-
console.log(` · ${path.join(homeDir, '
|
|
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(
|
|
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(
|
|
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(
|
|
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, '
|
|
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 });
|