@geraldmaron/construct 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/apps/chat/engine/ai-sdk-agent.mjs +183 -0
- package/apps/chat/engine/loop-driver.mjs +211 -0
- package/apps/chat/engine/models.mjs +122 -0
- package/apps/chat/engine/provider-adapters.mjs +171 -0
- package/apps/chat/engine/tools/permission.mjs +54 -0
- package/apps/chat/engine/tools/primitives.mjs +180 -0
- package/apps/chat/engine/tools/registry.mjs +122 -0
- package/apps/chat/engine/turn-controls.mjs +70 -0
- package/bin/construct +93 -79
- 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-type-from-path.mjs +9 -5
- package/lib/audit-specialists.mjs +1 -1
- package/lib/audit-trail.mjs +3 -3
- package/lib/auto-docs.mjs +10 -10
- 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/cli-commands.mjs +4 -6
- package/lib/cli-service-inventory.mjs +1 -1
- package/lib/comment-lint.mjs +9 -5
- package/lib/config/alias.mjs +3 -2
- package/lib/config/intake-policy.mjs +8 -78
- package/lib/config/legacy-config-migration.mjs +59 -0
- package/lib/config/project-config.mjs +95 -5
- package/lib/config/schema.mjs +9 -19
- 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/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 +1 -1
- 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/cli.mjs +7 -5
- package/lib/embed/config.mjs +6 -5
- package/lib/embed/daemon.mjs +6 -5
- package/lib/embed/inbox-live-watcher.mjs +1 -1
- package/lib/embed/inbox.mjs +52 -37
- 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/engine/registry.mjs +3 -3
- package/lib/env-config.mjs +34 -12
- 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 +6 -5
- 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 +35 -81
- package/lib/init.mjs +7 -5
- package/lib/install/desktop-binary-download.mjs +5 -2
- package/lib/intake/daemon.mjs +31 -8
- package/lib/intake/intake-config.mjs +5 -32
- 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 +1 -1
- 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/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-env.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 +24 -26
- package/lib/server/insights.mjs +5 -4
- package/lib/server/webhook.mjs +2 -1
- package/lib/service-manager.mjs +5 -3
- package/lib/setup.mjs +21 -11
- 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 +4 -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/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/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
- package/lib/intake/legacy-paths.mjs +0 -5
package/lib/project-root.mjs
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
* Writers that own per-project state (contract violations, audit reads, agent
|
|
5
5
|
* dispatches, intent verifications) call `resolveProjectScope()` to decide
|
|
6
6
|
* which `<project>/.cx/<file>.jsonl` to write to. When no project is
|
|
7
|
-
* detected, the call returns `null` and writers fall back to the
|
|
8
|
-
* user-scope path (
|
|
7
|
+
* detected, the call returns `null` and writers fall back to the global
|
|
8
|
+
* user-scope path (`<doctorRoot>/<file>.jsonl`) so existing standalone hook
|
|
9
9
|
* invocations don't lose data.
|
|
10
10
|
*
|
|
11
11
|
* Writers that own cross-project telemetry (skill calls, role-pending,
|
|
12
|
-
* session cost) keep using
|
|
12
|
+
* session cost) keep using the global doctor root but tag each entry with the
|
|
13
13
|
* `projectId` from `resolveProjectId()` so a reader can attribute the
|
|
14
14
|
* entry to a specific project later.
|
|
15
15
|
*
|
|
@@ -24,6 +24,8 @@ import path from 'node:path';
|
|
|
24
24
|
import os from 'node:os';
|
|
25
25
|
import { createHash } from 'node:crypto';
|
|
26
26
|
|
|
27
|
+
import { doctorRoot } from './config/xdg.mjs';
|
|
28
|
+
|
|
27
29
|
const HOME = os.homedir();
|
|
28
30
|
const MARKERS = ['.cx', '.construct'];
|
|
29
31
|
const cache = new Map(); // cwd → { projectRoot, projectId, scope } | null
|
|
@@ -84,13 +86,13 @@ export function resolveProjectScope(cwd = process.cwd()) {
|
|
|
84
86
|
|
|
85
87
|
/**
|
|
86
88
|
* For project-scoped writers: returns the `<project>/.cx/<basename>` path
|
|
87
|
-
* when cwd is inside a Construct project, otherwise the
|
|
88
|
-
*
|
|
89
|
-
* resolves the directory side. ensureDir=true creates the parent dir.
|
|
89
|
+
* when cwd is inside a Construct project, otherwise the global
|
|
90
|
+
* `<doctorRoot>/<basename>` path. Callers pass just the file basename; this
|
|
91
|
+
* helper resolves the directory side. ensureDir=true creates the parent dir.
|
|
90
92
|
*/
|
|
91
93
|
export function resolveProjectScopedPath(basename, { cwd, ensureDir = true } = {}) {
|
|
92
94
|
const scope = resolveProjectScope(cwd ?? process.cwd());
|
|
93
|
-
const dir = scope ? scope.cxDir :
|
|
95
|
+
const dir = scope ? scope.cxDir : doctorRoot();
|
|
94
96
|
if (ensureDir && !fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
95
97
|
return path.join(dir, basename);
|
|
96
98
|
}
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
|
-
import { homedir } from 'node:os';
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
import { doctorRoot } from './config/xdg.mjs';
|
|
23
|
+
|
|
24
|
+
const CAPABILITY_CACHE_PATH = join(doctorRoot(), 'provider-capabilities.json');
|
|
24
25
|
const CAPABILITY_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
25
26
|
|
|
26
27
|
const ADAPTERS = {
|
|
@@ -110,7 +111,7 @@ function readCapabilityCache() {
|
|
|
110
111
|
|
|
111
112
|
function writeCapabilityCache(capabilities) {
|
|
112
113
|
try {
|
|
113
|
-
mkdirSync(
|
|
114
|
+
mkdirSync(doctorRoot(), { recursive: true });
|
|
114
115
|
writeFileSync(CAPABILITY_CACHE_PATH, JSON.stringify({
|
|
115
116
|
fetchedAt: Date.now(),
|
|
116
117
|
capabilities,
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
|
+
import { configDir } from '../config/xdg.mjs';
|
|
14
15
|
|
|
15
16
|
function authDir() {
|
|
16
17
|
const home = process.env.HOME || homedir();
|
|
17
|
-
const dir = path.join(home, '
|
|
18
|
+
const dir = path.join(configDir(home), 'auth');
|
|
18
19
|
try { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } catch { /* ignore */ }
|
|
19
20
|
return dir;
|
|
20
21
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* its token is rejected by the exchange endpoint.
|
|
12
12
|
*
|
|
13
13
|
* Credentials persist to two places: Construct's own store
|
|
14
|
-
* (
|
|
14
|
+
* (github-copilot.json under the XDG config dir auth/) and the de-facto standard
|
|
15
15
|
* ~/.config/github-copilot/apps.json that other tools read and write, so a login
|
|
16
16
|
* here is reusable elsewhere and an existing login is reused here. The access
|
|
17
17
|
* token is refreshed from the refresh token when it expires; the session token
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import fs from 'node:fs';
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
import os from 'node:os';
|
|
25
|
+
import { configDir } from '../config/xdg.mjs';
|
|
25
26
|
|
|
26
27
|
const CLIENT_ID = 'Iv1.b507a08c87ecfe98';
|
|
27
28
|
const DEVICE_CODE_URL = 'https://github.com/login/device/code';
|
|
@@ -53,7 +54,7 @@ function homeDir() {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
function constructStorePath() {
|
|
56
|
-
return path.join(homeDir(), '
|
|
57
|
+
return path.join(configDir(homeDir()), 'auth', 'github-copilot.json');
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
function appsStorePath() {
|
package/lib/providers/creds.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/providers/creds.mjs — credential management for provider integrations.
|
|
3
3
|
*
|
|
4
|
-
* Reads and writes provider credentials stored in
|
|
4
|
+
* Reads and writes provider credentials stored in the user config.env (XDG config dir).
|
|
5
5
|
* Each provider block is delimited by comment markers so the file can be
|
|
6
6
|
* parsed without a full ini/dotenv library and can also be sourced by a shell.
|
|
7
7
|
*
|
|
@@ -24,11 +24,12 @@
|
|
|
24
24
|
import fs from 'node:fs';
|
|
25
25
|
import os from 'node:os';
|
|
26
26
|
import path from 'node:path';
|
|
27
|
+
import { configDir } from '../config/xdg.mjs';
|
|
27
28
|
|
|
28
29
|
const CONFIG_FILE = 'config.env';
|
|
29
30
|
|
|
30
31
|
function constructDir() {
|
|
31
|
-
return
|
|
32
|
+
return configDir();
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export function credsFilePath() {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Resolution order (last-write wins):
|
|
5
5
|
*
|
|
6
6
|
* 1. Built-in factories under `lib/providers/<id>/index.mjs` (loaded lazily).
|
|
7
|
-
* 2. Operator overrides at
|
|
7
|
+
* 2. Operator overrides at `providers.json` in the XDG config dir:
|
|
8
8
|
* { "providers": [{ "id": "...", "package": "...", "options": {} }] }
|
|
9
9
|
* 3. Project overrides at `<projectRoot>/.cx/providers.json` (same shape).
|
|
10
10
|
*
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { readFileSync, existsSync } from 'node:fs';
|
|
24
|
-
import { homedir } from 'node:os';
|
|
25
24
|
import { join, isAbsolute, resolve, dirname } from 'node:path';
|
|
26
25
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
27
26
|
import { assertProviderContract } from './contract.mjs';
|
|
28
27
|
import { getBreaker } from './circuit-breaker.mjs';
|
|
28
|
+
import { configDir } from '../config/xdg.mjs';
|
|
29
29
|
|
|
30
30
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
31
31
|
|
|
@@ -92,7 +92,7 @@ async function instantiate(mod, options) {
|
|
|
92
92
|
|
|
93
93
|
function gatherOverrides(rootDir) {
|
|
94
94
|
const out = [];
|
|
95
|
-
const userConfig = readJsonIfExists(join(
|
|
95
|
+
const userConfig = readJsonIfExists(join(configDir(), 'providers.json'));
|
|
96
96
|
if (userConfig?.providers) out.push(...userConfig.providers);
|
|
97
97
|
const projectConfig = readJsonIfExists(join(rootDir, '.cx', 'providers.json'));
|
|
98
98
|
if (projectConfig?.providers) out.push(...projectConfig.providers);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Resolves a canonical environment variable (for example ANTHROPIC_API_KEY) by
|
|
5
5
|
* walking the same sources the rest of Construct trusts, in priority order:
|
|
6
|
-
* process env,
|
|
6
|
+
* process env, the XDG config dir config.env, ~/.env, the project .env, then shell rc
|
|
7
7
|
* files (.zshrc/.bashrc/.bash_profile/.profile). When the value carries a
|
|
8
8
|
* 1Password reference — a bare `op://vault/item/field` or the shell form
|
|
9
9
|
* `$(op read 'op://...')` — it is resolved through the `op` CLI and cached for
|
|
@@ -22,6 +22,7 @@ import path from 'node:path';
|
|
|
22
22
|
import os from 'node:os';
|
|
23
23
|
import { spawnSync } from 'node:child_process';
|
|
24
24
|
import { discoverAlternateRawForVar } from './credential-sources.mjs';
|
|
25
|
+
import { configDir } from '../config/xdg.mjs';
|
|
25
26
|
|
|
26
27
|
// The 1Password desktop integration prompts for biometric unlock on the first
|
|
27
28
|
// read after the vault locks, and that interactive round-trip routinely takes
|
|
@@ -111,7 +112,7 @@ function readShellRcVar(varName, home) {
|
|
|
111
112
|
function rawCandidate(varName, { env, cwd, home }) {
|
|
112
113
|
const direct = env?.[varName];
|
|
113
114
|
if (typeof direct === 'string' && direct.length > 0) return direct;
|
|
114
|
-
const files = [path.join(home, '
|
|
115
|
+
const files = [path.join(configDir(home), 'config.env'), path.join(home, '.env'), path.join(cwd, '.env')];
|
|
115
116
|
for (const file of files) {
|
|
116
117
|
const value = readDotenvVar(file, varName);
|
|
117
118
|
if (value) return value;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { homedir } from 'node:os';
|
|
12
|
+
import { doctorRoot } from './config/xdg.mjs';
|
|
12
13
|
|
|
13
14
|
const SESSION_IDLE_RESET_MS = 2 * 60 * 60 * 1000;
|
|
14
15
|
const REPEATED_READ_WARNING_THRESHOLD = 5;
|
|
@@ -20,7 +21,7 @@ const EFFICIENCY_LARGE_READ_LIMIT = 400;
|
|
|
20
21
|
|
|
21
22
|
export function readTrackerPaths(env = process.env) {
|
|
22
23
|
const home = env.CX_HOME_OVERRIDE || env.HOME || homedir();
|
|
23
|
-
const cxDir =
|
|
24
|
+
const cxDir = doctorRoot(home);
|
|
24
25
|
return {
|
|
25
26
|
cxDir,
|
|
26
27
|
efficiencyStore: join(cxDir, 'session-efficiency.json'),
|
|
@@ -21,7 +21,7 @@ import fs from 'node:fs';
|
|
|
21
21
|
import path from 'node:path';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
23
|
|
|
24
|
-
import { homeDir } from '../paths.mjs';
|
|
24
|
+
import { homeDir, constructDir } from '../paths.mjs';
|
|
25
25
|
import {
|
|
26
26
|
getCodexConfigPath,
|
|
27
27
|
buildCodexMcpEntry,
|
|
@@ -34,7 +34,7 @@ function escapeRegExp(value) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function registryMcpServers() {
|
|
37
|
-
const installed = path.join(
|
|
37
|
+
const installed = path.join(constructDir(), 'specialists', 'registry.json');
|
|
38
38
|
const bundled = fileURLToPath(new URL('../../specialists/registry.json', import.meta.url));
|
|
39
39
|
for (const src of [installed, bundled]) {
|
|
40
40
|
try {
|
package/lib/reflect.mjs
CHANGED
|
@@ -13,9 +13,10 @@ import { loadConstructEnv } from './env-config.mjs';
|
|
|
13
13
|
import { KNOWLEDGE_ROOT, KNOWLEDGE_SUBDIRS, inferKnowledgeTarget } from './knowledge/layout.mjs';
|
|
14
14
|
import { extractSessionObservation } from './reflect/extractor.mjs';
|
|
15
15
|
import { readContextState, contextSummaryLine } from './context-state.mjs';
|
|
16
|
+
import { configDir } from './config/xdg.mjs';
|
|
16
17
|
|
|
17
18
|
const HOME = process.env.HOME || process.env.USERPROFILE;
|
|
18
|
-
const USER_ENV_PATH = join(HOME, '
|
|
19
|
+
const USER_ENV_PATH = join(configDir(HOME), 'config.env');
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Main reflect command handler.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/registry/generate-docs.mjs — generate reference docs from registry/capabilities.json.
|
|
3
3
|
*
|
|
4
|
-
* Writes docs/reference/capabilities.md so the human-readable capability matrix
|
|
4
|
+
* Writes docs/guides/reference/capabilities.md so the human-readable capability matrix
|
|
5
5
|
* cannot drift from the machine-readable registry.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -62,7 +62,7 @@ export function generateCapabilitiesDoc({ rootDir = REPO_ROOT, write = true } =
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const content = `${lines.join('\n')}\n`;
|
|
65
|
-
const out = path.join(rootDir, 'docs', 'reference', 'capabilities.md');
|
|
65
|
+
const out = path.join(rootDir, 'docs', 'guides', 'reference', 'capabilities.md');
|
|
66
66
|
if (!write) return { out, content, drift: fs.existsSync(out) ? fs.readFileSync(out, 'utf8') !== content : true };
|
|
67
67
|
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
68
68
|
fs.writeFileSync(out, content);
|
|
@@ -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-env.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/runtime-env.mjs — Construct runtime environment with credential bootstrap.
|
|
3
3
|
*
|
|
4
|
-
* prepareConstructEnv merges ~/.construct/config.env into the process environment.
|
|
4
|
+
* prepareConstructEnv merges ~/.config/construct/config.env into the process environment.
|
|
5
5
|
* Credential auto-link from 1Password runs only when autoLink:true (setup-credentials).
|
|
6
6
|
*/
|
|
7
7
|
|
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' });
|
|
@@ -1200,7 +1201,7 @@ const server = createServer(async (req, res) => {
|
|
|
1200
1201
|
|
|
1201
1202
|
if (url.pathname === '/api/performance/reviews' && req.method === 'GET') {
|
|
1202
1203
|
try {
|
|
1203
|
-
const dir = join(
|
|
1204
|
+
const dir = join(doctorRoot(), 'performance-reviews');
|
|
1204
1205
|
if (!existsSync(dir)) {
|
|
1205
1206
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1206
1207
|
res.end(JSON.stringify({ reviews: [], total: 0, mockFiltered: 0, generatorImplemented: false }));
|
|
@@ -1327,7 +1328,7 @@ const server = createServer(async (req, res) => {
|
|
|
1327
1328
|
if (url.pathname === '/api/mode' && req.method === 'GET') {
|
|
1328
1329
|
try {
|
|
1329
1330
|
const env = loadConstructEnv();
|
|
1330
|
-
const embedYamlPath = join(HOME, '
|
|
1331
|
+
const embedYamlPath = join(configDir(HOME), 'embed.yaml');
|
|
1331
1332
|
const embedStatus = resolveEmbedStatus(env);
|
|
1332
1333
|
|
|
1333
1334
|
// Determine mode based on embed status and configuration
|
|
@@ -1374,7 +1375,7 @@ const server = createServer(async (req, res) => {
|
|
|
1374
1375
|
parentUrl,
|
|
1375
1376
|
isEmbedded,
|
|
1376
1377
|
embedStatus: embedStatus.level,
|
|
1377
|
-
embedConfigExists: existsSync(join(HOME, '
|
|
1378
|
+
embedConfigExists: existsSync(join(configDir(HOME), 'embed.yaml')),
|
|
1378
1379
|
capabilities: {
|
|
1379
1380
|
modeDetection: true,
|
|
1380
1381
|
snapshotStatus: true,
|
|
@@ -1620,7 +1621,7 @@ const server = createServer(async (req, res) => {
|
|
|
1620
1621
|
rootDir: ROOT_DIR,
|
|
1621
1622
|
guidance: INTAKE_DEPTH_GUIDANCE,
|
|
1622
1623
|
hardMaxDepth: INTAKE_HARD_MAX_DEPTH,
|
|
1623
|
-
defaults: {
|
|
1624
|
+
defaults: { rootInbox: 'inbox/' },
|
|
1624
1625
|
label: rebrand.intakeQueueLabel,
|
|
1625
1626
|
itemNoun: rebrand.signalNoun,
|
|
1626
1627
|
}));
|
|
@@ -1780,7 +1781,7 @@ const server = createServer(async (req, res) => {
|
|
|
1780
1781
|
|
|
1781
1782
|
if (url.pathname === '/api/providers/subscriptions' && req.method === 'GET') {
|
|
1782
1783
|
try {
|
|
1783
|
-
const file = join(HOME, '
|
|
1784
|
+
const file = join(configDir(HOME), 'provider-subscriptions.json');
|
|
1784
1785
|
let data = { subscriptions: [] };
|
|
1785
1786
|
if (existsSync(file)) {
|
|
1786
1787
|
try { data = JSON.parse(readFileSync(file, 'utf8')); } catch { /* fall back to empty */ }
|
|
@@ -1810,7 +1811,7 @@ const server = createServer(async (req, res) => {
|
|
|
1810
1811
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1811
1812
|
res.end(JSON.stringify({
|
|
1812
1813
|
envPath: CONFIG_ENV_FILE,
|
|
1813
|
-
overridesPath: join(HOME, '
|
|
1814
|
+
overridesPath: join(configDir(HOME), 'providers.json'),
|
|
1814
1815
|
}));
|
|
1815
1816
|
return;
|
|
1816
1817
|
}
|
|
@@ -2298,7 +2299,7 @@ const server = createServer(async (req, res) => {
|
|
|
2298
2299
|
if (!resolved) throw new Error('op read returned an empty value');
|
|
2299
2300
|
|
|
2300
2301
|
const { writeEnvValues, loadConstructEnv } = await import('../env-config.mjs');
|
|
2301
|
-
mkdirSync(
|
|
2302
|
+
mkdirSync(configDir(HOME), { recursive: true });
|
|
2302
2303
|
writeEnvValues(CONFIG_ENV_FILE, { [envVar]: resolved });
|
|
2303
2304
|
process.env[envVar] = resolved;
|
|
2304
2305
|
try { chmodSync(CONFIG_ENV_FILE, 0o600); } catch { /* perms best-effort */ }
|
|
@@ -2310,7 +2311,7 @@ const server = createServer(async (req, res) => {
|
|
|
2310
2311
|
}
|
|
2311
2312
|
|
|
2312
2313
|
try {
|
|
2313
|
-
mkdirSync(
|
|
2314
|
+
mkdirSync(doctorRoot(), { recursive: true });
|
|
2314
2315
|
appendFileSync(CREDENTIAL_AUDIT_FILE, JSON.stringify({ ts: new Date().toISOString(), action: 'set', envVar, source: '1password', opRef }) + '\n');
|
|
2315
2316
|
} catch { /* audit best-effort */ }
|
|
2316
2317
|
|
|
@@ -2367,9 +2368,6 @@ const server = createServer(async (req, res) => {
|
|
|
2367
2368
|
const next = saveIntakeConfig(ROOT_DIR, {
|
|
2368
2369
|
parentDirs: Array.isArray(data.parentDirs) ? data.parentDirs : undefined,
|
|
2369
2370
|
maxDepth: data.maxDepth,
|
|
2370
|
-
includeProjectInbox: data.includeProjectInbox,
|
|
2371
|
-
includeDocsIntake: data.includeDocsIntake,
|
|
2372
|
-
includeRootInbox: data.includeRootInbox ?? data.includeArchetypeInbox,
|
|
2373
2371
|
});
|
|
2374
2372
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2375
2373
|
res.end(JSON.stringify({ success: true, config: next }));
|
|
@@ -2406,7 +2404,7 @@ const server = createServer(async (req, res) => {
|
|
|
2406
2404
|
}
|
|
2407
2405
|
} else if (url.pathname === '/api/providers/registry') {
|
|
2408
2406
|
const { BUILT_INS } = await import('../providers/registry.mjs');
|
|
2409
|
-
const overridesPath = join(HOME, '
|
|
2407
|
+
const overridesPath = join(configDir(HOME), 'providers.json');
|
|
2410
2408
|
const { action, id, package: pkg, options } = data;
|
|
2411
2409
|
if (!id || typeof id !== 'string' || !/^[\w-]+$/.test(id)) throw new Error('Invalid provider id');
|
|
2412
2410
|
if (BUILT_INS.includes(id)) throw new Error(`cannot override built-in provider '${id}'`);
|
|
@@ -2432,7 +2430,7 @@ const server = createServer(async (req, res) => {
|
|
|
2432
2430
|
throw new Error('action must be save or delete');
|
|
2433
2431
|
}
|
|
2434
2432
|
|
|
2435
|
-
mkdirSync(
|
|
2433
|
+
mkdirSync(configDir(HOME), { recursive: true });
|
|
2436
2434
|
const tmp = overridesPath + '.tmp';
|
|
2437
2435
|
writeFileSync(tmp, JSON.stringify(current, null, 2) + '\n');
|
|
2438
2436
|
renameSync(tmp, overridesPath);
|
|
@@ -2460,7 +2458,7 @@ const server = createServer(async (req, res) => {
|
|
|
2460
2458
|
if (value !== '' && typeof value !== 'string') throw new Error('value must be a string');
|
|
2461
2459
|
|
|
2462
2460
|
const { writeEnvValues, parseEnvFile, loadConstructEnv } = await import('../env-config.mjs');
|
|
2463
|
-
mkdirSync(
|
|
2461
|
+
mkdirSync(configDir(HOME), { recursive: true });
|
|
2464
2462
|
const action = value === '' ? 'unset' : 'set';
|
|
2465
2463
|
|
|
2466
2464
|
if (action === 'unset') {
|
|
@@ -2480,7 +2478,7 @@ const server = createServer(async (req, res) => {
|
|
|
2480
2478
|
}
|
|
2481
2479
|
|
|
2482
2480
|
try {
|
|
2483
|
-
mkdirSync(
|
|
2481
|
+
mkdirSync(doctorRoot(), { recursive: true });
|
|
2484
2482
|
appendFileSync(CREDENTIAL_AUDIT_FILE, JSON.stringify({ ts: new Date().toISOString(), action, envVar }) + '\n');
|
|
2485
2483
|
} catch { /* audit append best-effort */ }
|
|
2486
2484
|
|
|
@@ -2488,7 +2486,7 @@ const server = createServer(async (req, res) => {
|
|
|
2488
2486
|
res.end(JSON.stringify({ success: true }));
|
|
2489
2487
|
notifyClients();
|
|
2490
2488
|
} else if (url.pathname === '/api/providers/subscriptions') {
|
|
2491
|
-
const subsPath = join(HOME, '
|
|
2489
|
+
const subsPath = join(configDir(HOME), 'provider-subscriptions.json');
|
|
2492
2490
|
const { action, id, provider, name, config } = data;
|
|
2493
2491
|
if (!id || typeof id !== 'string' || !/^[\w.-]+$/.test(id)) throw new Error('Invalid subscription id');
|
|
2494
2492
|
|
|
@@ -2509,7 +2507,7 @@ const server = createServer(async (req, res) => {
|
|
|
2509
2507
|
throw new Error('action must be save or delete');
|
|
2510
2508
|
}
|
|
2511
2509
|
|
|
2512
|
-
mkdirSync(
|
|
2510
|
+
mkdirSync(configDir(HOME), { recursive: true });
|
|
2513
2511
|
const tmp = subsPath + '.tmp';
|
|
2514
2512
|
writeFileSync(tmp, JSON.stringify(current, null, 2) + '\n');
|
|
2515
2513
|
renameSync(tmp, subsPath);
|