@geraldmaron/construct 1.0.0 → 1.0.1
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/.env.example +10 -7
- package/LICENSE +201 -21
- package/README.md +132 -257
- package/agents/contracts.json +387 -0
- package/agents/prompts/cx-accessibility.md +8 -0
- package/agents/prompts/cx-ai-engineer.md +92 -0
- package/agents/prompts/cx-architect.md +10 -2
- package/agents/prompts/cx-business-strategist.md +13 -0
- package/agents/prompts/cx-data-analyst.md +80 -0
- package/agents/prompts/cx-data-engineer.md +4 -0
- package/agents/prompts/cx-debugger.md +8 -0
- package/agents/prompts/cx-designer.md +19 -0
- package/agents/prompts/cx-devil-advocate.md +4 -0
- package/agents/prompts/cx-docs-keeper.md +128 -0
- package/agents/prompts/cx-engineer.md +13 -0
- package/agents/prompts/cx-evaluator.md +4 -0
- package/agents/prompts/cx-explorer.md +12 -0
- package/agents/prompts/cx-legal-compliance.md +13 -0
- package/agents/prompts/cx-operations.md +13 -0
- package/agents/prompts/cx-orchestrator.md +107 -4
- package/agents/prompts/cx-platform-engineer.md +75 -0
- package/agents/prompts/cx-product-manager.md +8 -0
- package/agents/prompts/cx-qa.md +100 -0
- package/agents/prompts/cx-rd-lead.md +13 -0
- package/agents/prompts/cx-release-manager.md +8 -0
- package/agents/prompts/cx-researcher.md +21 -1
- package/agents/prompts/cx-reviewer.md +8 -0
- package/agents/prompts/cx-security.md +104 -0
- package/agents/prompts/cx-sre.md +104 -0
- package/agents/prompts/cx-test-automation.md +4 -0
- package/agents/prompts/cx-trace-reviewer.md +7 -3
- package/agents/prompts/cx-ux-researcher.md +4 -0
- package/agents/registry.json +365 -90
- package/agents/role-manifests.json +217 -0
- package/bin/construct +3345 -141
- package/bin/construct-postinstall.mjs +87 -0
- package/commands/build/feature.md +6 -6
- package/commands/plan/feature.md +6 -5
- package/commands/plan/requirements.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/work/drive.md +3 -3
- package/commands/work/optimize-prompts.md +3 -3
- package/db/{migrations → schema}/001_init.sql +2 -0
- package/db/schema/002_pgvector.sql +182 -0
- package/db/schema/003_intake.sql +47 -0
- package/examples/README.md +85 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
- package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
- package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
- package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
- package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
- package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
- package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
- package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
- package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
- package/examples/personas/construct/bad/commit-without-approval.md +30 -0
- package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
- package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
- package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
- package/examples/provider-plugin/README.md +34 -0
- package/examples/provider-plugin/index.mjs +74 -0
- package/examples/provider-plugin/package.json +15 -0
- package/examples/seed-observations/README.md +38 -0
- package/examples/seed-observations/anti-patterns.md +44 -0
- package/examples/seed-observations/decisions.md +36 -0
- package/examples/seed-observations/patterns.md +42 -0
- package/lib/agent-contracts-enforce.mjs +158 -0
- package/lib/agent-contracts.mjs +231 -0
- package/lib/agents/postconditions.mjs +126 -0
- package/lib/agents/schema.mjs +124 -0
- package/lib/artifact-capture.mjs +183 -0
- package/lib/audit-trail.mjs +149 -0
- package/lib/auto-docs.mjs +245 -65
- package/lib/beads/auto-close.mjs +126 -0
- package/lib/beads/drift.mjs +171 -0
- package/lib/beads-automation.mjs +542 -0
- package/lib/beads-client.mjs +518 -0
- package/lib/beads-lock.mjs +377 -0
- package/lib/beads-optimistic.mjs +365 -0
- package/lib/bootstrap/built-ins.mjs +136 -0
- package/lib/bootstrap/lazy-install.mjs +161 -0
- package/lib/bootstrap/resources.mjs +120 -0
- package/lib/bootstrap.mjs +105 -0
- package/lib/cache-governor.js +213 -0
- package/lib/cache-strategy-anthropic.js +62 -0
- package/lib/cache-strategy-google.js +79 -0
- package/lib/cache-strategy-none.js +30 -0
- package/lib/cache-strategy-openai.js +48 -0
- package/lib/cache-strategy.js +91 -0
- package/lib/claude-allow.mjs +149 -0
- package/lib/cli-commands.mjs +413 -258
- package/lib/codex-config.mjs +3 -1
- package/lib/comment-lint.mjs +55 -6
- package/lib/completions.mjs +21 -6
- package/lib/config/alias.mjs +56 -0
- package/lib/config/project-config.mjs +335 -0
- package/lib/config/schema.mjs +159 -0
- package/lib/context-router.mjs +308 -0
- package/lib/cost-ledger.mjs +177 -0
- package/lib/cost.mjs +171 -10
- package/lib/dashboard-static.mjs +158 -0
- package/lib/deployment-mode.mjs +86 -0
- package/lib/deprecate.mjs +49 -0
- package/lib/dispatch-batch.js +183 -0
- package/lib/distill.mjs +21 -8
- package/lib/doc-stamp.mjs +164 -0
- package/lib/doc-verify.mjs +119 -0
- package/lib/docs-routing.mjs +89 -0
- package/lib/docs-verify.mjs +417 -0
- package/lib/doctor/audit.mjs +71 -0
- package/lib/doctor/cli.mjs +99 -0
- package/lib/doctor/escalate.mjs +29 -0
- package/lib/doctor/index.mjs +140 -0
- package/lib/doctor/report.mjs +170 -0
- package/lib/doctor/watchers/bd-watch.mjs +117 -0
- package/lib/doctor/watchers/cost.mjs +130 -0
- package/lib/doctor/watchers/disk.mjs +122 -0
- package/lib/doctor/watchers/handoffs.mjs +33 -0
- package/lib/doctor/watchers/process-pressure.mjs +60 -0
- package/lib/doctor/watchers/service-health.mjs +188 -0
- package/lib/document-extract.mjs +288 -0
- package/lib/document-ingest.mjs +230 -0
- package/lib/drop.mjs +282 -0
- package/lib/embed/approval-queue.mjs +176 -0
- package/lib/embed/artifact.mjs +349 -0
- package/lib/embed/authority-guard.mjs +155 -0
- package/lib/embed/cli.mjs +408 -0
- package/lib/embed/config.mjs +355 -0
- package/lib/embed/conflict-detection.mjs +264 -0
- package/lib/embed/customer-profiles.mjs +480 -0
- package/lib/embed/daemon.mjs +1309 -0
- package/lib/embed/demand-fetch.mjs +449 -0
- package/lib/embed/docs-lifecycle.mjs +349 -0
- package/lib/embed/inbox-live-watcher.mjs +119 -0
- package/lib/embed/inbox.mjs +343 -0
- package/lib/embed/intake-metrics.mjs +190 -0
- package/lib/embed/jobs/vector-sync.mjs +198 -0
- package/lib/embed/notifications.mjs +75 -0
- package/lib/embed/output.mjs +79 -0
- package/lib/embed/providers/github.mjs +295 -0
- package/lib/embed/providers/jira.mjs +192 -0
- package/lib/embed/providers/linear.mjs +186 -0
- package/lib/embed/providers/registry.mjs +115 -0
- package/lib/embed/providers/slack.mjs +203 -0
- package/lib/embed/recommendation-store.mjs +378 -0
- package/lib/embed/roadmap.mjs +374 -0
- package/lib/embed/role-framing.mjs +110 -0
- package/lib/embed/scheduler.mjs +99 -0
- package/lib/embed/semantic.mjs +325 -0
- package/lib/embed/snapshot.mjs +191 -0
- package/lib/embed/supervision.mjs +235 -0
- package/lib/embed/target-resolver.mjs +186 -0
- package/lib/embed/worker.mjs +62 -0
- package/lib/embed/workspaces.mjs +297 -0
- package/lib/engine/chunker-headings.mjs +110 -0
- package/lib/engine/compressor-heuristic.mjs +100 -0
- package/lib/engine/consolidate.mjs +287 -0
- package/lib/engine/contracts.mjs +126 -0
- package/lib/engine/defaults.mjs +129 -0
- package/lib/engine/eval-retrieval.mjs +148 -0
- package/lib/engine/fuser-rrf.mjs +62 -0
- package/lib/engine/index.mjs +37 -0
- package/lib/engine/registry.mjs +146 -0
- package/lib/engine/reranker-mmr.mjs +90 -0
- package/lib/engine/tokens.mjs +77 -0
- package/lib/entity-store.mjs +280 -0
- package/lib/env-config.mjs +69 -4
- package/lib/evals/retrieval-bench.mjs +159 -0
- package/lib/evaluator-optimizer.mjs +317 -0
- package/lib/features.mjs +159 -29
- package/lib/gates-audit.mjs +236 -0
- package/lib/git-hooks/prepare-commit-msg +58 -0
- package/lib/handoffs/cleanup.mjs +159 -0
- package/lib/handoffs/contract.mjs +162 -0
- package/lib/handoffs/inventory.mjs +120 -0
- package/lib/headhunt.mjs +28 -47
- package/lib/health-check.mjs +399 -0
- package/lib/hook-health.mjs +442 -0
- package/lib/hooks/_lib/log.mjs +82 -0
- package/lib/hooks/adaptive-lint.mjs +26 -2
- package/lib/hooks/agent-tracker.mjs +171 -14
- package/lib/hooks/audit-reads.mjs +109 -0
- package/lib/hooks/audit-trail.mjs +153 -0
- package/lib/hooks/bash-output-logger.mjs +71 -0
- package/lib/hooks/block-no-verify.mjs +41 -0
- package/lib/hooks/ci-status-check.mjs +82 -0
- package/lib/hooks/comment-lint.mjs +29 -7
- package/lib/hooks/config-protection.mjs +39 -18
- package/lib/hooks/context-watch.mjs +137 -0
- package/lib/hooks/context-window-recovery.mjs +4 -20
- package/lib/hooks/dep-audit.mjs +16 -0
- package/lib/hooks/doc-coupling-check.mjs +80 -0
- package/lib/hooks/edit-accumulator.mjs +3 -0
- package/lib/hooks/edit-error-recovery.mjs +3 -0
- package/lib/hooks/edit-guard.mjs +45 -4
- package/lib/hooks/env-check.mjs +3 -0
- package/lib/hooks/guard-bash.mjs +58 -1
- package/lib/hooks/mcp-audit.mjs +18 -20
- package/lib/hooks/mcp-health-check.mjs +36 -0
- package/lib/hooks/model-fallback.mjs +42 -56
- package/lib/hooks/policy-engine.mjs +209 -0
- package/lib/hooks/post-merge-docs-check.mjs +63 -0
- package/lib/hooks/pre-compact.mjs +4 -24
- package/lib/hooks/pre-push-gate.mjs +200 -37
- package/lib/hooks/proactive-activation.mjs +284 -0
- package/lib/hooks/read-tracker.mjs +27 -4
- package/lib/hooks/readme-age-check.mjs +77 -0
- package/lib/hooks/registry-sync.mjs +12 -5
- package/lib/hooks/scan-secrets.mjs +50 -7
- package/lib/hooks/session-optimize.mjs +311 -0
- package/lib/hooks/session-start.mjs +287 -28
- package/lib/hooks/stop-notify.mjs +236 -96
- package/lib/hooks/stop-typecheck.mjs +13 -1
- package/lib/hooks/test-watch.mjs +68 -0
- package/lib/host-capabilities.mjs +31 -10
- package/lib/init-docs.mjs +731 -291
- package/lib/init-unified.mjs +1116 -0
- package/lib/init-update.mjs +168 -0
- package/lib/init.mjs +107 -0
- package/lib/install/first-invocation.mjs +119 -0
- package/lib/install/stage-project.mjs +69 -0
- package/lib/intake/classify.mjs +278 -0
- package/lib/intake/feedback.mjs +273 -0
- package/lib/intake/filesystem-queue.mjs +158 -0
- package/lib/intake/intake-config.mjs +132 -0
- package/lib/intake/postgres-queue.mjs +198 -0
- package/lib/intake/prepare.mjs +139 -0
- package/lib/intake/queue.mjs +83 -0
- package/lib/intake/session-prelude.mjs +50 -0
- package/lib/integrations/intake-integrations.mjs +740 -0
- package/lib/intent-classifier.mjs +253 -0
- package/lib/knowledge/layout.mjs +72 -0
- package/lib/knowledge/rag.mjs +331 -0
- package/lib/knowledge/search.mjs +315 -0
- package/lib/knowledge/trends.mjs +261 -0
- package/lib/logger.mjs +85 -0
- package/lib/mcp/broker.mjs +124 -0
- package/lib/mcp/server.mjs +554 -854
- package/lib/mcp/tools/document.mjs +132 -0
- package/lib/mcp/tools/memory.mjs +209 -0
- package/lib/mcp/tools/project.mjs +349 -0
- package/lib/mcp/tools/skills.mjs +409 -0
- package/lib/mcp/tools/storage.mjs +60 -0
- package/lib/mcp/tools/telemetry.mjs +315 -0
- package/lib/mcp/tools/workflow.mjs +112 -0
- package/lib/mcp-catalog.json +54 -3
- package/lib/mcp-manager.mjs +96 -48
- package/lib/mcp-platform-config.mjs +22 -22
- package/lib/memory-stats.mjs +121 -0
- package/lib/mode-commands.mjs +124 -0
- package/lib/model-free-selector.mjs +184 -0
- package/lib/model-pricing.mjs +152 -0
- package/lib/model-registry.mjs +226 -0
- package/lib/model-router.mjs +362 -386
- package/lib/observation-store.mjs +391 -0
- package/lib/ollama-manager.mjs +428 -0
- package/lib/opencode-config.mjs +13 -3
- package/lib/opencode-runtime-plugin.mjs +70 -38
- package/lib/opencode-telemetry.mjs +64 -6
- package/lib/orchestration-policy.mjs +509 -6
- package/lib/overrides/resolver.mjs +207 -0
- package/lib/parity.mjs +147 -0
- package/lib/paths.mjs +33 -0
- package/lib/performance/generate.mjs +212 -0
- package/lib/plugin-registry.mjs +268 -0
- package/lib/policy/engine.mjs +130 -0
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-detection.mjs +129 -0
- package/lib/project-init-shared.mjs +272 -0
- package/lib/project-profile.mjs +447 -0
- package/lib/prompt-composer.js +434 -0
- package/lib/prompt-metadata.mjs +1 -1
- package/lib/provider-capabilities-anthropic.js +44 -0
- package/lib/provider-capabilities-deepseek.js +37 -0
- package/lib/provider-capabilities-generic.js +26 -0
- package/lib/provider-capabilities-google.js +47 -0
- package/lib/provider-capabilities-openai.js +45 -0
- package/lib/provider-capabilities.js +142 -0
- package/lib/providers/atlassian-confluence/index.mjs +103 -0
- package/lib/providers/atlassian-jira/index.mjs +100 -0
- package/lib/providers/auth-manager.mjs +126 -0
- package/lib/providers/circuit-breaker.mjs +124 -0
- package/lib/providers/contract.mjs +93 -0
- package/lib/providers/github/index.mjs +126 -0
- package/lib/providers/registry.mjs +184 -0
- package/lib/providers/salesforce/index.mjs +100 -0
- package/lib/providers/slack/index.mjs +80 -0
- package/lib/reflect.mjs +137 -0
- package/lib/research-lint.mjs +164 -0
- package/lib/resources/budget.mjs +259 -0
- package/lib/role-preload.mjs +21 -6
- package/lib/roles/approval-surface.mjs +54 -0
- package/lib/roles/cli.mjs +118 -0
- package/lib/roles/event-bus.mjs +79 -0
- package/lib/roles/fence.mjs +84 -0
- package/lib/roles/gateway.mjs +260 -0
- package/lib/roles/hook-emit.mjs +37 -0
- package/lib/roles/manifest.mjs +48 -0
- package/lib/roles/router.mjs +27 -0
- package/lib/runtime-pressure.mjs +360 -0
- package/lib/schema-artifact.mjs +134 -0
- package/lib/schema-infer.mjs +551 -0
- package/lib/server/auth.mjs +168 -0
- package/lib/server/chat.mjs +336 -0
- package/lib/server/cors.mjs +77 -0
- package/lib/server/csrf.mjs +91 -0
- package/lib/server/index.mjs +1927 -78
- package/lib/server/insights.mjs +765 -0
- package/lib/server/rate-limit.mjs +91 -0
- package/lib/server/static/assets/index-ab25c707.js +70 -0
- package/lib/server/static/assets/index-f0c80a2b.css +1 -0
- package/lib/server/static/index.html +12 -817
- package/lib/server/telemetry-login.mjs +108 -0
- package/lib/server/webhook.mjs +510 -0
- package/lib/service-manager.mjs +522 -58
- package/lib/services/pattern-promotion-service.mjs +167 -0
- package/lib/services/telemetry-backend.mjs +178 -0
- package/lib/session-store.mjs +374 -0
- package/lib/setup-prompts.mjs +96 -0
- package/lib/setup.mjs +525 -38
- package/lib/skills-apply.mjs +280 -0
- package/lib/skills-scope.mjs +118 -0
- package/lib/status.mjs +261 -70
- package/lib/storage/admin.mjs +355 -0
- package/lib/storage/backend.mjs +2 -1
- package/lib/storage/backup.mjs +347 -0
- package/lib/storage/embeddings-engine.mjs +133 -0
- package/lib/storage/embeddings-legacy.mjs +85 -0
- package/lib/storage/embeddings-local.mjs +108 -0
- package/lib/storage/embeddings-ollama.mjs +78 -0
- package/lib/storage/embeddings-openai.mjs +85 -0
- package/lib/storage/embeddings.mjs +92 -33
- package/lib/storage/file-lock.mjs +130 -0
- package/lib/storage/fusion.mjs +95 -0
- package/lib/storage/hybrid-query.mjs +34 -27
- package/lib/storage/migrations.mjs +187 -0
- package/lib/storage/postgres-backup.mjs +124 -0
- package/lib/storage/sql-store.mjs +5 -15
- package/lib/storage/state-source.mjs +12 -13
- package/lib/storage/store-version.mjs +115 -0
- package/lib/storage/sync.mjs +144 -35
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/storage/vector-client.mjs +286 -0
- package/lib/storage/vector-store.mjs +71 -30
- package/lib/task-graph/generate.mjs +135 -0
- package/lib/task-graph/schema.mjs +81 -0
- package/lib/task-graph/store.mjs +71 -0
- package/lib/telemetry/backends/local.mjs +62 -0
- package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
- package/lib/telemetry/backfill.mjs +180 -0
- package/lib/telemetry/eval-datasets.mjs +203 -0
- package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
- package/lib/telemetry/intent-verifications.mjs +86 -0
- package/lib/telemetry/llm-judge.mjs +350 -0
- package/lib/telemetry/model-pricing-catalog.mjs +557 -0
- package/lib/telemetry/setup.mjs +151 -0
- package/lib/telemetry/skill-calls.mjs +78 -0
- package/lib/telemetry/team-rollup.mjs +4 -4
- package/lib/token-engine.js +117 -0
- package/lib/token-estimator-anthropic.js +15 -0
- package/lib/token-estimator-deepseek.js +13 -0
- package/lib/token-estimator-default.js +13 -0
- package/lib/token-estimator-google.js +13 -0
- package/lib/token-estimator-openai.js +13 -0
- package/lib/toolkit-env.mjs +1 -1
- package/lib/tty-prompts.mjs +211 -0
- package/lib/uninstall/uninstall.mjs +423 -0
- package/lib/update.mjs +115 -0
- package/lib/upgrade.mjs +141 -0
- package/lib/validator.mjs +51 -2
- package/lib/validators/skills.mjs +142 -0
- package/lib/wireframe.mjs +422 -0
- package/lib/worker/entrypoint.mjs +241 -0
- package/lib/worker/evidence.mjs +107 -0
- package/lib/worker/run.mjs +154 -0
- package/lib/worker/trace.mjs +182 -0
- package/lib/workflow-state.mjs +14 -18
- package/package.json +21 -8
- package/personas/construct.md +53 -51
- package/platforms/claude/CLAUDE.md +1 -1
- package/platforms/claude/settings.template.json +115 -68
- package/platforms/opencode/config.template.json +3 -7
- package/rules/common/agents.md +11 -38
- package/rules/common/beads-hygiene.md +75 -0
- package/rules/common/code-review.md +11 -100
- package/rules/common/coding-style.md +5 -3
- package/rules/common/comments.md +30 -111
- package/rules/common/commit-approval.md +53 -0
- package/rules/common/cx-agent-routing.md +1 -0
- package/rules/common/development-workflow.md +23 -41
- package/rules/common/doc-ownership.md +54 -0
- package/rules/common/efficiency.md +57 -0
- package/rules/common/framing.md +76 -0
- package/rules/common/git-workflow.md +2 -4
- package/rules/common/patterns.md +3 -7
- package/rules/common/performance.md +15 -31
- package/rules/common/release-gates.md +69 -0
- package/rules/common/research.md +107 -0
- package/rules/common/security.md +6 -6
- package/rules/common/skill-composition.md +67 -0
- package/rules/common/testing.md +15 -27
- package/rules/golang/hooks.md +0 -4
- package/rules/policy/bootstrap.yaml +11 -0
- package/rules/policy/drive.yaml +8 -0
- package/rules/policy/task.yaml +9 -0
- package/rules/policy/workflow.yaml +9 -0
- package/rules/python/hooks.md +0 -4
- package/rules/swift/hooks.md +0 -4
- package/rules/typescript/hooks.md +0 -4
- package/rules/web/hooks.md +0 -2
- package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
- package/skills/ai/prompt-optimizer.md +7 -7
- package/skills/compliance/ai-disclosure.md +58 -0
- package/skills/compliance/data-privacy.md +46 -0
- package/skills/compliance/license-audit.md +40 -0
- package/skills/compliance/regulatory-review.md +61 -0
- package/skills/docs/document-ingest-workflow.md +52 -0
- package/skills/docs/evidence-ingest-workflow.md +9 -0
- package/skills/docs/init-docs.md +51 -18
- package/skills/docs/product-intelligence-workflow.md +12 -0
- package/skills/docs/product-signal-workflow.md +4 -0
- package/skills/docs/research-workflow.md +23 -8
- package/skills/docs/runbook-workflow.md +1 -1
- package/skills/operating/orchestration-reference.md +151 -0
- package/skills/roles/architect.md +5 -0
- package/skills/roles/engineer.md +32 -0
- package/skills/roles/operator.md +12 -0
- package/skills/roles/reviewer.md +23 -0
- package/skills/routing.md +1 -1
- package/templates/devcontainer/Dockerfile.devcontainer +38 -0
- package/templates/devcontainer/devcontainer.json +31 -0
- package/templates/distribution/bootstrap.ps1 +142 -0
- package/templates/distribution/bootstrap.sh +196 -0
- package/templates/distribution/run.mjs +187 -0
- package/templates/docs/adr.md +44 -8
- package/templates/docs/changelog-entry.md +43 -0
- package/templates/docs/construct_guide.md +149 -0
- package/templates/docs/evidence-brief.md +2 -2
- package/templates/docs/meta-prd.md +140 -19
- package/templates/docs/onboarding.md +57 -0
- package/templates/docs/prd.md +159 -15
- package/templates/docs/research-brief.md +4 -4
- package/templates/homebrew/construct.rb +67 -0
- package/langfuse/docker-compose.yml +0 -82
- package/lib/eval-harness.mjs +0 -59
- package/lib/hooks/bootstrap-guard.mjs +0 -90
- package/lib/hooks/console-warn.mjs +0 -43
- package/lib/hooks/continuation-enforcer.mjs +0 -72
- package/lib/hooks/drive-guard.mjs +0 -89
- package/lib/hooks/mcp-task-scope.mjs +0 -47
- package/lib/hooks/task-completed-guard.mjs +0 -43
- package/lib/hooks/teammate-idle-guard.mjs +0 -54
- package/lib/hooks/workflow-guard.mjs +0 -62
- package/lib/prompt-composer.mjs +0 -196
- package/lib/review.mjs +0 -429
- package/lib/server/static/app.js +0 -841
- package/lib/telemetry/langfuse-model-sync.mjs +0 -270
- package/rules/common/hooks.md +0 -35
- package/rules/zh/README.md +0 -113
- package/rules/zh/agents.md +0 -55
- package/rules/zh/code-review.md +0 -129
- package/rules/zh/coding-style.md +0 -53
- package/rules/zh/development-workflow.md +0 -49
- package/rules/zh/git-workflow.md +0 -29
- package/rules/zh/hooks.md +0 -35
- package/rules/zh/patterns.md +0 -36
- package/rules/zh/performance.md +0 -60
- package/rules/zh/security.md +0 -34
- package/rules/zh/testing.md +0 -34
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/document-ingest.mjs — convert local source documents into normalized markdown artifacts.
|
|
3
|
+
*
|
|
4
|
+
* Reuses the shared extraction backends from lib/document-extract.mjs, writes
|
|
5
|
+
* markdown outputs into retrieval-friendly project paths, and can optionally
|
|
6
|
+
* trigger storage sync for SQL/vector indexing.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { basename, dirname, extname, isAbsolute, join, parse, relative, resolve } from 'node:path';
|
|
10
|
+
import { extractDocumentText, extractDocumentMetadata, isExtractableDocumentPath } from './document-extract.mjs';
|
|
11
|
+
import { syncFileStateToSql } from './storage/sync.mjs';
|
|
12
|
+
import { stampFrontmatter } from './doc-stamp.mjs';
|
|
13
|
+
import { KNOWLEDGE_ROOT, KNOWLEDGE_SUBDIRS } from './knowledge/layout.mjs';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_TARGET_DIR = '.cx/knowledge/internal';
|
|
16
|
+
|
|
17
|
+
function slugify(value) {
|
|
18
|
+
return String(value)
|
|
19
|
+
.normalize('NFKD')
|
|
20
|
+
.replace(/[^\w.-]+/g, '-')
|
|
21
|
+
.replace(/-+/g, '-')
|
|
22
|
+
.replace(/^-|-$/g, '')
|
|
23
|
+
.toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeOutputPath(value, cwd) {
|
|
27
|
+
return isAbsolute(value) ? value : resolve(cwd, value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatTitle(sourcePath) {
|
|
31
|
+
return basename(sourcePath).replace(/\.[^.]+$/, '').replace(/[-_]+/g, ' ').trim() || 'Ingested document';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function inferProjectName(rootDir) {
|
|
35
|
+
const name = basename(resolve(rootDir)).trim();
|
|
36
|
+
return slugify(name || 'construct') || 'construct';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ensureDir(dirPath) {
|
|
40
|
+
mkdirSync(dirPath, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function nextAvailablePath(targetPath) {
|
|
44
|
+
if (!existsSync(targetPath)) return targetPath;
|
|
45
|
+
const parsed = parse(targetPath);
|
|
46
|
+
let index = 2;
|
|
47
|
+
while (true) {
|
|
48
|
+
const candidate = join(parsed.dir, `${parsed.name}-${index}${parsed.ext}`);
|
|
49
|
+
if (!existsSync(candidate)) return candidate;
|
|
50
|
+
index += 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderMarkdown({ sourcePath, extractedAt, title, extractionMethod, characters, truncated, text, outputPath, cwd, metadata }) {
|
|
55
|
+
const relSource = relative(cwd, sourcePath) || basename(sourcePath);
|
|
56
|
+
const relOutput = relative(cwd, outputPath) || basename(outputPath);
|
|
57
|
+
const lines = [
|
|
58
|
+
'---',
|
|
59
|
+
`source_path: ${JSON.stringify(sourcePath)}`,
|
|
60
|
+
`source_relative_path: ${JSON.stringify(relSource)}`,
|
|
61
|
+
`source_extension: ${JSON.stringify(extname(sourcePath).toLowerCase())}`,
|
|
62
|
+
`extracted_at: ${JSON.stringify(extractedAt)}`,
|
|
63
|
+
`extraction_method: ${JSON.stringify(extractionMethod)}`,
|
|
64
|
+
`characters: ${characters}`,
|
|
65
|
+
`truncated: ${truncated ? 'true' : 'false'}`,
|
|
66
|
+
`output_path: ${JSON.stringify(outputPath)}`,
|
|
67
|
+
`output_relative_path: ${JSON.stringify(relOutput)}`,
|
|
68
|
+
];
|
|
69
|
+
if (metadata?.authors?.length) lines.push(`authors: [${metadata.authors.map((a) => JSON.stringify(a)).join(', ')}]`);
|
|
70
|
+
if (metadata?.dates && Object.keys(metadata.dates).length) {
|
|
71
|
+
for (const [k, v] of Object.entries(metadata.dates)) lines.push(`source_${k}: ${JSON.stringify(v)}`);
|
|
72
|
+
}
|
|
73
|
+
if (metadata?.links?.length) lines.push(`source_links_count: ${metadata.links.length}`);
|
|
74
|
+
lines.push('---', '', `# ${title}`, '', '## Source', '', `- File: \`${relSource}\``, `- Method: \`${extractionMethod}\``, `- Characters: ${characters}`, `- Truncated: ${truncated ? 'yes' : 'no'}`, `- Extracted at: ${extractedAt}`);
|
|
75
|
+
if (metadata?.authors?.length) lines.push(`- Authors: ${metadata.authors.join(', ')}`);
|
|
76
|
+
lines.push('', '## Extracted Content', '', text, '');
|
|
77
|
+
return lines.join('\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function collectInputFiles(inputPath) {
|
|
81
|
+
const resolvedPath = resolve(inputPath);
|
|
82
|
+
if (!existsSync(resolvedPath)) throw new Error(`Input path not found: ${resolvedPath}`);
|
|
83
|
+
|
|
84
|
+
const stat = statSync(resolvedPath);
|
|
85
|
+
if (stat.isFile()) {
|
|
86
|
+
if (!isExtractableDocumentPath(resolvedPath)) return [];
|
|
87
|
+
return [resolvedPath];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!stat.isDirectory()) return [];
|
|
91
|
+
|
|
92
|
+
const files = [];
|
|
93
|
+
const stack = [resolvedPath];
|
|
94
|
+
while (stack.length > 0) {
|
|
95
|
+
const current = stack.pop();
|
|
96
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
97
|
+
const full = join(current, entry.name);
|
|
98
|
+
if (entry.isDirectory()) {
|
|
99
|
+
stack.push(full);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (entry.isFile() && isExtractableDocumentPath(full)) files.push(full);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return files.sort((a, b) => a.localeCompare(b));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveOutputPath(sourcePath, { cwd, outputPath, outputDir, target }) {
|
|
109
|
+
if (outputPath) return normalizeOutputPath(outputPath, cwd);
|
|
110
|
+
if (target === 'sibling') {
|
|
111
|
+
return join(dirname(sourcePath), `${basename(sourcePath)}.md`);
|
|
112
|
+
}
|
|
113
|
+
// knowledge/<subdir> targets land in .cx/knowledge/<subdir>/
|
|
114
|
+
if (target.startsWith('knowledge/')) {
|
|
115
|
+
const sub = target.slice('knowledge/'.length);
|
|
116
|
+
const resolvedDir = normalizeOutputPath(`${KNOWLEDGE_ROOT}/${sub}`, cwd);
|
|
117
|
+
return join(resolvedDir, `${basename(sourcePath)}.md`);
|
|
118
|
+
}
|
|
119
|
+
const resolvedDir = normalizeOutputPath(outputDir || DEFAULT_TARGET_DIR, cwd);
|
|
120
|
+
return join(resolvedDir, `${basename(sourcePath)}.md`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function ingestDocuments(inputPaths, {
|
|
124
|
+
cwd = process.cwd(),
|
|
125
|
+
outputPath = null,
|
|
126
|
+
outputDir = null,
|
|
127
|
+
target = 'knowledge/internal',
|
|
128
|
+
sync = false,
|
|
129
|
+
env = process.env,
|
|
130
|
+
} = {}) {
|
|
131
|
+
if (!Array.isArray(inputPaths) || inputPaths.length === 0) {
|
|
132
|
+
throw new Error('At least one input path is required');
|
|
133
|
+
}
|
|
134
|
+
if (outputPath && inputPaths.length > 1) {
|
|
135
|
+
throw new Error('--out can only be used with a single input path');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const files = inputPaths.flatMap((inputPath) => collectInputFiles(isAbsolute(inputPath) ? inputPath : resolve(cwd, inputPath)));
|
|
139
|
+
if (files.length === 0) {
|
|
140
|
+
throw new Error('No supported document files found');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const results = [];
|
|
144
|
+
for (const sourcePath of files) {
|
|
145
|
+
const extracted = extractDocumentText(sourcePath, { maxChars: 200_000 });
|
|
146
|
+
const metadata = extractDocumentMetadata(sourcePath);
|
|
147
|
+
const targetPath = resolveOutputPath(sourcePath, { cwd, outputPath, outputDir, target });
|
|
148
|
+
ensureDir(dirname(targetPath));
|
|
149
|
+
const finalPath = nextAvailablePath(targetPath);
|
|
150
|
+
const extractedAt = new Date().toISOString();
|
|
151
|
+
const title = metadata?.title || formatTitle(sourcePath);
|
|
152
|
+
const markdown = renderMarkdown({
|
|
153
|
+
sourcePath,
|
|
154
|
+
extractedAt,
|
|
155
|
+
title,
|
|
156
|
+
extractionMethod: extracted.extractionMethod,
|
|
157
|
+
characters: extracted.characters,
|
|
158
|
+
truncated: extracted.truncated,
|
|
159
|
+
text: extracted.text,
|
|
160
|
+
outputPath: finalPath,
|
|
161
|
+
cwd,
|
|
162
|
+
metadata,
|
|
163
|
+
});
|
|
164
|
+
writeFileSync(finalPath, stampFrontmatter(markdown, { generator: 'construct/ingest' }));
|
|
165
|
+
results.push({
|
|
166
|
+
sourcePath,
|
|
167
|
+
outputPath: finalPath,
|
|
168
|
+
extension: extracted.extension,
|
|
169
|
+
extractionMethod: extracted.extractionMethod,
|
|
170
|
+
truncated: extracted.truncated,
|
|
171
|
+
characters: extracted.characters,
|
|
172
|
+
metadata: { title, authors: metadata?.authors || [], dates: metadata?.dates || {} },
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let syncResult = null;
|
|
177
|
+
if (sync) {
|
|
178
|
+
syncResult = await syncFileStateToSql(cwd, {
|
|
179
|
+
env,
|
|
180
|
+
project: inferProjectName(cwd),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
status: 'ok',
|
|
186
|
+
target: outputPath ? 'custom' : target,
|
|
187
|
+
outputDir: outputPath
|
|
188
|
+
? dirname(normalizeOutputPath(outputPath, cwd))
|
|
189
|
+
: target === 'sibling'
|
|
190
|
+
? null
|
|
191
|
+
: target.startsWith('knowledge/')
|
|
192
|
+
? normalizeOutputPath(`${KNOWLEDGE_ROOT}/${target.slice('knowledge/'.length)}`, cwd)
|
|
193
|
+
: normalizeOutputPath(outputDir || DEFAULT_TARGET_DIR, cwd),
|
|
194
|
+
indexedLocally: true,
|
|
195
|
+
storageSync: syncResult,
|
|
196
|
+
files: results,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function runIngestCli(argv = process.argv.slice(2), { cwd = process.cwd(), env = process.env } = {}) {
|
|
201
|
+
const inputs = [];
|
|
202
|
+
let outputPath = null;
|
|
203
|
+
let outputDir = null;
|
|
204
|
+
let target = 'knowledge/internal';
|
|
205
|
+
let sync = false;
|
|
206
|
+
|
|
207
|
+
for (const arg of argv) {
|
|
208
|
+
if (arg.startsWith('--out=')) outputPath = arg.split('=').slice(1).join('=');
|
|
209
|
+
else if (arg.startsWith('--out-dir=')) outputDir = arg.split('=').slice(1).join('=');
|
|
210
|
+
else if (arg.startsWith('--target=')) target = arg.split('=').slice(1).join('=');
|
|
211
|
+
else if (arg === '--sync') sync = true;
|
|
212
|
+
else inputs.push(arg);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (inputs.length === 0) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Usage: construct ingest <file-or-dir> [more paths] [--out=FILE] [--out-dir=DIR] ` +
|
|
218
|
+
`[--target=sibling|knowledge/<subdir>] [--sync]\n` +
|
|
219
|
+
` knowledge subdirs: ${KNOWLEDGE_SUBDIRS.join(', ')}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const knowledgeTargets = KNOWLEDGE_SUBDIRS.map((s) => `knowledge/${s}`);
|
|
223
|
+
if (!['sibling', ...knowledgeTargets].includes(target)) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Unsupported target: ${target}. Valid targets: sibling, ${knowledgeTargets.join(', ')}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return ingestDocuments(inputs, { cwd, outputPath, outputDir, target, sync, env });
|
|
230
|
+
}
|
package/lib/drop.mjs
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/drop.mjs — `construct drop` command, frictionless ingest of recently
|
|
3
|
+
* saved files from drop-zone directories (~/Downloads, ~/Desktop, etc.).
|
|
4
|
+
*
|
|
5
|
+
* The user drops a file in their host session; the host often can't read it
|
|
6
|
+
* because of sandbox constraints. `construct drop` closes that gap: it
|
|
7
|
+
* surfaces the N most recent files in configured watch dirs, filters by
|
|
8
|
+
* ingestable types (PDF, Office, text, etc.), and runs `ingestDocuments`
|
|
9
|
+
* on the selection. Cross-platform; no external dependencies.
|
|
10
|
+
*
|
|
11
|
+
* Watch dirs default to XDG-ish sane values per platform; override via
|
|
12
|
+
* `CONSTRUCT_DROP_DIRS` env (colon-separated) or `--source <dir>`.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
15
|
+
import { homedir, platform } from 'node:os';
|
|
16
|
+
import { basename, extname, join } from 'node:path';
|
|
17
|
+
import { isExtractableDocumentPath } from './document-extract.mjs';
|
|
18
|
+
import { ingestDocuments } from './document-ingest.mjs';
|
|
19
|
+
|
|
20
|
+
const DEFAULT_RECENCY_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
21
|
+
|
|
22
|
+
function defaultWatchDirs(home = homedir()) {
|
|
23
|
+
const downloads = join(home, 'Downloads');
|
|
24
|
+
const desktop = join(home, 'Desktop');
|
|
25
|
+
const documents = join(home, 'Documents');
|
|
26
|
+
const dirs = [downloads, desktop, documents];
|
|
27
|
+
// macOS: include ~/Library/Mobile Documents when it exists (iCloud drop)
|
|
28
|
+
if (platform() === 'darwin') {
|
|
29
|
+
const icloud = join(home, 'Library', 'Mobile Documents', 'com~apple~CloudDocs', 'Downloads');
|
|
30
|
+
dirs.push(icloud);
|
|
31
|
+
}
|
|
32
|
+
return dirs.filter((d) => existsSync(d));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveWatchDirs({ env = process.env, override = null, home = homedir() } = {}) {
|
|
36
|
+
if (override && override.length > 0) return override;
|
|
37
|
+
const raw = env.CONSTRUCT_DROP_DIRS;
|
|
38
|
+
if (raw) {
|
|
39
|
+
return raw.split(':').map((s) => s.trim()).filter(Boolean).filter((p) => existsSync(p));
|
|
40
|
+
}
|
|
41
|
+
return defaultWatchDirs(home);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseSinceSpec(spec) {
|
|
45
|
+
if (!spec) return DEFAULT_RECENCY_MS;
|
|
46
|
+
const match = /^(\d+)([smhd])$/i.exec(String(spec).trim());
|
|
47
|
+
if (!match) return DEFAULT_RECENCY_MS;
|
|
48
|
+
const value = Number(match[1]);
|
|
49
|
+
const unit = match[2].toLowerCase();
|
|
50
|
+
const multipliers = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
|
|
51
|
+
return value * multipliers[unit];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Walks watch dirs (non-recursive) and returns file candidates.
|
|
56
|
+
* Sorted by most-recent mtime first. Honors extension filter.
|
|
57
|
+
*/
|
|
58
|
+
export function collectCandidates({
|
|
59
|
+
dirs,
|
|
60
|
+
sinceMs = DEFAULT_RECENCY_MS,
|
|
61
|
+
extensionFilter = null,
|
|
62
|
+
limit = 10,
|
|
63
|
+
now = Date.now(),
|
|
64
|
+
} = {}) {
|
|
65
|
+
const cutoff = now - sinceMs;
|
|
66
|
+
const results = [];
|
|
67
|
+
for (const dir of dirs) {
|
|
68
|
+
try {
|
|
69
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
if (!entry.isFile()) continue;
|
|
72
|
+
if (entry.name.startsWith('.')) continue; // hidden / temp files
|
|
73
|
+
const full = join(dir, entry.name);
|
|
74
|
+
let stats;
|
|
75
|
+
try { stats = statSync(full); } catch { continue; }
|
|
76
|
+
if (stats.mtimeMs < cutoff) continue;
|
|
77
|
+
const ext = extname(entry.name).slice(1).toLowerCase();
|
|
78
|
+
if (extensionFilter && ext !== extensionFilter.replace(/^\./, '').toLowerCase()) continue;
|
|
79
|
+
if (!isExtractableDocumentPath(full)) continue;
|
|
80
|
+
results.push({
|
|
81
|
+
path: full,
|
|
82
|
+
name: entry.name,
|
|
83
|
+
size: stats.size,
|
|
84
|
+
mtime: stats.mtime.toISOString(),
|
|
85
|
+
mtimeMs: stats.mtimeMs,
|
|
86
|
+
source: dir,
|
|
87
|
+
ext,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
} catch { /* unreadable dir — skip */ }
|
|
91
|
+
}
|
|
92
|
+
results.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
93
|
+
return results.slice(0, limit);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatSize(bytes) {
|
|
97
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
98
|
+
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
99
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function formatAge(mtimeMs, now = Date.now()) {
|
|
103
|
+
const diff = now - mtimeMs;
|
|
104
|
+
if (diff < 60_000) return `${Math.round(diff / 1000)}s ago`;
|
|
105
|
+
if (diff < 3_600_000) return `${Math.round(diff / 60_000)}m ago`;
|
|
106
|
+
if (diff < 86_400_000) return `${Math.round(diff / 3_600_000)}h ago`;
|
|
107
|
+
return `${Math.round(diff / 86_400_000)}d ago`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function printCandidates(candidates) {
|
|
111
|
+
if (candidates.length === 0) {
|
|
112
|
+
console.log('No recent ingestable files found in drop-zone directories.');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
console.log(`Recent drop-zone files (${candidates.length}):`);
|
|
116
|
+
console.log('─'.repeat(90));
|
|
117
|
+
candidates.forEach((c, i) => {
|
|
118
|
+
const num = String(i + 1).padStart(2, ' ');
|
|
119
|
+
const name = c.name.length > 40 ? c.name.slice(0, 37) + '…' : c.name.padEnd(40);
|
|
120
|
+
const size = formatSize(c.size).padStart(8);
|
|
121
|
+
const age = formatAge(c.mtimeMs).padStart(8);
|
|
122
|
+
const src = basename(c.source);
|
|
123
|
+
console.log(` ${num}. ${name} ${size} ${age} ${src}`);
|
|
124
|
+
});
|
|
125
|
+
console.log('');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function confirm(question) {
|
|
129
|
+
if (!process.stdin.isTTY) return true;
|
|
130
|
+
process.stdout.write(`${question} [Y/n] `);
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
process.stdin.setEncoding('utf8');
|
|
133
|
+
process.stdin.once('data', (chunk) => {
|
|
134
|
+
const answer = String(chunk).trim().toLowerCase();
|
|
135
|
+
resolve(answer === '' || answer === 'y' || answer === 'yes');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function printHelp() {
|
|
141
|
+
console.log(`Usage: construct drop [options]
|
|
142
|
+
|
|
143
|
+
Ingests the most recently dropped file(s) from your drop-zone directories
|
|
144
|
+
(Downloads, Desktop, Documents, iCloud Drive Downloads). Closes the gap
|
|
145
|
+
where a host session can't read files outside its sandbox.
|
|
146
|
+
|
|
147
|
+
Options:
|
|
148
|
+
--list Show candidates without ingesting
|
|
149
|
+
--all Ingest all listed candidates (default: most recent)
|
|
150
|
+
--index N[,N...] Ingest specific candidates by list index
|
|
151
|
+
--yes, -y Skip confirmation prompt
|
|
152
|
+
--source <dir> Override watch directory (repeatable)
|
|
153
|
+
--type <ext> Filter by extension (pdf, docx, xlsx, txt, md, etc.)
|
|
154
|
+
--since <spec> Recency window: 30m, 1h, 2d (default: 24h)
|
|
155
|
+
--target <t> Ingest target: knowledge/internal (default) | knowledge/<subdir> | sibling
|
|
156
|
+
--sync Rebuild hybrid index after ingest
|
|
157
|
+
--json Emit machine-readable output
|
|
158
|
+
-h, --help Show this message
|
|
159
|
+
|
|
160
|
+
Environment:
|
|
161
|
+
CONSTRUCT_DROP_DIRS Colon-separated override for watch directories
|
|
162
|
+
|
|
163
|
+
Examples:
|
|
164
|
+
construct drop # ingest most recent ingestable file
|
|
165
|
+
construct drop --list # list candidates, no ingest
|
|
166
|
+
construct drop --index 2,3 # ingest candidates 2 and 3
|
|
167
|
+
construct drop --type pdf --since 1h # PDFs saved in last hour
|
|
168
|
+
`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseArgs(argv) {
|
|
172
|
+
const options = {
|
|
173
|
+
list: false,
|
|
174
|
+
all: false,
|
|
175
|
+
indexes: null,
|
|
176
|
+
yes: false,
|
|
177
|
+
sources: [],
|
|
178
|
+
type: null,
|
|
179
|
+
since: null,
|
|
180
|
+
target: null,
|
|
181
|
+
sync: false,
|
|
182
|
+
json: false,
|
|
183
|
+
help: false,
|
|
184
|
+
};
|
|
185
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
186
|
+
const arg = argv[i];
|
|
187
|
+
if (arg === '--list') options.list = true;
|
|
188
|
+
else if (arg === '--all') options.all = true;
|
|
189
|
+
else if (arg === '--yes' || arg === '-y') options.yes = true;
|
|
190
|
+
else if (arg === '--sync') options.sync = true;
|
|
191
|
+
else if (arg === '--json') options.json = true;
|
|
192
|
+
else if (arg === '--help' || arg === '-h') options.help = true;
|
|
193
|
+
else if (arg === '--source') options.sources.push(argv[++i]);
|
|
194
|
+
else if (arg.startsWith('--source=')) options.sources.push(arg.slice(9));
|
|
195
|
+
else if (arg === '--type') options.type = argv[++i];
|
|
196
|
+
else if (arg.startsWith('--type=')) options.type = arg.slice(7);
|
|
197
|
+
else if (arg === '--since') options.since = argv[++i];
|
|
198
|
+
else if (arg.startsWith('--since=')) options.since = arg.slice(8);
|
|
199
|
+
else if (arg === '--target') options.target = argv[++i];
|
|
200
|
+
else if (arg.startsWith('--target=')) options.target = arg.slice(9);
|
|
201
|
+
else if (arg === '--index') options.indexes = argv[++i];
|
|
202
|
+
else if (arg.startsWith('--index=')) options.indexes = arg.slice(8);
|
|
203
|
+
}
|
|
204
|
+
return options;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function runDropCli(argv = [], { cwd = process.cwd(), env = process.env } = {}) {
|
|
208
|
+
const options = parseArgs(argv);
|
|
209
|
+
if (options.help) {
|
|
210
|
+
printHelp();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const dirs = resolveWatchDirs({ env, override: options.sources.length ? options.sources : null });
|
|
215
|
+
if (dirs.length === 0) {
|
|
216
|
+
console.error('No drop-zone directories found. Set CONSTRUCT_DROP_DIRS or use --source.');
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const sinceMs = parseSinceSpec(options.since);
|
|
221
|
+
const candidates = collectCandidates({
|
|
222
|
+
dirs,
|
|
223
|
+
sinceMs,
|
|
224
|
+
extensionFilter: options.type,
|
|
225
|
+
limit: 10,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (options.json) {
|
|
229
|
+
console.log(JSON.stringify({ dirs, candidates }, null, 2));
|
|
230
|
+
if (options.list || candidates.length === 0) return;
|
|
231
|
+
} else {
|
|
232
|
+
printCandidates(candidates);
|
|
233
|
+
if (candidates.length === 0) return;
|
|
234
|
+
if (options.list) return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let selection;
|
|
238
|
+
if (options.indexes) {
|
|
239
|
+
const picks = String(options.indexes).split(',').map((s) => Number(s.trim()) - 1);
|
|
240
|
+
selection = picks
|
|
241
|
+
.filter((i) => Number.isInteger(i) && i >= 0 && i < candidates.length)
|
|
242
|
+
.map((i) => candidates[i]);
|
|
243
|
+
if (selection.length === 0) {
|
|
244
|
+
console.error('No valid indexes provided.');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
} else if (options.all) {
|
|
248
|
+
selection = candidates;
|
|
249
|
+
} else {
|
|
250
|
+
selection = [candidates[0]];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!options.yes && !options.json) {
|
|
254
|
+
const names = selection.map((c) => c.name).join(', ');
|
|
255
|
+
const proceed = await confirm(`Ingest ${selection.length} file${selection.length === 1 ? '' : 's'}: ${names}?`);
|
|
256
|
+
if (!proceed) {
|
|
257
|
+
console.log('Aborted.');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const inputs = selection.map((c) => c.path);
|
|
263
|
+
const result = await ingestDocuments(inputs, {
|
|
264
|
+
cwd,
|
|
265
|
+
target: options.target || 'knowledge/internal',
|
|
266
|
+
sync: options.sync,
|
|
267
|
+
env,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (options.json) {
|
|
271
|
+
console.log(JSON.stringify({ ingested: result }, null, 2));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log('');
|
|
276
|
+
console.log(`Ingested ${inputs.length} file${inputs.length === 1 ? '' : 's'} into the current project.`);
|
|
277
|
+
if (options.sync) {
|
|
278
|
+
console.log('Hybrid index rebuilt.');
|
|
279
|
+
} else {
|
|
280
|
+
console.log('Run `construct storage sync` to rebuild the hybrid index, or re-run with --sync.');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embed/approval-queue.mjs — high-risk action approval queue.
|
|
3
|
+
*
|
|
4
|
+
* Actions that match the approval.require list in embed config are held
|
|
5
|
+
* here until approved, rejected, or timed out.
|
|
6
|
+
*
|
|
7
|
+
* Persistence: in-memory by default; optionally writes to a JSONL file
|
|
8
|
+
* at ~/.cx/approval-queue.jsonl for cross-session visibility.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
|
|
15
|
+
export class ApprovalQueue {
|
|
16
|
+
#items = new Map(); // id → ApprovalItem
|
|
17
|
+
#persistPath = null;
|
|
18
|
+
#requirePatterns = [];
|
|
19
|
+
#timeoutMs = 3_600_000;
|
|
20
|
+
#fallback = 'reject';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} opts
|
|
24
|
+
* @param {string[]} opts.require - Action type patterns that need approval
|
|
25
|
+
* @param {number} [opts.timeoutMs] - Auto-expiry after this many ms (default: 1h)
|
|
26
|
+
* @param {string} [opts.fallback] - 'reject' | 'proceed' on timeout
|
|
27
|
+
* @param {string} [opts.persistPath] - Path to JSONL persistence file
|
|
28
|
+
*/
|
|
29
|
+
constructor({ require: requirePatterns = [], timeoutMs, fallback, persistPath } = {}) {
|
|
30
|
+
this.#requirePatterns = requirePatterns;
|
|
31
|
+
if (timeoutMs != null) this.#timeoutMs = timeoutMs;
|
|
32
|
+
if (fallback) this.#fallback = fallback;
|
|
33
|
+
if (persistPath) {
|
|
34
|
+
this.#persistPath = persistPath;
|
|
35
|
+
this.#loadFromDisk();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if an action type requires approval.
|
|
41
|
+
*/
|
|
42
|
+
requiresApproval(actionType) {
|
|
43
|
+
return this.#requirePatterns.some((p) => {
|
|
44
|
+
if (p.endsWith('*')) return actionType.startsWith(p.slice(0, -1));
|
|
45
|
+
return p === actionType;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Determine approval mode for an action type.
|
|
51
|
+
* Returns 'auto' if autonomous execution is safe, 'human' if gating required.
|
|
52
|
+
*
|
|
53
|
+
* Hybrid model rules (in priority order):
|
|
54
|
+
* 1. If requiresApproval() → 'human'
|
|
55
|
+
* 2. If action.autoApprove === true → 'auto'
|
|
56
|
+
* 3. If action type matches low-risk patterns → 'auto'
|
|
57
|
+
* 4. Default → 'human' (fail-safe)
|
|
58
|
+
*/
|
|
59
|
+
approvalMode(actionType, { autoApprove = false } = {}) {
|
|
60
|
+
if (this.requiresApproval(actionType)) return 'human';
|
|
61
|
+
if (autoApprove) return 'auto';
|
|
62
|
+
const LOW_RISK = ['webhook.', 'snapshot.', 'observation.', 'search.', 'read.'];
|
|
63
|
+
if (LOW_RISK.some(prefix => actionType.startsWith(prefix))) return 'auto';
|
|
64
|
+
return 'human';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Enqueue an action for approval. Returns the item id.
|
|
69
|
+
* @param {object} action - { type, provider, payload, requestedBy?, autoApprove? }
|
|
70
|
+
*
|
|
71
|
+
* If approvalMode resolves to 'auto' the item is immediately auto-approved
|
|
72
|
+
* and persisted with status 'auto-approved'. Callers can check the returned
|
|
73
|
+
* item's status to decide whether to proceed immediately.
|
|
74
|
+
*/
|
|
75
|
+
enqueue(action) {
|
|
76
|
+
const id = `appr-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
77
|
+
const mode = this.approvalMode(action.type ?? action.action ?? 'unknown', {
|
|
78
|
+
autoApprove: action.autoApprove === true,
|
|
79
|
+
});
|
|
80
|
+
const item = {
|
|
81
|
+
id,
|
|
82
|
+
type: action.type ?? action.action ?? 'unknown',
|
|
83
|
+
provider: action.provider ?? null,
|
|
84
|
+
payload: action.payload ?? {},
|
|
85
|
+
requestedBy: action.requestedBy ?? 'construct',
|
|
86
|
+
approvalMode: mode,
|
|
87
|
+
status: mode === 'auto' ? 'auto-approved' : 'pending',
|
|
88
|
+
enqueuedAt: new Date().toISOString(),
|
|
89
|
+
resolvedAt: mode === 'auto' ? new Date().toISOString() : null,
|
|
90
|
+
expiresAt: new Date(Date.now() + this.#timeoutMs).toISOString(),
|
|
91
|
+
};
|
|
92
|
+
this.#items.set(id, item);
|
|
93
|
+
this.#persist();
|
|
94
|
+
return id;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Approve a pending item. Returns the resolved item.
|
|
99
|
+
*/
|
|
100
|
+
approve(id, { approvedBy = 'human' } = {}) {
|
|
101
|
+
return this.#resolve(id, 'approved', { approvedBy });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reject a pending item.
|
|
106
|
+
*/
|
|
107
|
+
reject(id, { reason = '' } = {}) {
|
|
108
|
+
return this.#resolve(id, 'rejected', { reason });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get an item by id.
|
|
113
|
+
*/
|
|
114
|
+
get(id) {
|
|
115
|
+
return this.#items.get(id) ?? null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* List all items, optionally filtered by status.
|
|
120
|
+
*/
|
|
121
|
+
list(status = null) {
|
|
122
|
+
const items = [...this.#items.values()];
|
|
123
|
+
return status ? items.filter((i) => i.status === status) : items;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Expire any pending items past their expiry time.
|
|
128
|
+
* Returns the list of expired items.
|
|
129
|
+
*/
|
|
130
|
+
expireStale() {
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
const expired = [];
|
|
133
|
+
for (const item of this.#items.values()) {
|
|
134
|
+
if (item.status === 'pending' && new Date(item.expiresAt).getTime() < now) {
|
|
135
|
+
this.#resolve(item.id, this.#fallback === 'proceed' ? 'approved' : 'expired');
|
|
136
|
+
expired.push(item);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return expired;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#resolve(id, status, extra = {}) {
|
|
143
|
+
const item = this.#items.get(id);
|
|
144
|
+
if (!item) throw new Error(`Approval item not found: ${id}`);
|
|
145
|
+
if (item.status !== 'pending') throw new Error(`Item ${id} is already ${item.status}`);
|
|
146
|
+
Object.assign(item, { status, resolvedAt: new Date().toISOString(), ...extra });
|
|
147
|
+
this.#persist();
|
|
148
|
+
return item;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#persist() {
|
|
152
|
+
if (!this.#persistPath) return;
|
|
153
|
+
try {
|
|
154
|
+
fs.mkdirSync(path.dirname(this.#persistPath), { recursive: true });
|
|
155
|
+
const lines = [...this.#items.values()].map((i) => JSON.stringify(i)).join('\n');
|
|
156
|
+
fs.writeFileSync(this.#persistPath, lines + '\n', 'utf8');
|
|
157
|
+
} catch (err) { process.stderr.write('[approval-queue.mjs] persist: ' + (err?.message ?? String(err)) + '\n'); }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#loadFromDisk() {
|
|
161
|
+
try {
|
|
162
|
+
if (!fs.existsSync(this.#persistPath)) return;
|
|
163
|
+
const lines = fs.readFileSync(this.#persistPath, 'utf8').split('\n').filter(Boolean);
|
|
164
|
+
for (const line of lines) {
|
|
165
|
+
try {
|
|
166
|
+
const item = JSON.parse(line);
|
|
167
|
+
this.#items.set(item.id, item);
|
|
168
|
+
} catch { /* skip malformed */ }
|
|
169
|
+
}
|
|
170
|
+
} catch (err) { process.stderr.write('[approval-queue.mjs] load-from-disk: ' + (err?.message ?? String(err)) + '\n'); }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
static defaultPersistPath() {
|
|
174
|
+
return path.join(os.homedir(), '.cx', 'approval-queue.jsonl');
|
|
175
|
+
}
|
|
176
|
+
}
|