@geraldmaron/construct 1.0.0
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 +69 -0
- package/LICENSE +21 -0
- package/README.md +344 -0
- package/agents/prompts/cx-accessibility.md +29 -0
- package/agents/prompts/cx-ai-engineer.md +31 -0
- package/agents/prompts/cx-architect.md +40 -0
- package/agents/prompts/cx-business-strategist.md +25 -0
- package/agents/prompts/cx-data-analyst.md +30 -0
- package/agents/prompts/cx-data-engineer.md +32 -0
- package/agents/prompts/cx-debugger.md +27 -0
- package/agents/prompts/cx-designer.md +33 -0
- package/agents/prompts/cx-devil-advocate.md +36 -0
- package/agents/prompts/cx-docs-keeper.md +50 -0
- package/agents/prompts/cx-engineer.md +36 -0
- package/agents/prompts/cx-evaluator.md +26 -0
- package/agents/prompts/cx-explorer.md +38 -0
- package/agents/prompts/cx-legal-compliance.md +25 -0
- package/agents/prompts/cx-operations.md +28 -0
- package/agents/prompts/cx-orchestrator.md +30 -0
- package/agents/prompts/cx-platform-engineer.md +25 -0
- package/agents/prompts/cx-product-manager.md +30 -0
- package/agents/prompts/cx-qa.md +42 -0
- package/agents/prompts/cx-rd-lead.md +25 -0
- package/agents/prompts/cx-release-manager.md +31 -0
- package/agents/prompts/cx-researcher.md +31 -0
- package/agents/prompts/cx-reviewer.md +36 -0
- package/agents/prompts/cx-security.md +37 -0
- package/agents/prompts/cx-sre.md +26 -0
- package/agents/prompts/cx-test-automation.md +35 -0
- package/agents/prompts/cx-trace-reviewer.md +76 -0
- package/agents/prompts/cx-ux-researcher.md +30 -0
- package/agents/registry.json +879 -0
- package/agents/teams.json +94 -0
- package/bin/construct +613 -0
- package/commands/build/feature.md +21 -0
- package/commands/build/fix.md +20 -0
- package/commands/design/access.md +19 -0
- package/commands/design/flow.md +16 -0
- package/commands/design/ui.md +18 -0
- package/commands/measure/experiment.md +18 -0
- package/commands/measure/metrics.md +18 -0
- package/commands/measure/results.md +18 -0
- package/commands/plan/api.md +19 -0
- package/commands/plan/challenge.md +18 -0
- package/commands/plan/decide.md +20 -0
- package/commands/plan/feature.md +49 -0
- package/commands/plan/requirements.md +24 -0
- package/commands/remember/context.md +26 -0
- package/commands/remember/handoff.md +19 -0
- package/commands/remember/runbook.md +20 -0
- package/commands/review/code.md +21 -0
- package/commands/review/quality.md +19 -0
- package/commands/review/security.md +16 -0
- package/commands/ship/ready.md +19 -0
- package/commands/ship/release.md +19 -0
- package/commands/ship/status.md +18 -0
- package/commands/understand/docs.md +22 -0
- package/commands/understand/research.md +22 -0
- package/commands/understand/this.md +23 -0
- package/commands/understand/why.md +18 -0
- package/commands/work/clean.md +32 -0
- package/commands/work/drive.md +69 -0
- package/commands/work/optimize-prompts.md +46 -0
- package/commands/work/parallel-review.md +40 -0
- package/db/migrations/001_init.sql +38 -0
- package/langfuse/docker-compose.yml +82 -0
- package/lib/audit-skills.mjs +118 -0
- package/lib/auto-docs.mjs +293 -0
- package/lib/cli-commands.mjs +409 -0
- package/lib/codex-config.mjs +114 -0
- package/lib/comment-lint.mjs +191 -0
- package/lib/completions.mjs +170 -0
- package/lib/context-state.mjs +99 -0
- package/lib/cost.mjs +213 -0
- package/lib/distill.mjs +403 -0
- package/lib/efficiency.mjs +139 -0
- package/lib/env-config.mjs +53 -0
- package/lib/eval-harness.mjs +59 -0
- package/lib/features.mjs +209 -0
- package/lib/headhunt.mjs +597 -0
- package/lib/hooks/adaptive-lint.mjs +112 -0
- package/lib/hooks/agent-tracker.mjs +50 -0
- package/lib/hooks/bootstrap-guard.mjs +90 -0
- package/lib/hooks/comment-lint.mjs +26 -0
- package/lib/hooks/config-protection.mjs +52 -0
- package/lib/hooks/console-warn.mjs +43 -0
- package/lib/hooks/context-window-recovery.mjs +90 -0
- package/lib/hooks/continuation-enforcer.mjs +72 -0
- package/lib/hooks/dep-audit.mjs +155 -0
- package/lib/hooks/drive-guard.mjs +89 -0
- package/lib/hooks/edit-accumulator.mjs +36 -0
- package/lib/hooks/edit-error-recovery.mjs +46 -0
- package/lib/hooks/edit-guard.mjs +109 -0
- package/lib/hooks/env-check.mjs +80 -0
- package/lib/hooks/guard-bash.mjs +83 -0
- package/lib/hooks/mcp-audit.mjs +57 -0
- package/lib/hooks/mcp-health-check.mjs +51 -0
- package/lib/hooks/mcp-task-scope.mjs +47 -0
- package/lib/hooks/model-fallback.mjs +105 -0
- package/lib/hooks/pre-compact.mjs +212 -0
- package/lib/hooks/pre-push-gate.mjs +129 -0
- package/lib/hooks/read-tracker.mjs +129 -0
- package/lib/hooks/registry-sync.mjs +23 -0
- package/lib/hooks/scan-secrets.mjs +76 -0
- package/lib/hooks/session-start.mjs +95 -0
- package/lib/hooks/stop-notify.mjs +191 -0
- package/lib/hooks/stop-typecheck.mjs +83 -0
- package/lib/hooks/task-completed-guard.mjs +43 -0
- package/lib/hooks/teammate-idle-guard.mjs +54 -0
- package/lib/hooks/workflow-guard.mjs +62 -0
- package/lib/host-capabilities.mjs +123 -0
- package/lib/init-docs.mjs +382 -0
- package/lib/mcp/server.mjs +1123 -0
- package/lib/mcp-catalog.json +243 -0
- package/lib/mcp-manager.mjs +433 -0
- package/lib/mcp-platform-config.mjs +104 -0
- package/lib/model-router.mjs +810 -0
- package/lib/opencode-config.mjs +44 -0
- package/lib/opencode-runtime-plugin.mjs +909 -0
- package/lib/opencode-telemetry.mjs +433 -0
- package/lib/orchestration-policy.mjs +258 -0
- package/lib/prompt-composer.mjs +196 -0
- package/lib/prompt-metadata.mjs +68 -0
- package/lib/review.mjs +429 -0
- package/lib/role-preload.mjs +52 -0
- package/lib/schemas/decision.json +50 -0
- package/lib/schemas/implementation.json +51 -0
- package/lib/schemas/review-report.json +32 -0
- package/lib/schemas/test-report.json +43 -0
- package/lib/server/index.mjs +334 -0
- package/lib/server/static/app.js +841 -0
- package/lib/server/static/index.html +820 -0
- package/lib/service-manager.mjs +225 -0
- package/lib/setup.mjs +319 -0
- package/lib/status.mjs +707 -0
- package/lib/storage/backend.mjs +44 -0
- package/lib/storage/embeddings.mjs +70 -0
- package/lib/storage/hybrid-query.mjs +184 -0
- package/lib/storage/sql-store.mjs +55 -0
- package/lib/storage/state-source.mjs +101 -0
- package/lib/storage/sync.mjs +164 -0
- package/lib/storage/vector-store.mjs +59 -0
- package/lib/telemetry/backends/langfuse.mjs +58 -0
- package/lib/telemetry/backends/noop.mjs +16 -0
- package/lib/telemetry/langfuse-ingest.mjs +108 -0
- package/lib/telemetry/langfuse-model-sync.mjs +270 -0
- package/lib/telemetry/team-rollup.mjs +143 -0
- package/lib/toolkit-env.mjs +30 -0
- package/lib/validator.mjs +181 -0
- package/lib/workflow-state.mjs +794 -0
- package/package.json +53 -0
- package/personas/construct.md +94 -0
- package/platforms/claude/CLAUDE.md +30 -0
- package/platforms/claude/settings.template.json +472 -0
- package/platforms/opencode/config.template.json +65 -0
- package/platforms/opencode/plugins/construct-fallback.js +5 -0
- package/platforms/opencode/sync-config.mjs +69 -0
- package/rules/common/agents.md +55 -0
- package/rules/common/code-review.md +129 -0
- package/rules/common/coding-style.md +95 -0
- package/rules/common/comments.md +139 -0
- package/rules/common/cx-agent-routing.md +61 -0
- package/rules/common/cx-skill-routing.md +11 -0
- package/rules/common/development-workflow.md +49 -0
- package/rules/common/git-workflow.md +29 -0
- package/rules/common/hooks.md +35 -0
- package/rules/common/patterns.md +36 -0
- package/rules/common/performance.md +71 -0
- package/rules/common/security.md +34 -0
- package/rules/common/testing.md +62 -0
- package/rules/golang/coding-style.md +37 -0
- package/rules/golang/hooks.md +22 -0
- package/rules/golang/patterns.md +50 -0
- package/rules/golang/security.md +39 -0
- package/rules/golang/testing.md +36 -0
- package/rules/python/coding-style.md +47 -0
- package/rules/python/hooks.md +24 -0
- package/rules/python/patterns.md +44 -0
- package/rules/python/security.md +35 -0
- package/rules/python/testing.md +43 -0
- package/rules/swift/coding-style.md +52 -0
- package/rules/swift/hooks.md +25 -0
- package/rules/swift/patterns.md +71 -0
- package/rules/swift/security.md +38 -0
- package/rules/swift/testing.md +50 -0
- package/rules/typescript/coding-style.md +204 -0
- package/rules/typescript/hooks.md +27 -0
- package/rules/typescript/patterns.md +57 -0
- package/rules/typescript/security.md +33 -0
- package/rules/typescript/testing.md +23 -0
- package/rules/web/coding-style.md +101 -0
- package/rules/web/design-quality.md +68 -0
- package/rules/web/hooks.md +125 -0
- package/rules/web/patterns.md +84 -0
- package/rules/web/performance.md +69 -0
- package/rules/web/security.md +62 -0
- package/rules/web/testing.md +60 -0
- package/rules/zh/README.md +113 -0
- package/rules/zh/agents.md +55 -0
- package/rules/zh/code-review.md +129 -0
- package/rules/zh/coding-style.md +53 -0
- package/rules/zh/development-workflow.md +49 -0
- package/rules/zh/git-workflow.md +29 -0
- package/rules/zh/hooks.md +35 -0
- package/rules/zh/patterns.md +36 -0
- package/rules/zh/performance.md +60 -0
- package/rules/zh/security.md +34 -0
- package/rules/zh/testing.md +34 -0
- package/skills/ai/agent-dev.md +102 -0
- package/skills/ai/llm-security.md +105 -0
- package/skills/ai/ml-ops.md +169 -0
- package/skills/ai/orchestration-workflow.md +87 -0
- package/skills/ai/prompt-and-eval.md +114 -0
- package/skills/ai/prompt-optimizer.md +114 -0
- package/skills/ai/rag-system.md +104 -0
- package/skills/architecture/api-design.md +100 -0
- package/skills/architecture/caching.md +101 -0
- package/skills/architecture/cloud-native.md +97 -0
- package/skills/architecture/message-queue.md +100 -0
- package/skills/architecture/security-arch.md +105 -0
- package/skills/development/cpp.md +127 -0
- package/skills/development/go.md +129 -0
- package/skills/development/java.md +134 -0
- package/skills/development/kotlin.md +140 -0
- package/skills/development/mobile-crossplatform.md +144 -0
- package/skills/development/python.md +109 -0
- package/skills/development/rust.md +127 -0
- package/skills/development/shell.md +127 -0
- package/skills/development/swift.md +129 -0
- package/skills/development/typescript.md +129 -0
- package/skills/devops/ci-cd.md +108 -0
- package/skills/devops/containerization.md +106 -0
- package/skills/devops/cost-optimization.md +105 -0
- package/skills/devops/data-engineering.md +181 -0
- package/skills/devops/database.md +95 -0
- package/skills/devops/dependency-management.md +91 -0
- package/skills/devops/devsecops.md +96 -0
- package/skills/devops/git-workflow.md +100 -0
- package/skills/devops/incident-response.md +167 -0
- package/skills/devops/monorepo.md +87 -0
- package/skills/devops/observability.md +103 -0
- package/skills/devops/performance.md +104 -0
- package/skills/devops/testing.md +104 -0
- package/skills/docs/adr-workflow.md +32 -0
- package/skills/docs/backlog-proposal-workflow.md +19 -0
- package/skills/docs/customer-profile-workflow.md +22 -0
- package/skills/docs/evidence-ingest-workflow.md +23 -0
- package/skills/docs/init-docs.md +160 -0
- package/skills/docs/init-project.md +50 -0
- package/skills/docs/prd-workflow.md +57 -0
- package/skills/docs/prfaq-workflow.md +25 -0
- package/skills/docs/product-intelligence-review.md +22 -0
- package/skills/docs/product-intelligence-workflow.md +62 -0
- package/skills/docs/product-signal-workflow.md +27 -0
- package/skills/docs/research-workflow.md +28 -0
- package/skills/docs/runbook-workflow.md +24 -0
- package/skills/exploration/repo-map.md +297 -0
- package/skills/frameworks/django.md +159 -0
- package/skills/frameworks/nextjs.md +148 -0
- package/skills/frameworks/react.md +132 -0
- package/skills/frameworks/spring-boot.md +165 -0
- package/skills/frontend-design/accessibility.md +153 -0
- package/skills/frontend-design/component-patterns.md +111 -0
- package/skills/frontend-design/engineering.md +122 -0
- package/skills/frontend-design/state-management.md +118 -0
- package/skills/frontend-design/ui-aesthetics.md +102 -0
- package/skills/frontend-design/ux-principles.md +126 -0
- package/skills/quality-gates/review-work.md +90 -0
- package/skills/quality-gates/verify-change.md +94 -0
- package/skills/quality-gates/verify-module.md +96 -0
- package/skills/quality-gates/verify-quality.md +93 -0
- package/skills/quality-gates/verify-security.md +95 -0
- package/skills/roles/architect.ai-systems.md +37 -0
- package/skills/roles/architect.data.md +37 -0
- package/skills/roles/architect.enterprise.md +37 -0
- package/skills/roles/architect.integration.md +37 -0
- package/skills/roles/architect.md +68 -0
- package/skills/roles/architect.platform.md +37 -0
- package/skills/roles/data-analyst.experiment.md +37 -0
- package/skills/roles/data-analyst.md +68 -0
- package/skills/roles/data-analyst.product-intelligence.md +37 -0
- package/skills/roles/data-analyst.product.md +37 -0
- package/skills/roles/data-analyst.telemetry.md +37 -0
- package/skills/roles/data-engineer.pipeline.md +37 -0
- package/skills/roles/data-engineer.vector-retrieval.md +37 -0
- package/skills/roles/data-engineer.warehouse.md +37 -0
- package/skills/roles/debugger.md +68 -0
- package/skills/roles/designer.accessibility.md +43 -0
- package/skills/roles/designer.md +68 -0
- package/skills/roles/engineer.ai.md +49 -0
- package/skills/roles/engineer.data.md +49 -0
- package/skills/roles/engineer.md +85 -0
- package/skills/roles/engineer.platform.md +49 -0
- package/skills/roles/operator.docs.md +43 -0
- package/skills/roles/operator.md +68 -0
- package/skills/roles/operator.release.md +43 -0
- package/skills/roles/operator.sre.md +43 -0
- package/skills/roles/orchestrator.md +66 -0
- package/skills/roles/product-manager.ai-product.md +37 -0
- package/skills/roles/product-manager.business-strategy.md +43 -0
- package/skills/roles/product-manager.enterprise.md +37 -0
- package/skills/roles/product-manager.growth.md +37 -0
- package/skills/roles/product-manager.md +67 -0
- package/skills/roles/product-manager.platform.md +37 -0
- package/skills/roles/product-manager.product.md +37 -0
- package/skills/roles/qa.ai-eval.md +37 -0
- package/skills/roles/qa.api-contract.md +37 -0
- package/skills/roles/qa.data-pipeline.md +37 -0
- package/skills/roles/qa.md +68 -0
- package/skills/roles/qa.test-automation.md +49 -0
- package/skills/roles/qa.web-ui.md +37 -0
- package/skills/roles/researcher.explorer.md +43 -0
- package/skills/roles/researcher.md +68 -0
- package/skills/roles/researcher.ux.md +43 -0
- package/skills/roles/reviewer.devil-advocate.md +43 -0
- package/skills/roles/reviewer.evaluator.md +43 -0
- package/skills/roles/reviewer.md +68 -0
- package/skills/roles/reviewer.trace.md +43 -0
- package/skills/roles/security.ai.md +37 -0
- package/skills/roles/security.appsec.md +37 -0
- package/skills/roles/security.cloud.md +37 -0
- package/skills/roles/security.legal-compliance.md +49 -0
- package/skills/roles/security.md +68 -0
- package/skills/roles/security.privacy.md +37 -0
- package/skills/roles/security.supply-chain.md +37 -0
- package/skills/routing.md +139 -0
- package/skills/security/blue-team.md +83 -0
- package/skills/security/code-audit.md +94 -0
- package/skills/security/pentest.md +84 -0
- package/skills/security/red-team.md +81 -0
- package/skills/security/threat-intel.md +89 -0
- package/skills/security/vuln-research.md +87 -0
- package/skills/utility/clean-code.md +165 -0
- package/sync-agents.mjs +899 -0
- package/templates/docs/adr.md +27 -0
- package/templates/docs/backlog-proposal.md +26 -0
- package/templates/docs/customer-profile.md +34 -0
- package/templates/docs/evidence-brief.md +37 -0
- package/templates/docs/incident-report.md +46 -0
- package/templates/docs/memo.md +27 -0
- package/templates/docs/meta-prd.md +56 -0
- package/templates/docs/one-pager.md +24 -0
- package/templates/docs/prd-business.md +61 -0
- package/templates/docs/prd-platform.md +62 -0
- package/templates/docs/prd.md +47 -0
- package/templates/docs/prfaq.md +31 -0
- package/templates/docs/product-intelligence-report.md +31 -0
- package/templates/docs/research-brief.md +29 -0
- package/templates/docs/rfc-platform.md +60 -0
- package/templates/docs/rfc.md +46 -0
- package/templates/docs/runbook.md +33 -0
- package/templates/docs/signal-brief.md +25 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/comment-lint.mjs — enforce the Construct comment policy from rules/common/comments.md.
|
|
3
|
+
*
|
|
4
|
+
* lintFile(path) checks a single file. lintRepo({ rootDir, fix }) checks all
|
|
5
|
+
* scoped paths and optionally inserts stub headers for files missing one.
|
|
6
|
+
* Used by `construct lint:comments`, the comment-lint PostToolUse hook, and CI.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
// --- scoped paths that require a file header ---
|
|
13
|
+
|
|
14
|
+
const JS_HEADER_GLOBS = [
|
|
15
|
+
/^bin\//,
|
|
16
|
+
/^lib\/(?!server\/)([\w-]+\.mjs)$/,
|
|
17
|
+
/^lib\/hooks\//,
|
|
18
|
+
/^lib\/server\//,
|
|
19
|
+
/^lib\/mcp\//,
|
|
20
|
+
/^lib\/metrics\//,
|
|
21
|
+
/^sync-agents\.mjs$/,
|
|
22
|
+
/^tests\//,
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const MD_HEADER_GLOBS = [
|
|
26
|
+
/^personas\//,
|
|
27
|
+
/^skills\//,
|
|
28
|
+
/^rules\//,
|
|
29
|
+
/^commands\//,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function relPath(rootDir, absPath) {
|
|
33
|
+
return path.relative(rootDir, absPath).replace(/\\/g, '/');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function requiresHeader(rel) {
|
|
37
|
+
const ext = path.extname(rel);
|
|
38
|
+
const jsMatch = JS_HEADER_GLOBS.some(r => r.test(rel));
|
|
39
|
+
const mdMatch = MD_HEADER_GLOBS.some(r => r.test(rel));
|
|
40
|
+
const type = (jsMatch && ext !== '.html') ? 'js' : (jsMatch || mdMatch) ? 'md' : null;
|
|
41
|
+
return { required: jsMatch || mdMatch, type };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- header detection ---
|
|
45
|
+
|
|
46
|
+
const JS_HEADER_RE = /^(?:#![^\n]*\n)?(?:\/\/[^\n]*\n)*\/\*\*[\s\S]*?\*\//;
|
|
47
|
+
const MD_HEADER_RE = /^<!--[\s\S]*?-->/;
|
|
48
|
+
const SH_HEADER_RE = /^#!.*\n(?:#[^\n]*\n)*/;
|
|
49
|
+
|
|
50
|
+
function hasHeader(content, type) {
|
|
51
|
+
if (type === 'js') return JS_HEADER_RE.test(content.trimStart());
|
|
52
|
+
if (type === 'md') return MD_HEADER_RE.test(content.trimStart());
|
|
53
|
+
return SH_HEADER_RE.test(content.trimStart());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --- banned comment patterns ---
|
|
57
|
+
|
|
58
|
+
const BANNED = [
|
|
59
|
+
{ pattern: /\/\/.*\b(?:added for|added recently|recently added|just added|new:)\b/i, label: 'point-in-time: "added for / recently / just / new:"' },
|
|
60
|
+
{ pattern: /\/\/.*\b(?:used by|called from|only consumer)\b/i, label: 'caller reference: "used by / called from"' },
|
|
61
|
+
{ pattern: /\/\/.*\bpreviously\b.*\bwe used to\b/i, label: 'point-in-time: "previously / we used to"' },
|
|
62
|
+
{ pattern: /\/\/.*(?:#\d{3,}|GH-\d+|JIRA-\d+|closes #|fixes #|ticket )/i, label: 'issue/PR reference in source comment (put in commit message instead)' },
|
|
63
|
+
{ pattern: /<!--.*\b(?:added for|added recently|just added|new:)\b.*-->/i, label: 'point-in-time in markdown comment' },
|
|
64
|
+
{ pattern: /<!--.*\b(?:used by|called from|only consumer)\b.*-->/i, label: 'caller reference in markdown comment' },
|
|
65
|
+
{ pattern: /\/\/\s*TODO(?:\((\w+)\))?:?(?!\s*\(\w+\):)/i, label: 'TODO without owner — use: TODO(owner): what and why' },
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
function checkBanned(content, filePath) {
|
|
69
|
+
if (filePath && (filePath.endsWith('comment-lint.mjs') || filePath.endsWith('comments.md'))) return [];
|
|
70
|
+
const warnings = [];
|
|
71
|
+
const lines = content.split('\n');
|
|
72
|
+
for (let i = 0; i < lines.length; i++) {
|
|
73
|
+
for (const { pattern, label } of BANNED) {
|
|
74
|
+
if (pattern.test(lines[i])) {
|
|
75
|
+
warnings.push({ line: i + 1, label });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return warnings;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- stub header generation ---
|
|
83
|
+
|
|
84
|
+
function stubJsHeader(rel) {
|
|
85
|
+
return `/**\n * ${rel} — <one-line purpose>\n *\n * <2–6 line summary: what it does, who calls it, key side effects.>\n */\n`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function stubMdHeader(rel) {
|
|
89
|
+
return `<!--\n${rel} — <one-line purpose>\n\n<2–6 line summary.>\n-->\n`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function extractShebang(content) {
|
|
93
|
+
if (content.startsWith('#!')) {
|
|
94
|
+
const nl = content.indexOf('\n');
|
|
95
|
+
if (nl !== -1) return { shebang: content.slice(0, nl + 1), rest: content.slice(nl + 1) };
|
|
96
|
+
}
|
|
97
|
+
return { shebang: '', rest: content };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --- single file lint ---
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check one file against the comment policy.
|
|
104
|
+
* Returns { path, errors, warnings }.
|
|
105
|
+
*/
|
|
106
|
+
export function lintFile(filePath, { rootDir = process.cwd(), fix = false } = {}) {
|
|
107
|
+
const rel = relPath(rootDir, filePath);
|
|
108
|
+
const { required, type } = requiresHeader(rel);
|
|
109
|
+
|
|
110
|
+
let content;
|
|
111
|
+
try { content = fs.readFileSync(filePath, 'utf8'); } catch { return { path: rel, errors: [], warnings: [] }; }
|
|
112
|
+
|
|
113
|
+
const errors = [];
|
|
114
|
+
const warnings = [];
|
|
115
|
+
|
|
116
|
+
if (required && !hasHeader(content, type)) {
|
|
117
|
+
errors.push({ line: 1, label: `missing file header block (see rules/common/comments.md §1)` });
|
|
118
|
+
if (fix) {
|
|
119
|
+
const stub = type === 'md' ? stubMdHeader(rel) : stubJsHeader(rel);
|
|
120
|
+
const { shebang, rest } = extractShebang(content);
|
|
121
|
+
fs.writeFileSync(filePath, shebang + stub + rest);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const banned = checkBanned(content, filePath);
|
|
126
|
+
for (const w of banned) warnings.push(w);
|
|
127
|
+
|
|
128
|
+
return { path: rel, errors, warnings };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --- repo-wide lint ---
|
|
132
|
+
|
|
133
|
+
function walkDir(dir, results = []) {
|
|
134
|
+
let entries;
|
|
135
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return results; }
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
const full = path.join(dir, entry.name);
|
|
138
|
+
if (entry.isDirectory()) {
|
|
139
|
+
if (['.git', 'node_modules', 'site', '.cx', '.construct', '.claude'].includes(entry.name)) continue;
|
|
140
|
+
walkDir(full, results);
|
|
141
|
+
} else if (entry.isFile()) {
|
|
142
|
+
results.push(full);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return results;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Lint all scoped files in the repo. Returns an array of lint results.
|
|
150
|
+
* With fix:true, inserts stub headers for missing-header errors.
|
|
151
|
+
*/
|
|
152
|
+
export function lintRepo({ rootDir = process.cwd(), fix = false } = {}) {
|
|
153
|
+
const files = walkDir(rootDir);
|
|
154
|
+
const results = [];
|
|
155
|
+
for (const f of files) {
|
|
156
|
+
const rel = relPath(rootDir, f);
|
|
157
|
+
const { required } = requiresHeader(rel);
|
|
158
|
+
const ext = path.extname(f);
|
|
159
|
+
if (!required && !['.mjs', '.md', '.sh'].includes(ext)) continue;
|
|
160
|
+
const result = lintFile(f, { rootDir, fix });
|
|
161
|
+
if (result.errors.length || result.warnings.length) results.push(result);
|
|
162
|
+
}
|
|
163
|
+
return results;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Format lint results for terminal output. Returns { output, exitCode }.
|
|
168
|
+
*/
|
|
169
|
+
export function formatResults(results) {
|
|
170
|
+
if (!results.length) return { output: ' ✓ No comment policy violations found.\n', exitCode: 0 };
|
|
171
|
+
|
|
172
|
+
const lines = [];
|
|
173
|
+
let errorCount = 0;
|
|
174
|
+
let warnCount = 0;
|
|
175
|
+
|
|
176
|
+
for (const { path: p, errors, warnings } of results) {
|
|
177
|
+
for (const { line, label } of errors) {
|
|
178
|
+
lines.push(` error ${p}:${line} ${label}`);
|
|
179
|
+
errorCount++;
|
|
180
|
+
}
|
|
181
|
+
for (const { line, label } of warnings) {
|
|
182
|
+
lines.push(` warn ${p}:${line} ${label}`);
|
|
183
|
+
warnCount++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const summary = `\n ${errorCount} error(s), ${warnCount} warning(s)`;
|
|
188
|
+
lines.push(summary);
|
|
189
|
+
|
|
190
|
+
return { output: lines.join('\n') + '\n', exitCode: errorCount > 0 ? 1 : 0 };
|
|
191
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* completions.mjs — generates bash and zsh completion scripts for construct.
|
|
3
|
+
*
|
|
4
|
+
* Called by sync-agents.mjs after each sync.
|
|
5
|
+
* Outputs to a platform-appropriate per-user completions directory.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { CLI_COMMANDS } from './cli-commands.mjs';
|
|
9
|
+
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
|
|
13
|
+
const COMPLETIONS_DIR = process.platform === 'win32'
|
|
14
|
+
? join(process.env.APPDATA ?? join(homedir(), 'AppData', 'Roaming'), 'construct', 'completions')
|
|
15
|
+
: join(homedir(), '.local', 'share', 'construct', 'completions');
|
|
16
|
+
|
|
17
|
+
function buildBash() {
|
|
18
|
+
const names = CLI_COMMANDS.map(c => c.name).join(' ');
|
|
19
|
+
|
|
20
|
+
const subcommandCases = CLI_COMMANDS
|
|
21
|
+
.filter(c => c.subcommands?.length)
|
|
22
|
+
.map(c => {
|
|
23
|
+
const subs = c.subcommands.map(s => s.name).join(' ');
|
|
24
|
+
return ` ${c.name}) COMPREPLY=($(compgen -W "${subs}" -- "$cur")) ;;`;
|
|
25
|
+
})
|
|
26
|
+
.join('\n');
|
|
27
|
+
|
|
28
|
+
const dirCommands = CLI_COMMANDS
|
|
29
|
+
.filter(c => c.usage?.includes('<dir>'))
|
|
30
|
+
.map(c => c.name)
|
|
31
|
+
.join('|');
|
|
32
|
+
|
|
33
|
+
const optionCases = CLI_COMMANDS
|
|
34
|
+
.filter(c => c.options?.length)
|
|
35
|
+
.map(c => {
|
|
36
|
+
const flags = c.options.map(o => o.flag.split('=')[0]).join(' ');
|
|
37
|
+
return ` ${c.name}) COMPREPLY=($(compgen -W "${flags}" -- "$cur")) ;;`;
|
|
38
|
+
})
|
|
39
|
+
.join('\n');
|
|
40
|
+
|
|
41
|
+
return `# bash completion for construct
|
|
42
|
+
# Source this file in ~/.bashrc:
|
|
43
|
+
# source ~/.local/share/construct/completions/construct.bash
|
|
44
|
+
|
|
45
|
+
_construct_completions() {
|
|
46
|
+
local cur prev words cword
|
|
47
|
+
_init_completion 2>/dev/null || {
|
|
48
|
+
COMPREPLY=()
|
|
49
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
50
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
51
|
+
words=("\${COMP_WORDS[@]}")
|
|
52
|
+
cword=$COMP_CWORD
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
local commands="${names}"
|
|
56
|
+
|
|
57
|
+
# First argument: complete command names
|
|
58
|
+
if [[ $cword -eq 1 ]]; then
|
|
59
|
+
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
|
|
60
|
+
return 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
local cmd="\${words[1]}"
|
|
64
|
+
|
|
65
|
+
# Subcommands
|
|
66
|
+
if [[ $cword -eq 2 ]]; then
|
|
67
|
+
case "$cmd" in
|
|
68
|
+
${subcommandCases}
|
|
69
|
+
${dirCommands ? `${dirCommands}) _filedir -d ;;` : ''}
|
|
70
|
+
esac
|
|
71
|
+
return 0
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# Options (flags starting with -)
|
|
75
|
+
if [[ "$cur" == -* ]]; then
|
|
76
|
+
case "$cmd" in
|
|
77
|
+
${optionCases}
|
|
78
|
+
esac
|
|
79
|
+
return 0
|
|
80
|
+
fi
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
complete -F _construct_completions construct
|
|
84
|
+
`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildZsh() {
|
|
88
|
+
const commandDescriptions = CLI_COMMANDS
|
|
89
|
+
.map(c => ` '${c.name}:${c.emoji} ${c.description.replace(/'/g, "\\'")}' \\`)
|
|
90
|
+
.join('\n');
|
|
91
|
+
|
|
92
|
+
const subcommandFunctions = CLI_COMMANDS
|
|
93
|
+
.filter(c => c.subcommands?.length)
|
|
94
|
+
.map(c => {
|
|
95
|
+
const subs = c.subcommands
|
|
96
|
+
.map(s => ` '${s.name}:${s.desc.replace(/'/g, "\\'")}'`)
|
|
97
|
+
.join('\n');
|
|
98
|
+
return `_construct_${c.name.replace(/-/g, '_')}() {
|
|
99
|
+
local subcmds
|
|
100
|
+
subcmds=(
|
|
101
|
+
${subs}
|
|
102
|
+
)
|
|
103
|
+
_describe -t subcommands '${c.name} subcommand' subcmds
|
|
104
|
+
}`;
|
|
105
|
+
})
|
|
106
|
+
.join('\n\n');
|
|
107
|
+
|
|
108
|
+
const subcommandDispatch = CLI_COMMANDS
|
|
109
|
+
.filter(c => c.subcommands?.length)
|
|
110
|
+
.map(c => ` ${c.name}) _construct_${c.name.replace(/-/g, '_')} ;;`)
|
|
111
|
+
.join('\n');
|
|
112
|
+
|
|
113
|
+
const dirCommands = CLI_COMMANDS
|
|
114
|
+
.filter(c => c.usage?.includes('<dir>'))
|
|
115
|
+
.map(c => c.name)
|
|
116
|
+
.join('|');
|
|
117
|
+
|
|
118
|
+
return `#compdef construct
|
|
119
|
+
# zsh completion for construct
|
|
120
|
+
# Add to fpath: fpath=(~/.local/share/construct/completions $fpath)
|
|
121
|
+
# Then: autoload -Uz compinit && compinit
|
|
122
|
+
|
|
123
|
+
${subcommandFunctions}
|
|
124
|
+
|
|
125
|
+
_construct_commands() {
|
|
126
|
+
local commands
|
|
127
|
+
commands=(
|
|
128
|
+
${commandDescriptions}
|
|
129
|
+
)
|
|
130
|
+
_describe -t commands 'construct command' commands
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
_construct() {
|
|
134
|
+
local context state state_descr line
|
|
135
|
+
typeset -A opt_args
|
|
136
|
+
|
|
137
|
+
_arguments -C \\
|
|
138
|
+
'1: :_construct_commands' \\
|
|
139
|
+
'*:: :->args'
|
|
140
|
+
|
|
141
|
+
case $state in
|
|
142
|
+
args)
|
|
143
|
+
case $words[1] in
|
|
144
|
+
${subcommandDispatch}
|
|
145
|
+
${dirCommands ? `${dirCommands}) _files -/ ;;` : ''}
|
|
146
|
+
*) ;;
|
|
147
|
+
esac
|
|
148
|
+
;;
|
|
149
|
+
esac
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_construct
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function generateCompletions() {
|
|
157
|
+
try {
|
|
158
|
+
mkdirSync(COMPLETIONS_DIR, { recursive: true });
|
|
159
|
+
|
|
160
|
+
writeFileSync(join(COMPLETIONS_DIR, 'construct.bash'), buildBash());
|
|
161
|
+
writeFileSync(join(COMPLETIONS_DIR, '_construct'), buildZsh());
|
|
162
|
+
|
|
163
|
+
return COMPLETIONS_DIR;
|
|
164
|
+
} catch (err) {
|
|
165
|
+
process.stderr.write(`[completions] failed to write: ${err.message}\n`);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export { COMPLETIONS_DIR };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/context-state.mjs — compact project/session context persistence.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
export function contextJsonPath(rootDir) {
|
|
9
|
+
return join(rootDir, '.cx', 'context.json');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function contextMarkdownPath(rootDir) {
|
|
13
|
+
return join(rootDir, '.cx', 'context.md');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function readContextState(rootDir) {
|
|
17
|
+
const inspection = inspectContextState(rootDir);
|
|
18
|
+
return inspection.state;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function inspectContextState(rootDir) {
|
|
22
|
+
const jsonPath = contextJsonPath(rootDir);
|
|
23
|
+
const mdPath = contextMarkdownPath(rootDir);
|
|
24
|
+
const hasJsonFile = existsSync(jsonPath);
|
|
25
|
+
const hasMarkdownFile = existsSync(mdPath);
|
|
26
|
+
const hasFile = hasJsonFile || hasMarkdownFile;
|
|
27
|
+
|
|
28
|
+
let state = null;
|
|
29
|
+
let source = 'missing';
|
|
30
|
+
|
|
31
|
+
if (hasJsonFile) {
|
|
32
|
+
try {
|
|
33
|
+
state = JSON.parse(readFileSync(jsonPath, 'utf8'));
|
|
34
|
+
source = 'json';
|
|
35
|
+
} catch {
|
|
36
|
+
source = hasMarkdownFile ? 'markdown' : 'invalid';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!state && hasMarkdownFile) {
|
|
41
|
+
try {
|
|
42
|
+
state = {
|
|
43
|
+
format: 'markdown',
|
|
44
|
+
markdown: readFileSync(mdPath, 'utf8'),
|
|
45
|
+
};
|
|
46
|
+
if (source !== 'json') source = 'markdown';
|
|
47
|
+
} catch {
|
|
48
|
+
if (!hasJsonFile) source = 'invalid';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
hasFile,
|
|
54
|
+
source,
|
|
55
|
+
savedAt: state?.savedAt || null,
|
|
56
|
+
summary: state?.contextSummary || contextSummaryLine(state) || null,
|
|
57
|
+
state,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function writeContextState(rootDir, state, { markdown = null } = {}) {
|
|
62
|
+
const cxDir = join(rootDir, '.cx');
|
|
63
|
+
mkdirSync(cxDir, { recursive: true });
|
|
64
|
+
|
|
65
|
+
const payload = {
|
|
66
|
+
format: 'json',
|
|
67
|
+
savedAt: new Date().toISOString(),
|
|
68
|
+
...state,
|
|
69
|
+
};
|
|
70
|
+
writeFileSync(contextJsonPath(rootDir), `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
71
|
+
|
|
72
|
+
if (markdown !== null) {
|
|
73
|
+
writeFileSync(contextMarkdownPath(rootDir), markdown.endsWith('\n') ? markdown : `${markdown}\n`, 'utf8');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return payload;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function contextSummaryLine(state) {
|
|
80
|
+
if (!state || typeof state !== 'object') return '';
|
|
81
|
+
if (state.compact) return String(state.compact);
|
|
82
|
+
if (state.recoveryContext) return String(state.recoveryContext).slice(0, 240);
|
|
83
|
+
if (state.markdown) return String(state.markdown).slice(0, 240);
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildContextDigest(state, { maxItems = 3 } = {}) {
|
|
88
|
+
if (!state || typeof state !== 'object') return null;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
summary: state.contextSummary || contextSummaryLine(state) || null,
|
|
92
|
+
activeWork: Array.isArray(state.activeWork) ? state.activeWork.slice(0, maxItems) : [],
|
|
93
|
+
recentDecisions: Array.isArray(state.recentDecisions) ? state.recentDecisions.slice(0, maxItems) : [],
|
|
94
|
+
architectureNotes: Array.isArray(state.architectureNotes) ? state.architectureNotes.slice(0, maxItems) : [],
|
|
95
|
+
openQuestions: Array.isArray(state.openQuestions) ? state.openQuestions.slice(0, maxItems) : [],
|
|
96
|
+
source: state.source || null,
|
|
97
|
+
savedAt: state.savedAt || null,
|
|
98
|
+
};
|
|
99
|
+
}
|
package/lib/cost.mjs
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/cost.mjs — <one-line purpose>
|
|
3
|
+
*
|
|
4
|
+
* <2–6 line summary: what it does, who calls it, key side effects.>
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
function numberFrom(...values) {
|
|
10
|
+
for (const value of values) {
|
|
11
|
+
const n = Number(value);
|
|
12
|
+
if (Number.isFinite(n) && n !== 0) return n;
|
|
13
|
+
}
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizeCostEntry(entry = {}) {
|
|
18
|
+
const cacheCreation5mInputTokens = numberFrom(
|
|
19
|
+
entry.cache_creation_5m_input_tokens,
|
|
20
|
+
entry.cacheCreation5mInputTokens,
|
|
21
|
+
entry.cache_creation?.ephemeral_5m_input_tokens,
|
|
22
|
+
);
|
|
23
|
+
const cacheCreation1hInputTokens = numberFrom(
|
|
24
|
+
entry.cache_creation_1h_input_tokens,
|
|
25
|
+
entry.cacheCreation1hInputTokens,
|
|
26
|
+
entry.cache_creation?.ephemeral_1h_input_tokens,
|
|
27
|
+
);
|
|
28
|
+
const explicitCacheCreation = numberFrom(
|
|
29
|
+
entry.cache_creation_input_tokens,
|
|
30
|
+
entry.cacheCreationInputTokens,
|
|
31
|
+
);
|
|
32
|
+
const cacheCreationInputTokens = explicitCacheCreation || cacheCreation5mInputTokens + cacheCreation1hInputTokens;
|
|
33
|
+
const cacheReadInputTokens = numberFrom(
|
|
34
|
+
entry.cache_read_input_tokens,
|
|
35
|
+
entry.cacheReadInputTokens,
|
|
36
|
+
entry.prompt_tokens_details?.cached_tokens,
|
|
37
|
+
);
|
|
38
|
+
const inputTokens = numberFrom(
|
|
39
|
+
entry.input_tokens,
|
|
40
|
+
entry.inputTokens,
|
|
41
|
+
entry.prompt_tokens,
|
|
42
|
+
entry.promptTokens,
|
|
43
|
+
);
|
|
44
|
+
const outputTokens = numberFrom(
|
|
45
|
+
entry.output_tokens,
|
|
46
|
+
entry.outputTokens,
|
|
47
|
+
entry.completion_tokens,
|
|
48
|
+
entry.completionTokens,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
...entry,
|
|
53
|
+
inputTokens,
|
|
54
|
+
outputTokens,
|
|
55
|
+
cacheReadInputTokens,
|
|
56
|
+
cacheCreationInputTokens,
|
|
57
|
+
cacheCreation5mInputTokens,
|
|
58
|
+
cacheCreation1hInputTokens,
|
|
59
|
+
processedInputTokens: inputTokens + cacheReadInputTokens + cacheCreationInputTokens,
|
|
60
|
+
totalTokens: inputTokens + outputTokens,
|
|
61
|
+
costUsd: Number(entry.cost_usd ?? entry.costUsd ?? 0),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function readCostLog(homeDir) {
|
|
66
|
+
const logPath = join(homeDir, '.cx', 'session-cost.jsonl');
|
|
67
|
+
if (!existsSync(logPath)) return [];
|
|
68
|
+
try {
|
|
69
|
+
return readFileSync(logPath, 'utf8')
|
|
70
|
+
.split(/\r?\n/)
|
|
71
|
+
.map((line) => line.trim())
|
|
72
|
+
.filter(Boolean)
|
|
73
|
+
.map((line) => { try { return JSON.parse(line); } catch { return null; } })
|
|
74
|
+
.filter(Boolean);
|
|
75
|
+
} catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function clearCostLog(homeDir) {
|
|
81
|
+
const logPath = join(homeDir, '.cx', 'session-cost.jsonl');
|
|
82
|
+
if (existsSync(logPath)) writeFileSync(logPath, '');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function aggregateCostByAgent(entries) {
|
|
86
|
+
const agentMap = new Map();
|
|
87
|
+
for (const rawEntry of entries) {
|
|
88
|
+
const entry = normalizeCostEntry(rawEntry);
|
|
89
|
+
const agent = entry.agent || 'orchestrator';
|
|
90
|
+
const acc = agentMap.get(agent) ?? {
|
|
91
|
+
agent,
|
|
92
|
+
interactions: 0,
|
|
93
|
+
inputTokens: 0,
|
|
94
|
+
outputTokens: 0,
|
|
95
|
+
cacheReadInputTokens: 0,
|
|
96
|
+
cacheCreationInputTokens: 0,
|
|
97
|
+
processedInputTokens: 0,
|
|
98
|
+
costUsd: 0,
|
|
99
|
+
};
|
|
100
|
+
acc.interactions += 1;
|
|
101
|
+
acc.inputTokens += entry.inputTokens;
|
|
102
|
+
acc.outputTokens += entry.outputTokens;
|
|
103
|
+
acc.cacheReadInputTokens += entry.cacheReadInputTokens;
|
|
104
|
+
acc.cacheCreationInputTokens += entry.cacheCreationInputTokens;
|
|
105
|
+
acc.processedInputTokens += entry.processedInputTokens;
|
|
106
|
+
acc.costUsd += entry.costUsd;
|
|
107
|
+
acc.cacheReadRate = acc.processedInputTokens > 0
|
|
108
|
+
? Number((acc.cacheReadInputTokens / acc.processedInputTokens).toFixed(3))
|
|
109
|
+
: 0;
|
|
110
|
+
agentMap.set(agent, acc);
|
|
111
|
+
}
|
|
112
|
+
return [...agentMap.values()].sort((a, b) => b.costUsd - a.costUsd);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function computeCacheStats(entries) {
|
|
116
|
+
const normalized = entries.map((entry) => normalizeCostEntry(entry));
|
|
117
|
+
const totalInput = normalized.reduce((sum, e) => sum + e.inputTokens, 0);
|
|
118
|
+
const totalCacheReadInputTokens = normalized.reduce((sum, e) => sum + e.cacheReadInputTokens, 0);
|
|
119
|
+
const totalCacheCreationInputTokens = normalized.reduce((sum, e) => sum + e.cacheCreationInputTokens, 0);
|
|
120
|
+
const totalCacheCreation5mInputTokens = normalized.reduce((sum, e) => sum + e.cacheCreation5mInputTokens, 0);
|
|
121
|
+
const totalCacheCreation1hInputTokens = normalized.reduce((sum, e) => sum + e.cacheCreation1hInputTokens, 0);
|
|
122
|
+
const totalProcessedInputTokens = totalInput + totalCacheReadInputTokens + totalCacheCreationInputTokens;
|
|
123
|
+
return {
|
|
124
|
+
totalInput,
|
|
125
|
+
totalCacheReadInputTokens,
|
|
126
|
+
totalCacheCreationInputTokens,
|
|
127
|
+
totalCacheCreation5mInputTokens,
|
|
128
|
+
totalCacheCreation1hInputTokens,
|
|
129
|
+
totalProcessedInputTokens,
|
|
130
|
+
totalCached: totalCacheReadInputTokens + totalCacheCreationInputTokens,
|
|
131
|
+
cacheReadRate: totalProcessedInputTokens > 0 ? Number((totalCacheReadInputTokens / totalProcessedInputTokens).toFixed(3)) : 0,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function summarizeCostData(entries, { days, agent } = {}) {
|
|
136
|
+
const cutoff = days ? new Date(Date.now() - days * 86_400_000).toISOString() : null;
|
|
137
|
+
let filtered = cutoff ? entries.filter((e) => e.ts && e.ts >= cutoff) : entries;
|
|
138
|
+
if (agent) filtered = filtered.filter((e) => (e.agent || 'orchestrator') === agent);
|
|
139
|
+
|
|
140
|
+
const normalized = filtered.map((entry) => normalizeCostEntry(entry));
|
|
141
|
+
const totalInputTokens = normalized.reduce((s, e) => s + e.inputTokens, 0);
|
|
142
|
+
const totalOutputTokens = normalized.reduce((s, e) => s + e.outputTokens, 0);
|
|
143
|
+
const totalTokens = totalInputTokens + totalOutputTokens;
|
|
144
|
+
const totalCostUsd = normalized.reduce((s, e) => s + e.costUsd, 0);
|
|
145
|
+
const cacheStats = computeCacheStats(normalized);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
interactions: normalized.length,
|
|
149
|
+
totalInputTokens,
|
|
150
|
+
totalOutputTokens,
|
|
151
|
+
totalTokens,
|
|
152
|
+
totalCostUsd: Number(totalCostUsd.toFixed(4)),
|
|
153
|
+
processedInputTokens: cacheStats.totalProcessedInputTokens,
|
|
154
|
+
cacheReadInputTokens: cacheStats.totalCacheReadInputTokens,
|
|
155
|
+
cacheCreationInputTokens: cacheStats.totalCacheCreationInputTokens,
|
|
156
|
+
cacheCreation5mInputTokens: cacheStats.totalCacheCreation5mInputTokens,
|
|
157
|
+
cacheCreation1hInputTokens: cacheStats.totalCacheCreation1hInputTokens,
|
|
158
|
+
cachedTokens: cacheStats.totalCached,
|
|
159
|
+
cacheReadRate: cacheStats.cacheReadRate,
|
|
160
|
+
cacheHitRate: cacheStats.cacheReadRate,
|
|
161
|
+
byAgent: aggregateCostByAgent(normalized),
|
|
162
|
+
days: days ?? null,
|
|
163
|
+
agentFilter: agent ?? null,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function formatCostReport(data, colors = {}) {
|
|
168
|
+
const C = { bold: '', dim: '', reset: '', green: '', yellow: '', cyan: '', red: '', ...colors };
|
|
169
|
+
const lines = [];
|
|
170
|
+
lines.push(`${C.bold}Construct Cost Report${C.reset}`);
|
|
171
|
+
lines.push('═════════════════════');
|
|
172
|
+
lines.push('');
|
|
173
|
+
|
|
174
|
+
if (data.days) lines.push(`${C.dim}Window: last ${data.days} day${data.days === 1 ? '' : 's'}${C.reset}`);
|
|
175
|
+
if (data.agentFilter) lines.push(`${C.dim}Agent filter: ${data.agentFilter}${C.reset}`);
|
|
176
|
+
|
|
177
|
+
lines.push(`Interactions: ${data.interactions}`);
|
|
178
|
+
lines.push(`Total tokens: ${data.totalTokens.toLocaleString()} (${data.totalInputTokens.toLocaleString()} uncached in / ${data.totalOutputTokens.toLocaleString()} out)`);
|
|
179
|
+
lines.push(`Processed input: ${Number(data.processedInputTokens || data.totalInputTokens || 0).toLocaleString()}`);
|
|
180
|
+
|
|
181
|
+
const readPct = ((data.cacheReadRate ?? data.cacheHitRate ?? 0) * 100).toFixed(1);
|
|
182
|
+
const cacheColor = (data.cacheReadRate ?? data.cacheHitRate ?? 0) >= 0.5 ? C.green : (data.cacheReadRate ?? data.cacheHitRate ?? 0) >= 0.2 ? C.yellow : C.red;
|
|
183
|
+
lines.push(`Cache reads: ${Number(data.cacheReadInputTokens || 0).toLocaleString()} (${cacheColor}${readPct}% read rate${C.reset})`);
|
|
184
|
+
lines.push(`Cache writes: ${Number(data.cacheCreationInputTokens || 0).toLocaleString()} (${Number(data.cacheCreation5mInputTokens || 0).toLocaleString()} 5m / ${Number(data.cacheCreation1hInputTokens || 0).toLocaleString()} 1h)`);
|
|
185
|
+
lines.push(`Estimated cost: $${data.totalCostUsd.toFixed(4)}`);
|
|
186
|
+
|
|
187
|
+
if (data.byAgent.length > 0) {
|
|
188
|
+
lines.push('');
|
|
189
|
+
lines.push('By Agent:');
|
|
190
|
+
lines.push(` ${'Agent'.padEnd(28)} ${'Turns'.padStart(5)} ${'Input tok'.padStart(10)} ${'Cost'.padStart(8)} ${'Share'.padStart(5)} Cache`);
|
|
191
|
+
lines.push(` ${'-'.repeat(28)} ${'-'.repeat(5)} ${'-'.repeat(10)} ${'-'.repeat(8)} ${'-'.repeat(5)} -----`);
|
|
192
|
+
for (const a of data.byAgent) {
|
|
193
|
+
const sharePct = data.totalCostUsd > 0 ? Math.round((a.costUsd / data.totalCostUsd) * 100) : 0;
|
|
194
|
+
const hitRate = Math.round((a.cacheReadRate || 0) * 100);
|
|
195
|
+
const shareColor = sharePct >= 50 ? C.red : sharePct >= 25 ? C.yellow : '';
|
|
196
|
+
lines.push(
|
|
197
|
+
` ${a.agent.padEnd(28)} ${String(a.interactions).padStart(5)}` +
|
|
198
|
+
` ${a.inputTokens.toLocaleString().padStart(10)}` +
|
|
199
|
+
` ${'$' + a.costUsd.toFixed(4).padStart(7)}` +
|
|
200
|
+
` ${shareColor}${String(sharePct + '%').padStart(5)}${C.reset}` +
|
|
201
|
+
` ${hitRate}%`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (data.interactions === 0) {
|
|
207
|
+
lines.push('');
|
|
208
|
+
lines.push(`${C.dim}No cost data recorded yet. Token usage is logged at session end.${C.reset}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
lines.push('');
|
|
212
|
+
return lines.join('\n');
|
|
213
|
+
}
|