@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/document-ingest.mjs
CHANGED
|
@@ -137,19 +137,29 @@ function collectInputFiles(inputPath, { maxDepth = 10 } = {}) {
|
|
|
137
137
|
return files.sort((a, b) => a.localeCompare(b));
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
// Ingest writes a markdown rendering of the source, preserving the source name
|
|
141
|
+
// for provenance (report.pdf → report.pdf.md). A source already in markdown keeps
|
|
142
|
+
// its name unchanged so it never gains a second extension (_template.md →
|
|
143
|
+
// _template.md, not _template.md.md).
|
|
144
|
+
|
|
145
|
+
function markdownOutputName(sourcePath) {
|
|
146
|
+
const base = basename(sourcePath);
|
|
147
|
+
return /\.md$/i.test(base) ? base : `${base}.md`;
|
|
148
|
+
}
|
|
149
|
+
|
|
140
150
|
function resolveOutputPath(sourcePath, { cwd, outputPath, outputDir, target }) {
|
|
141
151
|
if (outputPath) return normalizeOutputPath(outputPath, cwd);
|
|
142
152
|
if (target === 'sibling') {
|
|
143
|
-
return join(dirname(sourcePath),
|
|
153
|
+
return join(dirname(sourcePath), markdownOutputName(sourcePath));
|
|
144
154
|
}
|
|
145
155
|
// knowledge/<subdir> targets land in .cx/knowledge/<subdir>/
|
|
146
156
|
if (target.startsWith('knowledge/')) {
|
|
147
157
|
const sub = target.slice('knowledge/'.length);
|
|
148
158
|
const resolvedDir = normalizeOutputPath(`${KNOWLEDGE_ROOT}/${sub}`, cwd);
|
|
149
|
-
return join(resolvedDir,
|
|
159
|
+
return join(resolvedDir, markdownOutputName(sourcePath));
|
|
150
160
|
}
|
|
151
161
|
const resolvedDir = normalizeOutputPath(outputDir || DEFAULT_TARGET_DIR, cwd);
|
|
152
|
-
return join(resolvedDir,
|
|
162
|
+
return join(resolvedDir, markdownOutputName(sourcePath));
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
// The high-fidelity (docling) path provisions a Python venv and downloads ML
|
package/lib/efficiency.mjs
CHANGED
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync, readFileSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
|
+
import { doctorRoot } from './config/xdg.mjs';
|
|
9
10
|
|
|
10
11
|
const HIGH_BYTES_THRESHOLD = 750_000;
|
|
11
12
|
const HIGH_REPEATED_RATIO = 0.4;
|
|
12
13
|
const HIGH_LARGE_READ_COUNT = 3;
|
|
13
14
|
|
|
14
15
|
export function readEfficiencyLog(homeDir) {
|
|
15
|
-
const filePath = join(homeDir, '
|
|
16
|
+
const filePath = join(doctorRoot(homeDir), 'session-efficiency.json');
|
|
16
17
|
if (!existsSync(filePath)) return null;
|
|
17
18
|
try {
|
|
18
19
|
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
14
15
|
|
|
15
16
|
export class ApprovalQueue {
|
|
16
17
|
#items = new Map(); // id → ApprovalItem
|
|
@@ -171,6 +172,6 @@ export class ApprovalQueue {
|
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
static defaultPersistPath() {
|
|
174
|
-
return path.join(
|
|
175
|
+
return path.join(doctorRoot(), 'approval-queue.jsonl');
|
|
175
176
|
}
|
|
176
177
|
}
|
package/lib/embed/cli.mjs
CHANGED
|
@@ -21,6 +21,8 @@ import fs from 'node:fs';
|
|
|
21
21
|
import os from 'node:os';
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import { spawn } from 'node:child_process';
|
|
24
|
+
import { configDir } from '../config/xdg.mjs';
|
|
25
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
24
26
|
|
|
25
27
|
const STATE_FILE = 'embed-daemon.json';
|
|
26
28
|
const LOG_FILE = 'embed-daemon.log';
|
|
@@ -30,7 +32,7 @@ const LOG_FILE = 'embed-daemon.log';
|
|
|
30
32
|
// ---------------------------------------------------------------------------
|
|
31
33
|
|
|
32
34
|
function runtimeDir(homeDir = os.homedir()) {
|
|
33
|
-
return path.join(homeDir, '
|
|
35
|
+
return path.join(doctorRoot(homeDir), 'runtime');
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
function statePath(homeDir = os.homedir()) {
|
|
@@ -273,8 +275,8 @@ async function cmdEmbedStart(args, { homeDir = os.homedir(), rootDir, _workerPat
|
|
|
273
275
|
const { config } = parseArgs(args);
|
|
274
276
|
const configPath = config
|
|
275
277
|
? path.resolve(config)
|
|
276
|
-
: fs.existsSync(path.join(
|
|
277
|
-
? path.join(
|
|
278
|
+
: fs.existsSync(path.join(configDir(), 'embed.yaml'))
|
|
279
|
+
? path.join(configDir(), 'embed.yaml')
|
|
278
280
|
: null;
|
|
279
281
|
|
|
280
282
|
const workerPath = _workerPath ?? path.join(rootDir, 'lib', 'embed', 'worker.mjs');
|
|
@@ -374,7 +376,7 @@ async function cmdEmbedStatus(_args, { homeDir = os.homedir() } = {}) {
|
|
|
374
376
|
process.stdout.write(` pid: ${state.pid}\n`);
|
|
375
377
|
process.stdout.write(` config: ${state.configPath}\n`);
|
|
376
378
|
process.stdout.write(` rootDir: ${rootDir}\n`);
|
|
377
|
-
process.stdout.write(` inbox: ${path.join(rootDir, '
|
|
379
|
+
process.stdout.write(` inbox: ${path.join(rootDir, 'inbox')}\n`);
|
|
378
380
|
process.stdout.write(` started at: ${state.startedAt}\n`);
|
|
379
381
|
process.stdout.write(` log: ${logPath(homeDir)}\n`);
|
|
380
382
|
}
|
|
@@ -387,7 +389,7 @@ async function cmdEmbedSnapshot(args, { homeDir = os.homedir() } = {}) {
|
|
|
387
389
|
const { config } = parseArgs(args);
|
|
388
390
|
const configPath = config
|
|
389
391
|
? path.resolve(config)
|
|
390
|
-
: path.join(
|
|
392
|
+
: path.join(configDir(), 'embed.yaml');
|
|
391
393
|
|
|
392
394
|
const { loadEmbedConfig, EMPTY_CONFIG } = await import('./config.mjs');
|
|
393
395
|
const { SnapshotEngine, renderMarkdown } = await import('./snapshot.mjs');
|
package/lib/embed/config.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/embed/config.mjs — embed mode configuration schema and loader.
|
|
3
3
|
*
|
|
4
|
-
* Reads embed.yaml from
|
|
4
|
+
* Reads embed.yaml from the XDG config dir (or a supplied path) and validates
|
|
5
5
|
* the file against the expected schema. Returns a parsed, normalized config object.
|
|
6
6
|
*
|
|
7
7
|
* Schema:
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
import fs from 'node:fs';
|
|
56
56
|
import os from 'node:os';
|
|
57
57
|
import path from 'node:path';
|
|
58
|
+
import { configDir, stateDir } from '../config/xdg.mjs';
|
|
58
59
|
|
|
59
60
|
const DEFAULTS = {
|
|
60
61
|
snapshot: { intervalMs: 300_000, maxItems: 100 },
|
|
@@ -74,13 +75,13 @@ const DEFAULTS = {
|
|
|
74
75
|
* ref: github.com/org/project-y
|
|
75
76
|
* path: /Users/user/Github/project-y # both — prefer local, fall back to remote
|
|
76
77
|
* - type: workspace # always present as fallback
|
|
77
|
-
* path:
|
|
78
|
+
* path: <XDG_STATE_HOME>/construct/workspace
|
|
78
79
|
*
|
|
79
80
|
* roles:
|
|
80
81
|
* primary: product-manager # shapes embed analysis lens
|
|
81
82
|
* secondary: architect # persistent background concern
|
|
82
83
|
*/
|
|
83
|
-
export const DEFAULT_WORKSPACE_PATH = path.join(
|
|
84
|
+
export const DEFAULT_WORKSPACE_PATH = path.join(stateDir(), 'workspace');
|
|
84
85
|
|
|
85
86
|
export const WORKSPACE_DOCS_LANES = ['adrs', 'prds', 'memos', 'notes', 'intake'];
|
|
86
87
|
|
|
@@ -99,7 +100,7 @@ export const DEFAULT_OPERATING_PROFILE = {
|
|
|
99
100
|
},
|
|
100
101
|
focalResources: [
|
|
101
102
|
{ type: 'plan', path: 'plan.md', purpose: 'active implementation plan' },
|
|
102
|
-
{ type: 'architecture', path: 'docs/concepts/architecture.md', purpose: 'system architecture and operating model' },
|
|
103
|
+
{ type: 'architecture', path: 'docs/guides/concepts/architecture.md', purpose: 'system architecture and operating model' },
|
|
103
104
|
{ type: 'knowledge', path: '.cx/knowledge', purpose: 'typed project knowledge and ingested evidence' },
|
|
104
105
|
{ type: 'runtime', path: '.cx/roadmap.md', purpose: 'generated roadmap output' },
|
|
105
106
|
],
|
|
@@ -351,5 +352,5 @@ export function parseEmbedYaml(text) {
|
|
|
351
352
|
}
|
|
352
353
|
|
|
353
354
|
export function defaultEmbedConfigPath() {
|
|
354
|
-
return path.join(
|
|
355
|
+
return path.join(configDir(), 'embed.yaml');
|
|
355
356
|
}
|
package/lib/embed/daemon.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - approval-expiry — expires stale approval queue items
|
|
14
14
|
* - eval-dataset-sync — syncs scored traces to telemetry dataset items
|
|
15
15
|
* - prompt-regression-check — detects low-quality score clusters per promptHash
|
|
16
|
-
* - inbox-watcher — ingests new files from
|
|
16
|
+
* - inbox-watcher — ingests new files from inbox/ + CX_INBOX_DIRS into
|
|
17
17
|
* observations (agnostic: specs, ADRs, meeting notes, etc.)
|
|
18
18
|
* - roadmap — cross-references open items + observations → .cx/roadmap.md
|
|
19
19
|
*
|
|
@@ -48,6 +48,7 @@ import { emitEmbedNotification, notifySlack } from './notifications.mjs';
|
|
|
48
48
|
import { intentToCategory } from './providers/slack.mjs';
|
|
49
49
|
import { runTelemetrySetup } from '../telemetry/setup.mjs';
|
|
50
50
|
import { emitTraceEvent, newTraceId, newSpanId } from '../worker/trace.mjs';
|
|
51
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
54
|
* Log a non-fatal error to stderr. Use instead of empty catch {} blocks
|
|
@@ -121,8 +122,8 @@ function resolveDaemonStatePath(env = process.env) {
|
|
|
121
122
|
|
|
122
123
|
// ─── Self-repair helpers ────────────────────────────────────────────────────
|
|
123
124
|
|
|
124
|
-
const LOCK_PATH = join(
|
|
125
|
-
const DAEMON_STATE_PATH = join(
|
|
125
|
+
const LOCK_PATH = join(doctorRoot(), 'sync.lock');
|
|
126
|
+
const DAEMON_STATE_PATH = join(doctorRoot(), 'runtime', 'embed-daemon.json');
|
|
126
127
|
|
|
127
128
|
/**
|
|
128
129
|
* Run in-process health checks and fix what can be fixed.
|
|
@@ -562,7 +563,7 @@ export class EmbedDaemon {
|
|
|
562
563
|
'daemon-log-rotation',
|
|
563
564
|
60_000,
|
|
564
565
|
async () => {
|
|
565
|
-
const logPath = join(
|
|
566
|
+
const logPath = join(doctorRoot(), 'runtime', 'embed-daemon.log');
|
|
566
567
|
const capMb = Number(process.env.CONSTRUCT_EMBED_LOG_MAX_MB) || 50;
|
|
567
568
|
const maxBytes = Math.floor(capMb * 1024 * 1024);
|
|
568
569
|
const maxSegments = Number(process.env.CONSTRUCT_EMBED_LOG_MAX_SEGMENTS) || 5;
|
|
@@ -729,7 +730,7 @@ this.#scheduler.register(
|
|
|
729
730
|
// Every 2 min — scan configured inbox dirs for new files, ingest them, and
|
|
730
731
|
// record observations. Agnostic to content: specs, ADRs, meeting notes,
|
|
731
732
|
// internal docs, PDFs, Office files — anything on the local filesystem.
|
|
732
|
-
// Dirs: <rootDir
|
|
733
|
+
// Dirs: <rootDir>/inbox/ always; CX_INBOX_DIRS env for extra paths.
|
|
733
734
|
this.#scheduler.register(
|
|
734
735
|
'inbox-watcher',
|
|
735
736
|
2 * 60_000,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Registers a node:fs.watch listener on each inbox directory and triggers
|
|
5
5
|
* `InboxWatcher.poll()` shortly after filesystem activity settles, so files
|
|
6
|
-
* dropped into
|
|
6
|
+
* dropped into `inbox/` flow through ingest + the review queue in
|
|
7
7
|
* seconds rather than waiting for the scheduler's next interval.
|
|
8
8
|
*
|
|
9
9
|
* Two limits keep this stable across platforms:
|
package/lib/embed/inbox.mjs
CHANGED
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
* handles specs, ADRs, meeting notes, internal docs, research, PDFs,
|
|
7
7
|
* Office files, plain text, code, or any format supported by document-extract.
|
|
8
8
|
*
|
|
9
|
-
* Watch dirs
|
|
10
|
-
* 1.
|
|
11
|
-
* 2.
|
|
12
|
-
*
|
|
9
|
+
* Watch dirs:
|
|
10
|
+
* 1. <rootDir>/inbox/ — the canonical project-root drop zone (always watched)
|
|
11
|
+
* 2. CX_INBOX_DIRS env var + intakePolicy.additionalDirs — opt-in extra paths
|
|
12
|
+
*
|
|
13
|
+
* Atomic handoff (ADR-0045 §C): writers stage a file under `inbox/.staging/`
|
|
14
|
+
* and atomically rename it into `inbox/`. The watcher ignores `.staging/` and
|
|
15
|
+
* dotfiles, and skips any top-level file whose size is still changing between
|
|
16
|
+
* two stats, so a partially-written drop is never consumed mid-write.
|
|
13
17
|
*
|
|
14
18
|
* State tracking:
|
|
15
19
|
* <rootDir>/.cx/runtime/inbox-state.json — maps filePath → processedAt
|
|
@@ -23,7 +27,6 @@
|
|
|
23
27
|
|
|
24
28
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, appendFileSync } from 'node:fs';
|
|
25
29
|
import { join, resolve, isAbsolute } from 'node:path';
|
|
26
|
-
import { homedir } from 'node:os';
|
|
27
30
|
import { isExtractableDocumentPath, AUDIO_VIDEO_EXTS } from '../document-extract.mjs';
|
|
28
31
|
import { ingestDocuments } from '../document-ingest.mjs';
|
|
29
32
|
import { addObservation } from '../observation-store.mjs';
|
|
@@ -36,29 +39,29 @@ import { loadIntakeConfig, INTAKE_DEFAULT_MAX_DEPTH } from '../intake/intake-con
|
|
|
36
39
|
import { loadManifest, recordFile, hasFile, sha256Of, MANIFEST_REL_PATH } from '../intake/manifest.mjs';
|
|
37
40
|
import { gatherAttribution } from '../intake/attribution.mjs';
|
|
38
41
|
import { acquirePollLock, releasePollLock } from '../intake/poll-lock.mjs';
|
|
42
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
39
43
|
|
|
40
44
|
const STATE_FILE = '.cx/runtime/inbox-state.json';
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const ARCHETYPE_INBOX_SUBDIR = 'inbox';
|
|
45
|
+
const ROOT_INBOX_SUBDIR = 'inbox';
|
|
46
|
+
const STAGING_SUBDIR = '.staging';
|
|
44
47
|
const MAX_FILE_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB hard cap
|
|
45
48
|
const STALE_STATE_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
46
49
|
|
|
47
50
|
// ─── Extraction-warning helpers ───────────────────────────────────────────────
|
|
48
51
|
|
|
49
52
|
function extractionWarningsLog() {
|
|
50
|
-
return join(
|
|
53
|
+
return join(doctorRoot(), 'intake', 'extraction-warnings.jsonl');
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
function needsAsrDir() {
|
|
54
|
-
return join(
|
|
57
|
+
return join(doctorRoot(), 'intake', 'needs-asr');
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
function writeExtractionWarning({ sourcePath, droppedInfo }) {
|
|
58
61
|
if (!droppedInfo?.length) return;
|
|
59
62
|
const logPath = extractionWarningsLog();
|
|
60
63
|
try {
|
|
61
|
-
mkdirSync(join(
|
|
64
|
+
mkdirSync(join(doctorRoot(), 'intake'), { recursive: true });
|
|
62
65
|
const line = JSON.stringify({ ts: new Date().toISOString(), sourcePath, droppedInfo }) + '\n';
|
|
63
66
|
appendFileSync(logPath, line);
|
|
64
67
|
} catch { /* non-fatal */ }
|
|
@@ -139,31 +142,20 @@ function writeState(rootDir, state) {
|
|
|
139
142
|
|
|
140
143
|
/**
|
|
141
144
|
* Resolve inbox watch directories from the persisted intake-config.
|
|
142
|
-
*
|
|
143
|
-
* config
|
|
144
|
-
*
|
|
145
|
-
*
|
|
145
|
+
* The canonical <rootDir>/inbox/ is always watched (ADR-0045 §C). `parentDirs`
|
|
146
|
+
* from intake-config plus CX_INBOX_DIRS env var add opt-in extras.
|
|
147
|
+
*
|
|
148
|
+
* The canonical inbox gets a `.staging/` subdir on resolution so writers have
|
|
149
|
+
* a stable place to assemble a file before the atomic rename into `inbox/`.
|
|
146
150
|
*/
|
|
147
151
|
export function resolveInboxDirs(rootDir, env = process.env) {
|
|
148
152
|
const config = loadIntakeConfig(rootDir, env);
|
|
149
153
|
const dirs = [];
|
|
150
154
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (config.includeProjectInbox) {
|
|
158
|
-
const projectInbox = join(rootDir, DEFAULT_INBOX_SUBDIR);
|
|
159
|
-
mkdirSync(projectInbox, { recursive: true });
|
|
160
|
-
dirs.push(projectInbox);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (config.includeDocsIntake) {
|
|
164
|
-
const docsIntake = join(rootDir, DOCS_INTAKE_SUBDIR);
|
|
165
|
-
if (existsSync(docsIntake)) dirs.push(docsIntake);
|
|
166
|
-
}
|
|
155
|
+
const rootInbox = join(rootDir, ROOT_INBOX_SUBDIR);
|
|
156
|
+
mkdirSync(rootInbox, { recursive: true });
|
|
157
|
+
mkdirSync(join(rootInbox, STAGING_SUBDIR), { recursive: true });
|
|
158
|
+
dirs.push(rootInbox);
|
|
167
159
|
|
|
168
160
|
for (const candidate of config.parentDirs) {
|
|
169
161
|
if (!candidate) continue;
|
|
@@ -177,6 +169,20 @@ export function resolveInboxDirs(rootDir, env = process.env) {
|
|
|
177
169
|
|
|
178
170
|
// ─── File scanning ────────────────────────────────────────────────────────────
|
|
179
171
|
|
|
172
|
+
// Dotfiles and dot-directories — including the `inbox/.staging/` assembly
|
|
173
|
+
// area writers rename out of — are skipped wholesale, so a staged file is
|
|
174
|
+
// never visible to the scanner until it lands at a top-level name.
|
|
175
|
+
|
|
176
|
+
function isStableFile(full) {
|
|
177
|
+
try {
|
|
178
|
+
const a = statSync(full);
|
|
179
|
+
const b = statSync(full);
|
|
180
|
+
return a.size === b.size && a.mtimeMs === b.mtimeMs ? a : null;
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
180
186
|
/**
|
|
181
187
|
* Walk `dir` up to `maxDepth` subdirectory levels.
|
|
182
188
|
* maxDepth=0 → only files in `dir` itself (no subdirs).
|
|
@@ -198,11 +204,15 @@ function scanDir(dir, maxDepth = INTAKE_DEFAULT_MAX_DEPTH) {
|
|
|
198
204
|
}
|
|
199
205
|
if (!entry.isFile()) continue;
|
|
200
206
|
if (!isExtractableDocumentPath(full)) continue;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
207
|
+
|
|
208
|
+
// Stability backstop for writers that drop in place instead of staging:
|
|
209
|
+
// a file whose size/mtime is still moving between two stats is mid-write,
|
|
210
|
+
// so leave it for the next poll once it settles.
|
|
211
|
+
|
|
212
|
+
const st = isStableFile(full);
|
|
213
|
+
if (!st) continue;
|
|
214
|
+
if (st.size === 0 || st.size > MAX_FILE_SIZE_BYTES) continue;
|
|
215
|
+
results.push({ path: full, size: st.size, mtimeMs: st.mtimeMs });
|
|
206
216
|
}
|
|
207
217
|
}
|
|
208
218
|
} catch (err) { process.stderr.write('[inbox.mjs] scan-dir: ' + (err?.message ?? String(err)) + '\n'); }
|
|
@@ -269,7 +279,12 @@ function maybePromoteToDocs(rootDir, { sourcePath, extractedText }) {
|
|
|
269
279
|
if (!existsSync(laneDir)) return null;
|
|
270
280
|
|
|
271
281
|
const sourceName = sourcePath.split('/').pop() ?? 'intake-doc';
|
|
272
|
-
|
|
282
|
+
|
|
283
|
+
// A source already in markdown keeps its name; others gain a .md rendering
|
|
284
|
+
// suffix. Without the guard an intake item named foo.md promotes to foo.md.md.
|
|
285
|
+
|
|
286
|
+
const docFileName = /\.md$/i.test(sourceName) ? sourceName : `${sourceName}.md`;
|
|
287
|
+
const targetPath = nextAvailablePath(join(laneDir, docFileName));
|
|
273
288
|
const title = sourceName.replace(/\.[^.]+$/, '').replace(/[-_]+/g, ' ').trim() || 'Promoted intake document';
|
|
274
289
|
const markdown = [
|
|
275
290
|
'# ' + title,
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
18
18
|
import { join } from 'node:path';
|
|
19
|
-
|
|
19
|
+
|
|
20
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Scan an intake directory and return file metadata.
|
|
@@ -125,7 +126,7 @@ export function computeIntakeMetrics({ rootDir } = {}) {
|
|
|
125
126
|
// — Metrics from recommendation store
|
|
126
127
|
let recStats = { active: 0, dismissed: 0, total: 0 };
|
|
127
128
|
try {
|
|
128
|
-
const recIndexPath = join(
|
|
129
|
+
const recIndexPath = join(doctorRoot(), 'intake', 'recommendations-index.json');
|
|
129
130
|
if (existsSync(recIndexPath)) {
|
|
130
131
|
const index = JSON.parse(readFileSync(recIndexPath, 'utf8'));
|
|
131
132
|
const entries = Object.values(index);
|
package/lib/embed/reconcile.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* with no changes re-embeds nothing.
|
|
13
13
|
*
|
|
14
14
|
* Reconciliation-on-sync is preferred over a fixed-interval polling cron (see
|
|
15
|
-
* docs/research/vector-search-best-practices.md): the pass runs where Construct
|
|
15
|
+
* docs/notes/research/vector-search-best-practices.md): the pass runs where Construct
|
|
16
16
|
* already touches storage (the model-change re-embed, the storage CLI), keys
|
|
17
17
|
* idempotency on a content hash, and re-embeds on model change.
|
|
18
18
|
*/
|
package/lib/embed/semantic.mjs
CHANGED
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
22
22
|
import { join } from 'node:path';
|
|
23
23
|
import { createHash, randomUUID } from 'node:crypto';
|
|
24
|
-
import { homedir } from 'node:os';
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
26
|
+
|
|
27
|
+
const CACHE_DIR = join(doctorRoot(), 'cache', 'embeddings');
|
|
27
28
|
const INDEX_FILE = join(CACHE_DIR, 'index.json');
|
|
28
29
|
const MODEL_NAME = 'Xenova/all-MiniLM-L6-v2';
|
|
29
30
|
const DIMENSIONS = 384;
|
|
@@ -21,13 +21,15 @@ import os from 'node:os';
|
|
|
21
21
|
import path from 'node:path';
|
|
22
22
|
import { spawnSync } from 'node:child_process';
|
|
23
23
|
|
|
24
|
+
import { stateDir, doctorRoot } from '../config/xdg.mjs';
|
|
25
|
+
|
|
24
26
|
const HOME = os.homedir();
|
|
25
27
|
const PLATFORM = process.platform;
|
|
26
28
|
const NODE_BIN = process.execPath;
|
|
27
29
|
|
|
28
30
|
// Path to the construct bin — we use absolute path so launchd/systemd can find it.
|
|
29
31
|
function constructBin() {
|
|
30
|
-
const local = path.join(HOME, '
|
|
32
|
+
const local = path.join(stateDir(HOME), 'bin', 'construct');
|
|
31
33
|
if (fs.existsSync(local)) return local;
|
|
32
34
|
const result = spawnSync('which', ['construct'], { encoding: 'utf8' });
|
|
33
35
|
return result.status === 0 ? result.stdout.trim() : 'construct';
|
|
@@ -195,7 +197,7 @@ function windowsStatus() {
|
|
|
195
197
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
196
198
|
|
|
197
199
|
function logPath() {
|
|
198
|
-
return path.join(
|
|
200
|
+
return path.join(doctorRoot(), 'runtime', 'embed-daemon.log');
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
/**
|
package/lib/engine/registry.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Resolution order (last-write wins):
|
|
5
5
|
* 1. Built-in defaults (lib/engine/defaults.mjs)
|
|
6
|
-
* 2.
|
|
6
|
+
* 2. plugins.json in the XDG config dir (operator-level overrides)
|
|
7
7
|
* 3. <project>/.cx/plugins.json (repo-level overrides)
|
|
8
8
|
* 4. CONSTRUCT_PLUGIN_<LAYER> environment variables (one-off overrides)
|
|
9
9
|
*
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { readFileSync, existsSync } from 'node:fs';
|
|
23
|
-
import { homedir } from 'node:os';
|
|
24
23
|
import { join, isAbsolute, resolve } from 'node:path';
|
|
25
24
|
import { pathToFileURL } from 'node:url';
|
|
26
25
|
import { LAYERS, assertContract } from './contracts.mjs';
|
|
27
26
|
import { loadDefaults } from './defaults.mjs';
|
|
27
|
+
import { configDir } from '../config/xdg.mjs';
|
|
28
28
|
|
|
29
29
|
const ENV_PREFIX = 'CONSTRUCT_PLUGIN_';
|
|
30
30
|
|
|
@@ -43,7 +43,7 @@ function readJsonIfExists(path) {
|
|
|
43
43
|
function gatherOverrides(rootDir) {
|
|
44
44
|
const overrides = [];
|
|
45
45
|
|
|
46
|
-
const userConfig = readJsonIfExists(join(
|
|
46
|
+
const userConfig = readJsonIfExists(join(configDir(), 'plugins.json'));
|
|
47
47
|
if (userConfig?.plugins) overrides.push(...userConfig.plugins);
|
|
48
48
|
|
|
49
49
|
const projectConfig = readJsonIfExists(join(rootDir, '.cx', 'plugins.json'));
|
package/lib/env-config.mjs
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/env-config.mjs — Read and write Construct environment configuration files.
|
|
3
3
|
*
|
|
4
|
-
* Provides helpers to load
|
|
5
|
-
* key/value pairs back to disk.
|
|
6
|
-
*
|
|
4
|
+
* Provides helpers to load the user config.env (XDG config dir) and a project
|
|
5
|
+
* .env, merge them with process.env, and persist key/value pairs back to disk.
|
|
6
|
+
* Project .env wins over user config.env, which wins over shell exports. Backs
|
|
7
|
+
* setup, model-router, and the MCP server when they resolve API keys and model
|
|
8
|
+
* overrides without leaking secrets.
|
|
7
9
|
*/
|
|
8
10
|
import fs from 'node:fs';
|
|
9
11
|
import os from 'node:os';
|
|
10
12
|
import path from 'node:path';
|
|
13
|
+
import { configDir } from './config/xdg.mjs';
|
|
11
14
|
import { API_KEY_CREDENTIALS } from './providers/credential-catalog.mjs';
|
|
12
15
|
import { extractOpRef } from './providers/secret-resolver.mjs';
|
|
13
16
|
|
|
@@ -26,7 +29,7 @@ export function parseEnvFile(filePath) {
|
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
export function getUserConfigDir(homeDir = os.homedir()) {
|
|
29
|
-
return
|
|
32
|
+
return configDir(homeDir);
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
export function getUserEnvPath(homeDir = os.homedir()) {
|
|
@@ -109,31 +112,50 @@ function preferInjectedCredential(envValue, fileValue) {
|
|
|
109
112
|
export function loadConstructEnv({ rootDir, homeDir = os.homedir(), env = process.env, warn = true } = {}) {
|
|
110
113
|
const rootEnv = rootDir ? parseEnvFile(path.join(rootDir, '.env')) : {};
|
|
111
114
|
const userEnv = parseEnvFile(getUserEnvPath(homeDir));
|
|
112
|
-
|
|
115
|
+
|
|
116
|
+
// Most-specific config wins: project .env overrides user config.env, which
|
|
117
|
+
// overrides shell exports. A repo's local .env is closer to the work than a
|
|
118
|
+
// machine-wide default, so it takes the file-tier precedence.
|
|
119
|
+
const fileEnv = { ...userEnv, ...rootEnv };
|
|
120
|
+
const userEnvPath = getUserEnvPath(homeDir);
|
|
113
121
|
|
|
114
122
|
if (warn) {
|
|
123
|
+
// Every key that both files define to a different value is a real shadow:
|
|
124
|
+
// project .env silently wins, so surface it rather than gating on a
|
|
125
|
+
// hardcoded credential subset.
|
|
126
|
+
for (const key of Object.keys(rootEnv)) {
|
|
127
|
+
if (!(key in userEnv)) continue;
|
|
128
|
+
if (rootEnv[key] === userEnv[key]) continue;
|
|
129
|
+
if (preferInjectedCredential(rootEnv[key], userEnv[key])) continue;
|
|
130
|
+
process.stderr.write(
|
|
131
|
+
`[construct] WARNING: ${key} is set in both project .env (${rootEnv[key].slice(0, 6)}…) ` +
|
|
132
|
+
`and ${userEnvPath} (${userEnv[key].slice(0, 6)}…). ` +
|
|
133
|
+
`Project .env wins. To silence: remove the key from one file.\n`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
115
137
|
for (const key of Object.keys(env)) {
|
|
116
138
|
if (ENV_SHADOW_IGNORE.has(key)) continue;
|
|
117
139
|
if (!ENV_SHADOW_WARN.has(key)) continue;
|
|
118
140
|
// Only warn when process.env has a non-empty competing value.
|
|
119
141
|
// An empty-string env var (e.g. Claude for Desktop clears ANTHROPIC_API_KEY
|
|
120
|
-
// so its internal auth is used instead) is not a real conflict —
|
|
121
|
-
// silently and no user action is needed.
|
|
142
|
+
// so its internal auth is used instead) is not a real conflict — the file
|
|
143
|
+
// value wins silently and no user action is needed.
|
|
122
144
|
if (!env[key]) continue;
|
|
123
145
|
if (key in fileEnv && fileEnv[key] !== env[key]) {
|
|
124
146
|
if (preferInjectedCredential(env[key], fileEnv[key])) continue;
|
|
125
147
|
process.stderr.write(
|
|
126
148
|
`[construct] WARNING: process.env.${key} (${env[key].slice(0, 6)}…) ` +
|
|
127
|
-
`differs from config
|
|
128
|
-
`config
|
|
129
|
-
`update
|
|
149
|
+
`differs from the config file value (${fileEnv[key].slice(0, 6)}…). ` +
|
|
150
|
+
`The config file will be used. To silence: unset the shell variable or ` +
|
|
151
|
+
`update the config file to match.\n`,
|
|
130
152
|
);
|
|
131
153
|
}
|
|
132
154
|
}
|
|
133
155
|
}
|
|
134
156
|
|
|
135
|
-
// process.env is the base;
|
|
136
|
-
//
|
|
157
|
+
// process.env is the base; .env-file values override shell values so that
|
|
158
|
+
// credentials set in the dashboard always win over stale shell exports.
|
|
137
159
|
// Materialized credentials from `op run` (or an explicit export) win over stored
|
|
138
160
|
// op:// references so one 1Password auth per invocation is enough.
|
|
139
161
|
const merged = { ...env, ...fileEnv };
|
package/lib/features.mjs
CHANGED
|
@@ -9,13 +9,14 @@ import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
|
9
9
|
import { homedir } from 'os';
|
|
10
10
|
import { join } from 'path';
|
|
11
11
|
import { loadPluginRegistry } from './plugin-registry.mjs';
|
|
12
|
+
import { configDir } from './config/xdg.mjs';
|
|
12
13
|
|
|
13
14
|
function getHomeDir(overrides = {}) {
|
|
14
15
|
return overrides.homeDir ?? homedir();
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
function getFeaturesFile(overrides = {}) {
|
|
18
|
-
return overrides.featuresFile ?? join(getHomeDir(overrides), '
|
|
19
|
+
return overrides.featuresFile ?? join(configDir(getHomeDir(overrides)), 'features.json');
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
function getClaudeSettingsPath(overrides = {}) {
|
|
@@ -145,10 +146,10 @@ function getProjectMcpPaths(overrides = {}) {
|
|
|
145
146
|
* Load Claude.ai server-side managed MCP IDs.
|
|
146
147
|
* These are integrations connected through Claude.ai's web interface
|
|
147
148
|
* (e.g., Notion, Google Drive) that aren't discoverable from local config.
|
|
148
|
-
* Users declare them via
|
|
149
|
+
* Users declare them via claude-ai-mcps.json in the XDG config dir: { "mcps": ["notion", "slack"] }
|
|
149
150
|
*/
|
|
150
151
|
function getClaudeAiManagedMcps(overrides = {}) {
|
|
151
|
-
const p = overrides.claudeAiMcpsPath ?? join(getHomeDir(overrides), '
|
|
152
|
+
const p = overrides.claudeAiMcpsPath ?? join(configDir(getHomeDir(overrides)), 'claude-ai-mcps.json');
|
|
152
153
|
if (!existsSync(p)) return [];
|
|
153
154
|
try {
|
|
154
155
|
const data = JSON.parse(readFileSync(p, 'utf8'));
|