nodedex 0.1.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/adapters/claude-code-watcher.mjs +336 -0
- package/adapters/hermes-statedb-watcher.mjs +234 -0
- package/adapters/nodedex-capture-core.mjs +129 -0
- package/adapters/nodedex-capture.mjs +169 -0
- package/dist/agent-protocol.d.ts +7 -0
- package/dist/agent-protocol.d.ts.map +1 -0
- package/dist/agent-protocol.js +38 -0
- package/dist/agent-protocol.js.map +1 -0
- package/dist/api-server.d.ts +5 -0
- package/dist/api-server.d.ts.map +1 -0
- package/dist/api-server.js +351 -0
- package/dist/api-server.js.map +1 -0
- package/dist/boot-env.d.ts +2 -0
- package/dist/boot-env.d.ts.map +1 -0
- package/dist/boot-env.js +12 -0
- package/dist/boot-env.js.map +1 -0
- package/dist/engine/__tests__/search-core.test.d.ts +2 -0
- package/dist/engine/__tests__/search-core.test.d.ts.map +1 -0
- package/dist/engine/__tests__/search-core.test.js +139 -0
- package/dist/engine/__tests__/search-core.test.js.map +1 -0
- package/dist/engine/ai-provider.d.ts +45 -0
- package/dist/engine/ai-provider.d.ts.map +1 -0
- package/dist/engine/ai-provider.js +5 -0
- package/dist/engine/ai-provider.js.map +1 -0
- package/dist/engine/embeddings.d.ts +51 -0
- package/dist/engine/embeddings.d.ts.map +1 -0
- package/dist/engine/embeddings.js +89 -0
- package/dist/engine/embeddings.js.map +1 -0
- package/dist/engine/providers/__tests__/failure-policy.test.d.ts +2 -0
- package/dist/engine/providers/__tests__/failure-policy.test.d.ts.map +1 -0
- package/dist/engine/providers/__tests__/failure-policy.test.js +134 -0
- package/dist/engine/providers/__tests__/failure-policy.test.js.map +1 -0
- package/dist/engine/providers/__tests__/model-caps.test.d.ts +2 -0
- package/dist/engine/providers/__tests__/model-caps.test.d.ts.map +1 -0
- package/dist/engine/providers/__tests__/model-caps.test.js +38 -0
- package/dist/engine/providers/__tests__/model-caps.test.js.map +1 -0
- package/dist/engine/providers/__tests__/openai-structured.test.d.ts +2 -0
- package/dist/engine/providers/__tests__/openai-structured.test.d.ts.map +1 -0
- package/dist/engine/providers/__tests__/openai-structured.test.js +73 -0
- package/dist/engine/providers/__tests__/openai-structured.test.js.map +1 -0
- package/dist/engine/providers/__tests__/usage-ledger.test.d.ts +2 -0
- package/dist/engine/providers/__tests__/usage-ledger.test.d.ts.map +1 -0
- package/dist/engine/providers/__tests__/usage-ledger.test.js +108 -0
- package/dist/engine/providers/__tests__/usage-ledger.test.js.map +1 -0
- package/dist/engine/providers/anthropic.d.ts +17 -0
- package/dist/engine/providers/anthropic.d.ts.map +1 -0
- package/dist/engine/providers/anthropic.js +125 -0
- package/dist/engine/providers/anthropic.js.map +1 -0
- package/dist/engine/providers/failure-policy.d.ts +56 -0
- package/dist/engine/providers/failure-policy.d.ts.map +1 -0
- package/dist/engine/providers/failure-policy.js +120 -0
- package/dist/engine/providers/failure-policy.js.map +1 -0
- package/dist/engine/providers/gemini.d.ts +22 -0
- package/dist/engine/providers/gemini.d.ts.map +1 -0
- package/dist/engine/providers/gemini.js +180 -0
- package/dist/engine/providers/gemini.js.map +1 -0
- package/dist/engine/providers/index.d.ts +8 -0
- package/dist/engine/providers/index.d.ts.map +1 -0
- package/dist/engine/providers/index.js +67 -0
- package/dist/engine/providers/index.js.map +1 -0
- package/dist/engine/providers/local.d.ts +12 -0
- package/dist/engine/providers/local.d.ts.map +1 -0
- package/dist/engine/providers/local.js +46 -0
- package/dist/engine/providers/local.js.map +1 -0
- package/dist/engine/providers/model-caps.d.ts +6 -0
- package/dist/engine/providers/model-caps.d.ts.map +1 -0
- package/dist/engine/providers/model-caps.js +49 -0
- package/dist/engine/providers/model-caps.js.map +1 -0
- package/dist/engine/providers/openai.d.ts +30 -0
- package/dist/engine/providers/openai.d.ts.map +1 -0
- package/dist/engine/providers/openai.js +309 -0
- package/dist/engine/providers/openai.js.map +1 -0
- package/dist/engine/providers/usage-ledger.d.ts +69 -0
- package/dist/engine/providers/usage-ledger.d.ts.map +1 -0
- package/dist/engine/providers/usage-ledger.js +209 -0
- package/dist/engine/providers/usage-ledger.js.map +1 -0
- package/dist/engine/search-core.d.ts +40 -0
- package/dist/engine/search-core.d.ts.map +1 -0
- package/dist/engine/search-core.js +109 -0
- package/dist/engine/search-core.js.map +1 -0
- package/dist/engine/vector-math.d.ts +5 -0
- package/dist/engine/vector-math.d.ts.map +1 -0
- package/dist/engine/vector-math.js +25 -0
- package/dist/engine/vector-math.js.map +1 -0
- package/dist/home-env.d.ts +26 -0
- package/dist/home-env.d.ts.map +1 -0
- package/dist/home-env.js +87 -0
- package/dist/home-env.js.map +1 -0
- package/dist/mcp-server.d.ts +13 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +79 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/middleware/auth.d.ts +23 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +104 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/auto-recall.d.ts +7 -0
- package/dist/middleware/auto-recall.d.ts.map +1 -0
- package/dist/middleware/auto-recall.js +257 -0
- package/dist/middleware/auto-recall.js.map +1 -0
- package/dist/middleware/auto-reflect.d.ts +4 -0
- package/dist/middleware/auto-reflect.d.ts.map +1 -0
- package/dist/middleware/auto-reflect.js +5 -0
- package/dist/middleware/auto-reflect.js.map +1 -0
- package/dist/middleware/reflect/apply-flag-verdict.d.ts +27 -0
- package/dist/middleware/reflect/apply-flag-verdict.d.ts.map +1 -0
- package/dist/middleware/reflect/apply-flag-verdict.js +57 -0
- package/dist/middleware/reflect/apply-flag-verdict.js.map +1 -0
- package/dist/middleware/reflect/arc-entity-resolve.d.ts +29 -0
- package/dist/middleware/reflect/arc-entity-resolve.d.ts.map +1 -0
- package/dist/middleware/reflect/arc-entity-resolve.js +356 -0
- package/dist/middleware/reflect/arc-entity-resolve.js.map +1 -0
- package/dist/middleware/reflect/arc-inactivity-timer.d.ts +47 -0
- package/dist/middleware/reflect/arc-inactivity-timer.d.ts.map +1 -0
- package/dist/middleware/reflect/arc-inactivity-timer.js +175 -0
- package/dist/middleware/reflect/arc-inactivity-timer.js.map +1 -0
- package/dist/middleware/reflect/arc-pipeline.d.ts +33 -0
- package/dist/middleware/reflect/arc-pipeline.d.ts.map +1 -0
- package/dist/middleware/reflect/arc-pipeline.js +498 -0
- package/dist/middleware/reflect/arc-pipeline.js.map +1 -0
- package/dist/middleware/reflect/comprehend-pergroup.d.ts +100 -0
- package/dist/middleware/reflect/comprehend-pergroup.d.ts.map +1 -0
- package/dist/middleware/reflect/comprehend-pergroup.js +610 -0
- package/dist/middleware/reflect/comprehend-pergroup.js.map +1 -0
- package/dist/middleware/reflect/comprehend.d.ts +237 -0
- package/dist/middleware/reflect/comprehend.d.ts.map +1 -0
- package/dist/middleware/reflect/comprehend.js +706 -0
- package/dist/middleware/reflect/comprehend.js.map +1 -0
- package/dist/middleware/reflect/config.d.ts +34 -0
- package/dist/middleware/reflect/config.d.ts.map +1 -0
- package/dist/middleware/reflect/config.js +131 -0
- package/dist/middleware/reflect/config.js.map +1 -0
- package/dist/middleware/reflect/context.d.ts +138 -0
- package/dist/middleware/reflect/context.d.ts.map +1 -0
- package/dist/middleware/reflect/context.js +619 -0
- package/dist/middleware/reflect/context.js.map +1 -0
- package/dist/middleware/reflect/cost-breakdown.d.ts +69 -0
- package/dist/middleware/reflect/cost-breakdown.d.ts.map +1 -0
- package/dist/middleware/reflect/cost-breakdown.js +63 -0
- package/dist/middleware/reflect/cost-breakdown.js.map +1 -0
- package/dist/middleware/reflect/cost-guard.d.ts +102 -0
- package/dist/middleware/reflect/cost-guard.d.ts.map +1 -0
- package/dist/middleware/reflect/cost-guard.js +243 -0
- package/dist/middleware/reflect/cost-guard.js.map +1 -0
- package/dist/middleware/reflect/cost-pricing.d.ts +54 -0
- package/dist/middleware/reflect/cost-pricing.d.ts.map +1 -0
- package/dist/middleware/reflect/cost-pricing.js +148 -0
- package/dist/middleware/reflect/cost-pricing.js.map +1 -0
- package/dist/middleware/reflect/cross-group-link.d.ts +61 -0
- package/dist/middleware/reflect/cross-group-link.d.ts.map +1 -0
- package/dist/middleware/reflect/cross-group-link.js +212 -0
- package/dist/middleware/reflect/cross-group-link.js.map +1 -0
- package/dist/middleware/reflect/dedup-by-source-and-value.d.ts +70 -0
- package/dist/middleware/reflect/dedup-by-source-and-value.d.ts.map +1 -0
- package/dist/middleware/reflect/dedup-by-source-and-value.js +0 -0
- package/dist/middleware/reflect/dedup-by-source-and-value.js.map +1 -0
- package/dist/middleware/reflect/describe-roots.d.ts +58 -0
- package/dist/middleware/reflect/describe-roots.d.ts.map +1 -0
- package/dist/middleware/reflect/describe-roots.js +266 -0
- package/dist/middleware/reflect/describe-roots.js.map +1 -0
- package/dist/middleware/reflect/flag-reviewer-startup.d.ts +16 -0
- package/dist/middleware/reflect/flag-reviewer-startup.d.ts.map +1 -0
- package/dist/middleware/reflect/flag-reviewer-startup.js +107 -0
- package/dist/middleware/reflect/flag-reviewer-startup.js.map +1 -0
- package/dist/middleware/reflect/flag-reviewer.d.ts +69 -0
- package/dist/middleware/reflect/flag-reviewer.d.ts.map +1 -0
- package/dist/middleware/reflect/flag-reviewer.js +520 -0
- package/dist/middleware/reflect/flag-reviewer.js.map +1 -0
- package/dist/middleware/reflect/inline-dedup.d.ts +26 -0
- package/dist/middleware/reflect/inline-dedup.d.ts.map +1 -0
- package/dist/middleware/reflect/inline-dedup.js +131 -0
- package/dist/middleware/reflect/inline-dedup.js.map +1 -0
- package/dist/middleware/reflect/justify-decisions.d.ts +37 -0
- package/dist/middleware/reflect/justify-decisions.d.ts.map +1 -0
- package/dist/middleware/reflect/justify-decisions.js +159 -0
- package/dist/middleware/reflect/justify-decisions.js.map +1 -0
- package/dist/middleware/reflect/nl-accept.d.ts +35 -0
- package/dist/middleware/reflect/nl-accept.d.ts.map +1 -0
- package/dist/middleware/reflect/nl-accept.js +167 -0
- package/dist/middleware/reflect/nl-accept.js.map +1 -0
- package/dist/middleware/reflect/pass0.d.ts +20 -0
- package/dist/middleware/reflect/pass0.d.ts.map +1 -0
- package/dist/middleware/reflect/pass0.js +423 -0
- package/dist/middleware/reflect/pass0.js.map +1 -0
- package/dist/middleware/reflect/pass1.d.ts +17 -0
- package/dist/middleware/reflect/pass1.d.ts.map +1 -0
- package/dist/middleware/reflect/pass1.js +241 -0
- package/dist/middleware/reflect/pass1.js.map +1 -0
- package/dist/middleware/reflect/pass2-quarantine.d.ts +129 -0
- package/dist/middleware/reflect/pass2-quarantine.d.ts.map +1 -0
- package/dist/middleware/reflect/pass2-quarantine.js +272 -0
- package/dist/middleware/reflect/pass2-quarantine.js.map +1 -0
- package/dist/middleware/reflect/pass2-seams.d.ts +205 -0
- package/dist/middleware/reflect/pass2-seams.d.ts.map +1 -0
- package/dist/middleware/reflect/pass2-seams.js +279 -0
- package/dist/middleware/reflect/pass2-seams.js.map +1 -0
- package/dist/middleware/reflect/pass2-split-orchestrator.d.ts +37 -0
- package/dist/middleware/reflect/pass2-split-orchestrator.d.ts.map +1 -0
- package/dist/middleware/reflect/pass2-split-orchestrator.js +531 -0
- package/dist/middleware/reflect/pass2-split-orchestrator.js.map +1 -0
- package/dist/middleware/reflect/pass2.d.ts +17 -0
- package/dist/middleware/reflect/pass2.d.ts.map +1 -0
- package/dist/middleware/reflect/pass2.js +324 -0
- package/dist/middleware/reflect/pass2.js.map +1 -0
- package/dist/middleware/reflect/pass2a.d.ts +141 -0
- package/dist/middleware/reflect/pass2a.d.ts.map +1 -0
- package/dist/middleware/reflect/pass2a.js +404 -0
- package/dist/middleware/reflect/pass2a.js.map +1 -0
- package/dist/middleware/reflect/pass2b.d.ts +108 -0
- package/dist/middleware/reflect/pass2b.d.ts.map +1 -0
- package/dist/middleware/reflect/pass2b.js +480 -0
- package/dist/middleware/reflect/pass2b.js.map +1 -0
- package/dist/middleware/reflect/pass2c.d.ts +113 -0
- package/dist/middleware/reflect/pass2c.d.ts.map +1 -0
- package/dist/middleware/reflect/pass2c.js +360 -0
- package/dist/middleware/reflect/pass2c.js.map +1 -0
- package/dist/middleware/reflect/pass3-batch.d.ts +62 -0
- package/dist/middleware/reflect/pass3-batch.d.ts.map +1 -0
- package/dist/middleware/reflect/pass3-batch.js +139 -0
- package/dist/middleware/reflect/pass3-batch.js.map +1 -0
- package/dist/middleware/reflect/pass3.d.ts +23 -0
- package/dist/middleware/reflect/pass3.d.ts.map +1 -0
- package/dist/middleware/reflect/pass3.js +371 -0
- package/dist/middleware/reflect/pass3.js.map +1 -0
- package/dist/middleware/reflect/pass4-slice.d.ts +25 -0
- package/dist/middleware/reflect/pass4-slice.d.ts.map +1 -0
- package/dist/middleware/reflect/pass4-slice.js +315 -0
- package/dist/middleware/reflect/pass4-slice.js.map +1 -0
- package/dist/middleware/reflect/pass4.d.ts +30 -0
- package/dist/middleware/reflect/pass4.d.ts.map +1 -0
- package/dist/middleware/reflect/pass4.js +193 -0
- package/dist/middleware/reflect/pass4.js.map +1 -0
- package/dist/middleware/reflect/pass5.d.ts +22 -0
- package/dist/middleware/reflect/pass5.d.ts.map +1 -0
- package/dist/middleware/reflect/pass5.js +178 -0
- package/dist/middleware/reflect/pass5.js.map +1 -0
- package/dist/middleware/reflect/pass_judge.d.ts +44 -0
- package/dist/middleware/reflect/pass_judge.d.ts.map +1 -0
- package/dist/middleware/reflect/pass_judge.js +263 -0
- package/dist/middleware/reflect/pass_judge.js.map +1 -0
- package/dist/middleware/reflect/pipeline-flags.d.ts +140 -0
- package/dist/middleware/reflect/pipeline-flags.d.ts.map +1 -0
- package/dist/middleware/reflect/pipeline-flags.js +314 -0
- package/dist/middleware/reflect/pipeline-flags.js.map +1 -0
- package/dist/middleware/reflect/pipeline.d.ts +237 -0
- package/dist/middleware/reflect/pipeline.d.ts.map +1 -0
- package/dist/middleware/reflect/pipeline.js +3114 -0
- package/dist/middleware/reflect/pipeline.js.map +1 -0
- package/dist/middleware/reflect/promptOverride.d.ts +14 -0
- package/dist/middleware/reflect/promptOverride.d.ts.map +1 -0
- package/dist/middleware/reflect/promptOverride.js +28 -0
- package/dist/middleware/reflect/promptOverride.js.map +1 -0
- package/dist/middleware/reflect/provenance-check.d.ts +48 -0
- package/dist/middleware/reflect/provenance-check.d.ts.map +1 -0
- package/dist/middleware/reflect/provenance-check.js +180 -0
- package/dist/middleware/reflect/provenance-check.js.map +1 -0
- package/dist/middleware/reflect/provenance-reviewer.d.ts +52 -0
- package/dist/middleware/reflect/provenance-reviewer.d.ts.map +1 -0
- package/dist/middleware/reflect/provenance-reviewer.js +253 -0
- package/dist/middleware/reflect/provenance-reviewer.js.map +1 -0
- package/dist/middleware/reflect/prune-collapsed-types.d.ts +11 -0
- package/dist/middleware/reflect/prune-collapsed-types.d.ts.map +1 -0
- package/dist/middleware/reflect/prune-collapsed-types.js +32 -0
- package/dist/middleware/reflect/prune-collapsed-types.js.map +1 -0
- package/dist/middleware/reflect/recognize-root.d.ts +75 -0
- package/dist/middleware/reflect/recognize-root.d.ts.map +1 -0
- package/dist/middleware/reflect/recognize-root.js +204 -0
- package/dist/middleware/reflect/recognize-root.js.map +1 -0
- package/dist/middleware/reflect/render-agent-flag.d.ts +25 -0
- package/dist/middleware/reflect/render-agent-flag.d.ts.map +1 -0
- package/dist/middleware/reflect/render-agent-flag.js +39 -0
- package/dist/middleware/reflect/render-agent-flag.js.map +1 -0
- package/dist/middleware/reflect/retrieve-graph-slice.d.ts +54 -0
- package/dist/middleware/reflect/retrieve-graph-slice.d.ts.map +1 -0
- package/dist/middleware/reflect/retrieve-graph-slice.js +173 -0
- package/dist/middleware/reflect/retrieve-graph-slice.js.map +1 -0
- package/dist/middleware/reflect/root-relatedness.d.ts +31 -0
- package/dist/middleware/reflect/root-relatedness.d.ts.map +1 -0
- package/dist/middleware/reflect/root-relatedness.js +92 -0
- package/dist/middleware/reflect/root-relatedness.js.map +1 -0
- package/dist/middleware/reflect/schema-heal.d.ts +22 -0
- package/dist/middleware/reflect/schema-heal.d.ts.map +1 -0
- package/dist/middleware/reflect/schema-heal.js +119 -0
- package/dist/middleware/reflect/schema-heal.js.map +1 -0
- package/dist/middleware/reflect/schema-validator.d.ts +85 -0
- package/dist/middleware/reflect/schema-validator.d.ts.map +1 -0
- package/dist/middleware/reflect/schema-validator.js +196 -0
- package/dist/middleware/reflect/schema-validator.js.map +1 -0
- package/dist/middleware/reflect/stage-audit-graph.d.ts +115 -0
- package/dist/middleware/reflect/stage-audit-graph.d.ts.map +1 -0
- package/dist/middleware/reflect/stage-audit-graph.js +563 -0
- package/dist/middleware/reflect/stage-audit-graph.js.map +1 -0
- package/dist/middleware/reflect/stage-d-resolve-graph.d.ts +87 -0
- package/dist/middleware/reflect/stage-d-resolve-graph.d.ts.map +1 -0
- package/dist/middleware/reflect/stage-d-resolve-graph.js +256 -0
- package/dist/middleware/reflect/stage-d-resolve-graph.js.map +1 -0
- package/dist/middleware/reflect/synthesizeFromSceneCard.d.ts +15 -0
- package/dist/middleware/reflect/synthesizeFromSceneCard.d.ts.map +1 -0
- package/dist/middleware/reflect/synthesizeFromSceneCard.js +91 -0
- package/dist/middleware/reflect/synthesizeFromSceneCard.js.map +1 -0
- package/dist/middleware/reflect/types.d.ts +261 -0
- package/dist/middleware/reflect/types.d.ts.map +1 -0
- package/dist/middleware/reflect/types.js +3 -0
- package/dist/middleware/reflect/types.js.map +1 -0
- package/dist/middleware/reflect/v2-integrate.d.ts +120 -0
- package/dist/middleware/reflect/v2-integrate.d.ts.map +1 -0
- package/dist/middleware/reflect/v2-integrate.js +388 -0
- package/dist/middleware/reflect/v2-integrate.js.map +1 -0
- package/dist/middleware/reflect/v2-judge.d.ts +44 -0
- package/dist/middleware/reflect/v2-judge.d.ts.map +1 -0
- package/dist/middleware/reflect/v2-judge.js +191 -0
- package/dist/middleware/reflect/v2-judge.js.map +1 -0
- package/dist/relation-sets.d.ts +2 -0
- package/dist/relation-sets.d.ts.map +1 -0
- package/dist/relation-sets.js +35 -0
- package/dist/relation-sets.js.map +1 -0
- package/dist/routes/__tests__/flags.test.d.ts +2 -0
- package/dist/routes/__tests__/flags.test.d.ts.map +1 -0
- package/dist/routes/__tests__/flags.test.js +257 -0
- package/dist/routes/__tests__/flags.test.js.map +1 -0
- package/dist/routes/__tests__/models-catalog.test.d.ts +2 -0
- package/dist/routes/__tests__/models-catalog.test.d.ts.map +1 -0
- package/dist/routes/__tests__/models-catalog.test.js +130 -0
- package/dist/routes/__tests__/models-catalog.test.js.map +1 -0
- package/dist/routes/__tests__/reflect-pause-drain.test.d.ts +2 -0
- package/dist/routes/__tests__/reflect-pause-drain.test.d.ts.map +1 -0
- package/dist/routes/__tests__/reflect-pause-drain.test.js +38 -0
- package/dist/routes/__tests__/reflect-pause-drain.test.js.map +1 -0
- package/dist/routes/__tests__/spend-pause-drain.test.d.ts +2 -0
- package/dist/routes/__tests__/spend-pause-drain.test.d.ts.map +1 -0
- package/dist/routes/__tests__/spend-pause-drain.test.js +38 -0
- package/dist/routes/__tests__/spend-pause-drain.test.js.map +1 -0
- package/dist/routes/admin.d.ts +49 -0
- package/dist/routes/admin.d.ts.map +1 -0
- package/dist/routes/admin.js +471 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/blocks.d.ts +4 -0
- package/dist/routes/blocks.d.ts.map +1 -0
- package/dist/routes/blocks.js +893 -0
- package/dist/routes/blocks.js.map +1 -0
- package/dist/routes/chat-proxy.d.ts +5 -0
- package/dist/routes/chat-proxy.d.ts.map +1 -0
- package/dist/routes/chat-proxy.js +225 -0
- package/dist/routes/chat-proxy.js.map +1 -0
- package/dist/routes/conversations.d.ts +4 -0
- package/dist/routes/conversations.d.ts.map +1 -0
- package/dist/routes/conversations.js +139 -0
- package/dist/routes/conversations.js.map +1 -0
- package/dist/routes/flags.d.ts +4 -0
- package/dist/routes/flags.d.ts.map +1 -0
- package/dist/routes/flags.js +151 -0
- package/dist/routes/flags.js.map +1 -0
- package/dist/routes/inject.d.ts +4 -0
- package/dist/routes/inject.d.ts.map +1 -0
- package/dist/routes/inject.js +183 -0
- package/dist/routes/inject.js.map +1 -0
- package/dist/routes/mcp-http.d.ts +5 -0
- package/dist/routes/mcp-http.d.ts.map +1 -0
- package/dist/routes/mcp-http.js +94 -0
- package/dist/routes/mcp-http.js.map +1 -0
- package/dist/routes/quarantine.d.ts +4 -0
- package/dist/routes/quarantine.d.ts.map +1 -0
- package/dist/routes/quarantine.js +66 -0
- package/dist/routes/quarantine.js.map +1 -0
- package/dist/routes/recall.d.ts +5 -0
- package/dist/routes/recall.d.ts.map +1 -0
- package/dist/routes/recall.js +573 -0
- package/dist/routes/recall.js.map +1 -0
- package/dist/routes/reflect.d.ts +5 -0
- package/dist/routes/reflect.d.ts.map +1 -0
- package/dist/routes/reflect.js +231 -0
- package/dist/routes/reflect.js.map +1 -0
- package/dist/routes/session.d.ts +4 -0
- package/dist/routes/session.d.ts.map +1 -0
- package/dist/routes/session.js +418 -0
- package/dist/routes/session.js.map +1 -0
- package/dist/routes/state.d.ts +116 -0
- package/dist/routes/state.d.ts.map +1 -0
- package/dist/routes/state.js +621 -0
- package/dist/routes/state.js.map +1 -0
- package/dist/routes/usage.d.ts +3 -0
- package/dist/routes/usage.d.ts.map +1 -0
- package/dist/routes/usage.js +141 -0
- package/dist/routes/usage.js.map +1 -0
- package/dist/routes/workspace.d.ts +5 -0
- package/dist/routes/workspace.d.ts.map +1 -0
- package/dist/routes/workspace.js +435 -0
- package/dist/routes/workspace.js.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +298 -0
- package/dist/server.js.map +1 -0
- package/dist/store/__tests__/backup.test.d.ts +2 -0
- package/dist/store/__tests__/backup.test.d.ts.map +1 -0
- package/dist/store/__tests__/backup.test.js +53 -0
- package/dist/store/__tests__/backup.test.js.map +1 -0
- package/dist/store/__tests__/quality.test.d.ts +2 -0
- package/dist/store/__tests__/quality.test.d.ts.map +1 -0
- package/dist/store/__tests__/quality.test.js +75 -0
- package/dist/store/__tests__/quality.test.js.map +1 -0
- package/dist/store/backup.d.ts +14 -0
- package/dist/store/backup.d.ts.map +1 -0
- package/dist/store/backup.js +95 -0
- package/dist/store/backup.js.map +1 -0
- package/dist/store/database.d.ts +407 -0
- package/dist/store/database.d.ts.map +1 -0
- package/dist/store/database.js +2004 -0
- package/dist/store/database.js.map +1 -0
- package/dist/store/quality.d.ts +25 -0
- package/dist/store/quality.d.ts.map +1 -0
- package/dist/store/quality.js +48 -0
- package/dist/store/quality.js.map +1 -0
- package/dist/tools/__tests__/assemble-block-chains.test.d.ts +2 -0
- package/dist/tools/__tests__/assemble-block-chains.test.d.ts.map +1 -0
- package/dist/tools/__tests__/assemble-block-chains.test.js +118 -0
- package/dist/tools/__tests__/assemble-block-chains.test.js.map +1 -0
- package/dist/tools/__tests__/filter-roots-by-concepts.test.d.ts +2 -0
- package/dist/tools/__tests__/filter-roots-by-concepts.test.d.ts.map +1 -0
- package/dist/tools/__tests__/filter-roots-by-concepts.test.js +68 -0
- package/dist/tools/__tests__/filter-roots-by-concepts.test.js.map +1 -0
- package/dist/tools/__tests__/flag-surface.test.d.ts +2 -0
- package/dist/tools/__tests__/flag-surface.test.d.ts.map +1 -0
- package/dist/tools/__tests__/flag-surface.test.js +130 -0
- package/dist/tools/__tests__/flag-surface.test.js.map +1 -0
- package/dist/tools/core.d.ts +5 -0
- package/dist/tools/core.d.ts.map +1 -0
- package/dist/tools/core.js +962 -0
- package/dist/tools/core.js.map +1 -0
- package/dist/tools/derive.d.ts +5 -0
- package/dist/tools/derive.d.ts.map +1 -0
- package/dist/tools/derive.js +182 -0
- package/dist/tools/derive.js.map +1 -0
- package/dist/tools/flag-surface.d.ts +26 -0
- package/dist/tools/flag-surface.d.ts.map +1 -0
- package/dist/tools/flag-surface.js +59 -0
- package/dist/tools/flag-surface.js.map +1 -0
- package/dist/tools/helpers.d.ts +99 -0
- package/dist/tools/helpers.d.ts.map +1 -0
- package/dist/tools/helpers.js +243 -0
- package/dist/tools/helpers.js.map +1 -0
- package/dist/tools/projects.d.ts +5 -0
- package/dist/tools/projects.d.ts.map +1 -0
- package/dist/tools/projects.js +175 -0
- package/dist/tools/projects.js.map +1 -0
- package/dist/tools/system.d.ts +5 -0
- package/dist/tools/system.d.ts.map +1 -0
- package/dist/tools/system.js +1361 -0
- package/dist/tools/system.js.map +1 -0
- package/dist/tools/tasks.d.ts +5 -0
- package/dist/tools/tasks.d.ts.map +1 -0
- package/dist/tools/tasks.js +289 -0
- package/dist/tools/tasks.js.map +1 -0
- package/package.json +69 -0
- package/scripts/nodedex-entry.mjs +396 -0
- package/tui-dist/App.js +185 -0
- package/tui-dist/api.js +197 -0
- package/tui-dist/cli.js +53 -0
- package/tui-dist/components.js +63 -0
- package/tui-dist/config.js +242 -0
- package/tui-dist/connect-snippets.js +98 -0
- package/tui-dist/feed.js +51 -0
- package/tui-dist/health.js +465 -0
- package/tui-dist/hooks.js +23 -0
- package/tui-dist/memory.js +220 -0
- package/tui-dist/onboarding.js +498 -0
- package/tui-dist/review.js +193 -0
- package/tui-dist/servers.js +556 -0
- package/tui-dist/smoke.js +15 -0
- package/tui-dist/theme.js +106 -0
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
// servers.ts — multi-server registry + process manager for the Servers pane.
|
|
2
|
+
//
|
|
3
|
+
// The TUI is normally a pure read-client. This module is the one deliberate
|
|
4
|
+
// exception (user-approved): it can LAUNCH and STOP NodeDex servers. That
|
|
5
|
+
// crosses into process-management, so the footguns this project already hit are
|
|
6
|
+
// fenced HERE, in one place:
|
|
7
|
+
// - launched servers run the FULL system: the maintenance workers (flag-reviewer,
|
|
8
|
+
// describer, schema-heal, …) default ON — overridable via env / ~/.nodedex/.env.
|
|
9
|
+
// The legacy per-turn NODEDEX_INACTIVITY_REFLECT stays 0 (arc mode supersedes it).
|
|
10
|
+
// - user settings saved to ~/.nodedex/.env (arc auto-turns, etc.) are read here so they
|
|
11
|
+
// survive a relaunch (the launcher's explicit env otherwise shadows the env-file)
|
|
12
|
+
// - env overrides (PORT, WORKSPACE_DB_PATH) WIN over the server's .env because
|
|
13
|
+
// node --env-file does not override already-set vars (the isolation the
|
|
14
|
+
// RUNBOOK relies on)
|
|
15
|
+
// - stdout/stderr are captured to a log file (no silent failures)
|
|
16
|
+
// - the TUI can only STOP servers IT launched (we hold the child handle);
|
|
17
|
+
// pre-existing servers are shown but never killed (containment)
|
|
18
|
+
// - all launched children are killed when the TUI exits
|
|
19
|
+
import { spawn } from "child_process";
|
|
20
|
+
import { createServer } from "net";
|
|
21
|
+
import { fileURLToPath } from "url";
|
|
22
|
+
import { dirname, resolve } from "path";
|
|
23
|
+
import { homedir } from "os";
|
|
24
|
+
import { mkdirSync, createWriteStream, readFileSync, writeFileSync, existsSync, readdirSync, statSync, renameSync, rmSync } from "fs";
|
|
25
|
+
import { probeServer, prefer127, registerToken } from "./api.js";
|
|
26
|
+
import { providerEnv } from "./config.js";
|
|
27
|
+
import { randomBytes } from "node:crypto";
|
|
28
|
+
// Server dir — two layouts:
|
|
29
|
+
// repo/dev: <repo>/tui/src/servers.ts → ../../server (runs src via tsx)
|
|
30
|
+
// packaged: <pkg>/tui-dist/servers.js → .. (the npm package root; runs dist)
|
|
31
|
+
// Probe for the server's entry instead of assuming the repo shape.
|
|
32
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const SERVER_DIR = [resolve(HERE, "..", "..", "server"), resolve(HERE, "..")]
|
|
34
|
+
.find((d) => existsSync(resolve(d, "src", "server.ts")) || existsSync(resolve(d, "dist", "server.js")))
|
|
35
|
+
?? resolve(HERE, "..", "..", "server");
|
|
36
|
+
const NODEDEX_HOME = resolve(homedir(), ".nodedex");
|
|
37
|
+
/** Read the persisted ~/.nodedex/.env (the file POST /api/admin/config writes) so a user's
|
|
38
|
+
* saved settings — e.g. arc auto-turns — survive a TUI relaunch. The launcher sets some env
|
|
39
|
+
* keys explicitly, and those WIN over the spawned server's --env-file load, so without reading
|
|
40
|
+
* the saved file here a user's choice would be shadowed by the hardcoded default every relaunch.
|
|
41
|
+
* Precedence used below: explicit process.env > saved ~/.nodedex/.env > built-in default.
|
|
42
|
+
* Best-effort: a missing/garbled file yields {}. */
|
|
43
|
+
function readHomeEnv() {
|
|
44
|
+
const out = {};
|
|
45
|
+
try {
|
|
46
|
+
const p = resolve(NODEDEX_HOME, ".env");
|
|
47
|
+
if (!existsSync(p))
|
|
48
|
+
return out;
|
|
49
|
+
for (const raw of readFileSync(p, "utf8").split(/\r?\n/)) {
|
|
50
|
+
const line = raw.trim();
|
|
51
|
+
if (!line || line.startsWith("#"))
|
|
52
|
+
continue;
|
|
53
|
+
const eq = line.indexOf("=");
|
|
54
|
+
if (eq < 1)
|
|
55
|
+
continue;
|
|
56
|
+
out[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch { /* best-effort — fall back to defaults */ }
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
const PINS_FILE = resolve(NODEDEX_HOME, "tui-servers.json");
|
|
63
|
+
const SESSION_FILE = resolve(NODEDEX_HOME, "tui-session.json");
|
|
64
|
+
const LOG_DIR = resolve(NODEDEX_HOME, "tui-logs");
|
|
65
|
+
// ports probed during discovery (plus any pinned urls). 127.0.0.1, not localhost —
|
|
66
|
+
// the IPv4 host skips Windows's ::1-first penalty (see prefer127 in api.ts).
|
|
67
|
+
const CANDIDATE_PORTS = [3001, 3002, 3003, 3004, 3005, 3099];
|
|
68
|
+
const candidateUrl = (p) => `http://127.0.0.1:${p}`;
|
|
69
|
+
// ─── available DB files (for the launch/swap picker) ────────────────────────
|
|
70
|
+
// A port is just an access point; the DB is the content. Let the user pick the
|
|
71
|
+
// DB to run rather than type a path. Scan the dirs where DBs actually live.
|
|
72
|
+
// DBs live in the config home (~/.nodedex) — the same place the onboarding wizard
|
|
73
|
+
// (config.ts) creates + lists them, so both pickers agree. (Was repo/data + a hardcoded
|
|
74
|
+
// C:/tmp dev-scratch dir — dev leftovers that put user data inside the repo.)
|
|
75
|
+
const DATA_DIR = NODEDEX_HOME;
|
|
76
|
+
const DB_DIRS = [NODEDEX_HOME];
|
|
77
|
+
export function listDbs() {
|
|
78
|
+
const out = [];
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
for (const dir of DB_DIRS) {
|
|
81
|
+
let names = [];
|
|
82
|
+
try {
|
|
83
|
+
names = readdirSync(dir);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
for (const f of names) {
|
|
89
|
+
if (!f.endsWith(".db"))
|
|
90
|
+
continue; // excludes -wal/-shm and .backup-* sidecars
|
|
91
|
+
const p = resolve(dir, f);
|
|
92
|
+
if (seen.has(p))
|
|
93
|
+
continue;
|
|
94
|
+
seen.add(p);
|
|
95
|
+
try {
|
|
96
|
+
const s = statSync(p);
|
|
97
|
+
out.push({ path: p, name: f, sizeKB: Math.round(s.size / 1024), mtime: s.mtimeMs, empty: s.size <= 8192 });
|
|
98
|
+
}
|
|
99
|
+
catch { /* skip unreadable */ }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out.sort((a, b) => b.mtime - a.mtime); // most-recent first
|
|
103
|
+
}
|
|
104
|
+
// ─── create / rename / delete a db file ─────────────────────────────────────
|
|
105
|
+
// New DBs land in ~/.nodedex (a dir the picker already scans). A NodeDex DB is just a
|
|
106
|
+
// SQLite file the server creates + inits on first open, so "create" only resolves a
|
|
107
|
+
// valid path — the file appears when a server launches on it. Rename/delete touch the
|
|
108
|
+
// file + its -wal/-shm sidecars. GUARD: never rename/delete a db a running server holds
|
|
109
|
+
// (the DB-corruption rule) — refuse if a TUI-managed server has it open, and the OS lock
|
|
110
|
+
// is the backstop for external servers.
|
|
111
|
+
const DB_SIDECARS = ["", "-wal", "-shm"];
|
|
112
|
+
function sanitizeDbName(name) {
|
|
113
|
+
return (name || "").trim().replace(/\.db$/i, "")
|
|
114
|
+
.replace(/[^a-z0-9_-]/gi, "-") // any unsafe char → dash
|
|
115
|
+
.replace(/-+/g, "-") // collapse runs of dashes
|
|
116
|
+
.replace(/^-+|-+$/g, ""); // trim leading/trailing dashes
|
|
117
|
+
}
|
|
118
|
+
/** Is this db file currently open by a server THIS TUI launched? */
|
|
119
|
+
export function isDbInUse(dbPath) {
|
|
120
|
+
const norm = dbPath.replace(/\\/g, "/").toLowerCase();
|
|
121
|
+
for (const m of managed.values()) {
|
|
122
|
+
if ((m.dbPath ?? "").replace(/\\/g, "/").toLowerCase() === norm)
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
/** Resolve a new db path from a user name (data/<name>.db). Rejects empties + collisions. */
|
|
128
|
+
export function resolveNewDbPath(name) {
|
|
129
|
+
const safe = sanitizeDbName(name);
|
|
130
|
+
if (!safe)
|
|
131
|
+
return { ok: false, error: "name needs a letter or number" };
|
|
132
|
+
const path = resolve(DATA_DIR, `${safe}.db`);
|
|
133
|
+
if (existsSync(path))
|
|
134
|
+
return { ok: false, error: `"${safe}" already exists — pick it from the list` };
|
|
135
|
+
return { ok: true, path };
|
|
136
|
+
}
|
|
137
|
+
export function renameDb(oldPath, newName) {
|
|
138
|
+
const safe = sanitizeDbName(newName);
|
|
139
|
+
if (!safe)
|
|
140
|
+
return { ok: false, error: "name needs a letter or number" };
|
|
141
|
+
if (isDbInUse(oldPath))
|
|
142
|
+
return { ok: false, error: "db is in use — stop its server first ([x])" };
|
|
143
|
+
const path = resolve(dirname(oldPath), `${safe}.db`);
|
|
144
|
+
if (path === oldPath)
|
|
145
|
+
return { ok: true, path };
|
|
146
|
+
if (existsSync(path))
|
|
147
|
+
return { ok: false, error: `"${safe}" already exists` };
|
|
148
|
+
try {
|
|
149
|
+
for (const s of DB_SIDECARS) {
|
|
150
|
+
if (existsSync(oldPath + s))
|
|
151
|
+
renameSync(oldPath + s, path + s);
|
|
152
|
+
}
|
|
153
|
+
return { ok: true, path };
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
return { ok: false, error: e?.code === "EBUSY" || e?.code === "EPERM" ? "db is locked (a server has it open)" : String(e?.message ?? e) };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
export function deleteDb(dbPath) {
|
|
160
|
+
if (isDbInUse(dbPath))
|
|
161
|
+
return { ok: false, error: "db is in use — stop its server first ([x])" };
|
|
162
|
+
try {
|
|
163
|
+
for (const s of DB_SIDECARS) {
|
|
164
|
+
if (existsSync(dbPath + s))
|
|
165
|
+
rmSync(dbPath + s, { force: true });
|
|
166
|
+
}
|
|
167
|
+
return { ok: true };
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
return { ok: false, error: e?.code === "EBUSY" || e?.code === "EPERM" ? "db is locked (a server has it open)" : String(e?.message ?? e) };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// ─── pinned servers (~/.nodedex/tui-servers.json) ───────────────────────────
|
|
174
|
+
export function loadPins() {
|
|
175
|
+
try {
|
|
176
|
+
const raw = readFileSync(PINS_FILE, "utf8");
|
|
177
|
+
const arr = JSON.parse(raw);
|
|
178
|
+
return Array.isArray(arr) ? arr.filter((p) => p && typeof p.url === "string") : [];
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function savePins(pins) {
|
|
185
|
+
try {
|
|
186
|
+
mkdirSync(NODEDEX_HOME, { recursive: true });
|
|
187
|
+
writeFileSync(PINS_FILE, JSON.stringify(pins, null, 2));
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
/* best-effort; a read-only home just means no persistence */
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
export function addPin(url, name) {
|
|
194
|
+
const u = url.replace(/\/$/, "");
|
|
195
|
+
const pins = loadPins().filter((p) => p.url.replace(/\/$/, "") !== u);
|
|
196
|
+
pins.push({ url: u, name: name?.trim() || undefined });
|
|
197
|
+
savePins(pins);
|
|
198
|
+
}
|
|
199
|
+
export function namePin(url, name) {
|
|
200
|
+
const u = url.replace(/\/$/, "");
|
|
201
|
+
const pins = loadPins();
|
|
202
|
+
const hit = pins.find((p) => p.url.replace(/\/$/, "") === u);
|
|
203
|
+
if (hit)
|
|
204
|
+
hit.name = name.trim() || undefined;
|
|
205
|
+
else
|
|
206
|
+
pins.push({ url: u, name: name.trim() || undefined });
|
|
207
|
+
savePins(pins);
|
|
208
|
+
}
|
|
209
|
+
function loadSession() {
|
|
210
|
+
try {
|
|
211
|
+
const r = JSON.parse(readFileSync(SESSION_FILE, "utf8"));
|
|
212
|
+
// back-compat with the OLD single-server format ({url, port, dbPath, managed})
|
|
213
|
+
if (r && typeof r.url === "string") {
|
|
214
|
+
return { managed: r.managed && r.port && r.dbPath ? [{ port: r.port, dbPath: r.dbPath }] : [], connected: r };
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
managed: Array.isArray(r?.managed) ? r.managed.filter((m) => m && typeof m.port === "number" && m.dbPath) : [],
|
|
218
|
+
connected: r?.connected,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return { managed: [] };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function saveSession(s) {
|
|
226
|
+
try {
|
|
227
|
+
mkdirSync(NODEDEX_HOME, { recursive: true });
|
|
228
|
+
writeFileSync(SESSION_FILE, JSON.stringify(s, null, 2));
|
|
229
|
+
}
|
|
230
|
+
catch { /* best-effort; read-only home just means no restore */ }
|
|
231
|
+
}
|
|
232
|
+
/** Snapshot the CURRENTLY-managed servers into the session. Called on launch + stop — NOT
|
|
233
|
+
* on child-exit, so a quit (which kills every child) can't wipe the list it should restore. */
|
|
234
|
+
function persistManagedSnapshot() {
|
|
235
|
+
const list = [...managed.values()].map((m) => ({ port: m.port, dbPath: m.dbPath, bindHost: m.bindHost, token: m.token }));
|
|
236
|
+
saveSession({ ...loadSession(), managed: list });
|
|
237
|
+
}
|
|
238
|
+
/** Record which server the user is focused on (the one to reconnect to first on restart). */
|
|
239
|
+
export function saveLastServer(rec) {
|
|
240
|
+
saveSession({ ...loadSession(), connected: rec });
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Restore the session on startup: relaunch EVERY saved managed server that isn't already up,
|
|
244
|
+
* then reconnect to the focused one (or the first managed). Returns the URL to connect to,
|
|
245
|
+
* or null. We never resurrect an external (non-managed) url — only ones we have a db path for.
|
|
246
|
+
*/
|
|
247
|
+
export async function restoreSession() {
|
|
248
|
+
const s = loadSession();
|
|
249
|
+
for (const m of s.managed) {
|
|
250
|
+
if (!existsSync(m.dbPath))
|
|
251
|
+
continue; // db moved/deleted → skip
|
|
252
|
+
if (managed.has(candidateUrl(m.port)))
|
|
253
|
+
continue; // we already launched it
|
|
254
|
+
if ((await probeServer(candidateUrl(m.port))).up)
|
|
255
|
+
continue; // something already there
|
|
256
|
+
launchServer({ port: m.port, dbPath: m.dbPath, bindHost: m.bindHost, token: m.token });
|
|
257
|
+
}
|
|
258
|
+
// Prefer the focused server — but ONLY if it's actually reachable. A stale `connected`
|
|
259
|
+
// pointer (e.g. onboarding just launched a server on a NEW port, leaving the old one dead)
|
|
260
|
+
// must NOT strand the TUI on a dead url, so fall back to a live MANAGED server and adopt it
|
|
261
|
+
// as the focus so the next restart is clean.
|
|
262
|
+
const connected = s.connected?.url ?? null;
|
|
263
|
+
if (!connected && s.managed.length === 0)
|
|
264
|
+
return null;
|
|
265
|
+
const deadline = Date.now() + 12000;
|
|
266
|
+
while (Date.now() < deadline) {
|
|
267
|
+
if (connected && (await probeServer(connected)).up)
|
|
268
|
+
return connected;
|
|
269
|
+
for (const m of s.managed) {
|
|
270
|
+
const url = candidateUrl(m.port);
|
|
271
|
+
if ((await probeServer(url)).up) {
|
|
272
|
+
if (url !== connected)
|
|
273
|
+
saveLastServer({ url, port: m.port, dbPath: m.dbPath, managed: true });
|
|
274
|
+
return url;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
await new Promise((res) => setTimeout(res, 300));
|
|
278
|
+
}
|
|
279
|
+
return connected ?? (s.managed[0] ? candidateUrl(s.managed[0].port) : null); // hand back; poll shows state
|
|
280
|
+
}
|
|
281
|
+
const managed = new Map(); // keyed by normalized url
|
|
282
|
+
/** The DB path a managed server was launched with (for last-session restore). */
|
|
283
|
+
export function managedDbPath(url) {
|
|
284
|
+
return managed.get(norm(url))?.dbPath;
|
|
285
|
+
}
|
|
286
|
+
const norm = (url) => prefer127(url.replace(/\/$/, ""));
|
|
287
|
+
const portOf = (url) => {
|
|
288
|
+
const m = url.match(/:(\d+)/);
|
|
289
|
+
return m ? Number(m[1]) : null;
|
|
290
|
+
};
|
|
291
|
+
// ─── discovery ───────────────────────────────────────────────────────────────
|
|
292
|
+
// Probe candidate ports + pinned urls in parallel; merge with managed state.
|
|
293
|
+
export async function discover() {
|
|
294
|
+
const pins = loadPins();
|
|
295
|
+
const urls = new Set([
|
|
296
|
+
...CANDIDATE_PORTS.map(candidateUrl),
|
|
297
|
+
...pins.map((p) => norm(p.url)),
|
|
298
|
+
...managed.keys(),
|
|
299
|
+
]);
|
|
300
|
+
const nameFor = (u) => pins.find((p) => norm(p.url) === u)?.name;
|
|
301
|
+
const entries = await Promise.all([...urls].map(async (url) => {
|
|
302
|
+
const r = await probeServer(url);
|
|
303
|
+
return {
|
|
304
|
+
url,
|
|
305
|
+
port: portOf(url),
|
|
306
|
+
name: nameFor(url),
|
|
307
|
+
up: r.up,
|
|
308
|
+
db: r.db,
|
|
309
|
+
blocks: r.blocks,
|
|
310
|
+
managed: managed.has(url),
|
|
311
|
+
};
|
|
312
|
+
}));
|
|
313
|
+
// up first, then by port
|
|
314
|
+
return entries.sort((a, b) => Number(b.up) - Number(a.up) || (a.port ?? 0) - (b.port ?? 0));
|
|
315
|
+
}
|
|
316
|
+
// ─── free-port scan ──────────────────────────────────────────────────────────
|
|
317
|
+
// Return ports that are actually BINDABLE (can host a new server) — not merely "no
|
|
318
|
+
// Nodedex responding". A port held by ANY process (the user's ":3001 always used up")
|
|
319
|
+
// fails the bind, so it won't be suggested. Used by onboarding + the launch flow so the
|
|
320
|
+
// user never has to guess. Momentary bind + close; the launch-fails-loudly path covers
|
|
321
|
+
// the small TOCTOU race if something grabs the port in between.
|
|
322
|
+
function portBindable(port, timeoutMs = 600) {
|
|
323
|
+
return new Promise((resolve) => {
|
|
324
|
+
const srv = createServer();
|
|
325
|
+
let settled = false;
|
|
326
|
+
let timer;
|
|
327
|
+
const done = (free) => {
|
|
328
|
+
if (settled)
|
|
329
|
+
return;
|
|
330
|
+
settled = true;
|
|
331
|
+
if (timer)
|
|
332
|
+
clearTimeout(timer); // don't leave a stray timer per scan
|
|
333
|
+
try {
|
|
334
|
+
srv.close();
|
|
335
|
+
}
|
|
336
|
+
catch { /* */ }
|
|
337
|
+
resolve(free);
|
|
338
|
+
};
|
|
339
|
+
srv.once("error", () => done(false)); // EADDRINUSE / EACCES → not free
|
|
340
|
+
srv.once("listening", () => done(true)); // bound ok → free
|
|
341
|
+
try {
|
|
342
|
+
srv.listen(port, "127.0.0.1");
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
done(false);
|
|
346
|
+
}
|
|
347
|
+
timer = setTimeout(() => done(false), timeoutMs);
|
|
348
|
+
timer.unref?.(); // never keep the event loop alive on this
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
export async function scanFreePorts(range = [3001, 3002, 3003, 3004, 3005, 3006, 3007, 3008, 3009, 3099], max = 6) {
|
|
352
|
+
const free = [];
|
|
353
|
+
for (const p of range) {
|
|
354
|
+
// Free ⟺ bindable AND no Nodedex answering. The probe catches the Windows case
|
|
355
|
+
// where 127.0.0.1 binds OK even though a server holds the port on 0.0.0.0.
|
|
356
|
+
if (await portBindable(p) && !(await probeServer(candidateUrl(p))).up)
|
|
357
|
+
free.push(p);
|
|
358
|
+
if (free.length >= max)
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
return free;
|
|
362
|
+
}
|
|
363
|
+
/** Random secret for NODEDEX_API_TOKEN when launching a network-reachable (0.0.0.0) server. */
|
|
364
|
+
export function genToken() {
|
|
365
|
+
return randomBytes(18).toString("hex");
|
|
366
|
+
}
|
|
367
|
+
export function launchServer(opts) {
|
|
368
|
+
const url = candidateUrl(opts.port); // 127.0.0.1 — keep managed keys IPv4 like discovery
|
|
369
|
+
if (managed.has(url))
|
|
370
|
+
return { ok: false, url, error: "already launched by this TUI" };
|
|
371
|
+
if (!existsSync(SERVER_DIR))
|
|
372
|
+
return { ok: false, url, error: `server dir not found: ${SERVER_DIR}` };
|
|
373
|
+
try {
|
|
374
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
375
|
+
const logPath = resolve(LOG_DIR, `server-${opts.port}.log`);
|
|
376
|
+
const out = createWriteStream(logPath, { flags: "a" });
|
|
377
|
+
out.write(`\n=== launch ${new Date().toISOString()} port=${opts.port} db=${opts.dbPath} ===\n`);
|
|
378
|
+
// --env-file=.env is a DEV convenience (repo-local overrides). A fresh clone has
|
|
379
|
+
// NO repo .env — config comes from ~/.nodedex/.env (loaded by boot-env) + the env
|
|
380
|
+
// passed below. `node --env-file` HARD-FAILS if the file is missing, so only add it
|
|
381
|
+
// when it actually exists; otherwise a clean install can't launch at all.
|
|
382
|
+
// Dev repo (src present) runs source via tsx; the npm package ships dist only.
|
|
383
|
+
const nodeArgs = existsSync(resolve(SERVER_DIR, "src", "server.ts"))
|
|
384
|
+
? ["--import=tsx/esm", "src/server.ts"]
|
|
385
|
+
: ["dist/server.js"];
|
|
386
|
+
if (existsSync(resolve(SERVER_DIR, ".env")))
|
|
387
|
+
nodeArgs.unshift("--env-file=.env");
|
|
388
|
+
const homeEnv = readHomeEnv(); // persisted user settings (Settings → ~/.nodedex/.env)
|
|
389
|
+
const child = spawn(process.execPath, // the same node running the TUI
|
|
390
|
+
nodeArgs, {
|
|
391
|
+
cwd: SERVER_DIR,
|
|
392
|
+
env: {
|
|
393
|
+
...process.env,
|
|
394
|
+
// OpenRouter key/provider from ~/.nodedex/config.json (onboarding). Empty
|
|
395
|
+
// when un-configured. WINS over .env (node --env-file won't override set vars).
|
|
396
|
+
...providerEnv(),
|
|
397
|
+
PORT: String(opts.port),
|
|
398
|
+
WORKSPACE_DB_PATH: opts.dbPath,
|
|
399
|
+
// Docker/remote launch: bind all interfaces + require a token (set by the launcher
|
|
400
|
+
// when the user picks "agent in a container / on another machine"). Omitted →
|
|
401
|
+
// the server's default localhost bind, no token.
|
|
402
|
+
...(opts.bindHost ? { NODEDEX_BIND_HOST: opts.bindHost } : {}),
|
|
403
|
+
...(opts.token ? { NODEDEX_API_TOKEN: opts.token } : {}),
|
|
404
|
+
// FULL RUN: the maintenance workers (flag-reviewer, describer, schema-heal, …) are ON
|
|
405
|
+
// by default — that's the complete system the pipeline is designed to run with.
|
|
406
|
+
// flag-reviewer is set explicitly so the intent is legible (the gate treats anything
|
|
407
|
+
// ≠ "off" as on). OVERRIDABLE: an explicit env or the saved ~/.nodedex/.env wins, so a
|
|
408
|
+
// user who wants to cut the idle reviewer can set NODEDEX_FLAG_REVIEWER_ENABLED=off.
|
|
409
|
+
NODEDEX_FLAG_REVIEWER_ENABLED: process.env.NODEDEX_FLAG_REVIEWER_ENABLED ?? homeEnv.NODEDEX_FLAG_REVIEWER_ENABLED ?? "on",
|
|
410
|
+
// Legacy per-turn inactivity-reflect stays OFF — arc mode uses NODEDEX_ARC_INACTIVITY_ENABLED.
|
|
411
|
+
NODEDEX_INACTIVITY_REFLECT: "0",
|
|
412
|
+
// v2 ARC EXTRACTION is the default pipeline (the validated one; per-turn scene-card is
|
|
413
|
+
// legacy). DEFAULTED here but OVERRIDABLE — an explicit env OR a saved ~/.nodedex/.env
|
|
414
|
+
// value wins (`process.env ?? homeEnv ?? default`), so a user's Settings choice SURVIVES
|
|
415
|
+
// a relaunch. Arc captures turns for batched extraction; the triggers that commit them:
|
|
416
|
+
// · ARC_AUTO_TURNS=N → auto-extract every N captured turns (0 = off; user-settable in Settings)
|
|
417
|
+
// · ARC_INACTIVITY → auto-extract after the conversation goes idle (last-resort sweep)
|
|
418
|
+
// (the agent can still fire workspace_extract_arc sooner at its own task boundaries).
|
|
419
|
+
NODEDEX_ARC_EXTRACTION: process.env.NODEDEX_ARC_EXTRACTION ?? homeEnv.NODEDEX_ARC_EXTRACTION ?? "1",
|
|
420
|
+
NODEDEX_ARC_INACTIVITY_ENABLED: process.env.NODEDEX_ARC_INACTIVITY_ENABLED ?? homeEnv.NODEDEX_ARC_INACTIVITY_ENABLED ?? "on",
|
|
421
|
+
NODEDEX_ARC_AUTO_TURNS: process.env.NODEDEX_ARC_AUTO_TURNS ?? homeEnv.NODEDEX_ARC_AUTO_TURNS ?? "8",
|
|
422
|
+
},
|
|
423
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
424
|
+
windowsHide: true,
|
|
425
|
+
});
|
|
426
|
+
child.stdout?.pipe(out);
|
|
427
|
+
child.stderr?.pipe(out);
|
|
428
|
+
child.on("exit", () => managed.delete(url));
|
|
429
|
+
managed.set(url, { child, logPath, port: opts.port, dbPath: opts.dbPath, bindHost: opts.bindHost, token: opts.token });
|
|
430
|
+
if (opts.token)
|
|
431
|
+
registerToken(url, opts.token); // so the TUI's own reads authenticate
|
|
432
|
+
if (opts.name)
|
|
433
|
+
addPin(url, opts.name);
|
|
434
|
+
// Track it among the servers to restore next start (the WHOLE managed set, not just one).
|
|
435
|
+
persistManagedSnapshot();
|
|
436
|
+
return { ok: true, url, logPath };
|
|
437
|
+
}
|
|
438
|
+
catch (e) {
|
|
439
|
+
return { ok: false, url, error: String(e) };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// ─── stop (managed only) ──────────────────────────────────────────────────────
|
|
443
|
+
export function stopServer(url) {
|
|
444
|
+
const m = managed.get(norm(url));
|
|
445
|
+
if (!m)
|
|
446
|
+
return { ok: false, error: "not managed by this TUI — won't kill a server it didn't launch" };
|
|
447
|
+
killChild(m);
|
|
448
|
+
managed.delete(norm(url));
|
|
449
|
+
persistManagedSnapshot(); // explicit stop → drop it from the restore set
|
|
450
|
+
return { ok: true };
|
|
451
|
+
}
|
|
452
|
+
export function isManaged(url) {
|
|
453
|
+
return managed.has(norm(url));
|
|
454
|
+
}
|
|
455
|
+
// ─── swap: run a DIFFERENT db on the SAME port ──────────────────────────────
|
|
456
|
+
// "a single port that can run different db at a time" — stop the managed server
|
|
457
|
+
// on this port, wait for the port to actually release (polling, not a fixed
|
|
458
|
+
// guess — the OS frees it in a few hundred ms but timing varies), then relaunch
|
|
459
|
+
// it on the new db. Only valid for a TUI-managed port.
|
|
460
|
+
export async function swapDb(url, port, dbPath) {
|
|
461
|
+
if (!managed.has(norm(url)))
|
|
462
|
+
return { ok: false, url, error: "not managed by this TUI" };
|
|
463
|
+
stopServer(url);
|
|
464
|
+
const deadline = Date.now() + 8000;
|
|
465
|
+
while (Date.now() < deadline) {
|
|
466
|
+
const r = await probeServer(candidateUrl(port));
|
|
467
|
+
if (!r.up)
|
|
468
|
+
break;
|
|
469
|
+
await new Promise((res) => setTimeout(res, 250));
|
|
470
|
+
}
|
|
471
|
+
return launchServer({ port, dbPath });
|
|
472
|
+
}
|
|
473
|
+
function killChild(m) {
|
|
474
|
+
try {
|
|
475
|
+
if (process.platform === "win32") {
|
|
476
|
+
// tree-kill: --import=tsx/esm runs in-process, but be safe against children
|
|
477
|
+
spawn("taskkill", ["/PID", String(m.child.pid), "/T", "/F"], { windowsHide: true });
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
m.child.kill("SIGTERM");
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
/* already gone */
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// ─── Capture watchers (one per host — thin shims over nodedex-capture-core) ─
|
|
488
|
+
// Each watcher reads its host's own turn store read-only (Hermes: state.db; Claude Code:
|
|
489
|
+
// ~/.claude/projects/*.jsonl) and posts turns to the live server. The TUI owns their
|
|
490
|
+
// lifecycle so the user controls them from Settings (toggle = start/stop). Each watcher's
|
|
491
|
+
// config lives in ~/.nodedex/config.json and is re-read by the watcher each poll — so
|
|
492
|
+
// editing a filter in the TUI applies WITHOUT a restart here. Default host stays "hermes"
|
|
493
|
+
// for backward-compatible call sites.
|
|
494
|
+
const WATCHER_DEFS = {
|
|
495
|
+
hermes: { script: "hermes-statedb-watcher.mjs", log: "hermes-watcher.log" },
|
|
496
|
+
"claude-code": { script: "claude-code-watcher.mjs", log: "claude-code-watcher.log" },
|
|
497
|
+
};
|
|
498
|
+
export const WATCHER_HOSTS = Object.keys(WATCHER_DEFS);
|
|
499
|
+
const watchers = new Map();
|
|
500
|
+
export function isWatcherRunning(host = "hermes") {
|
|
501
|
+
const w = watchers.get(host);
|
|
502
|
+
return !!w && w.exitCode === null && !w.killed;
|
|
503
|
+
}
|
|
504
|
+
export function launchWatcher(host = "hermes") {
|
|
505
|
+
if (isWatcherRunning(host))
|
|
506
|
+
return { ok: true };
|
|
507
|
+
const def = WATCHER_DEFS[host];
|
|
508
|
+
const script = resolve(SERVER_DIR, "adapters", def.script);
|
|
509
|
+
if (!existsSync(script))
|
|
510
|
+
return { ok: false, error: `watcher not found: ${script}` };
|
|
511
|
+
try {
|
|
512
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
513
|
+
const out = createWriteStream(resolve(LOG_DIR, def.log), { flags: "a" });
|
|
514
|
+
out.write(`\n=== watcher launch ${new Date().toISOString()} ===\n`);
|
|
515
|
+
// cwd=SERVER_DIR so a watcher's createRequire resolves better-sqlite3 from server/node_modules.
|
|
516
|
+
const child = spawn(process.execPath, [script], {
|
|
517
|
+
cwd: SERVER_DIR,
|
|
518
|
+
env: { ...process.env },
|
|
519
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
520
|
+
windowsHide: true,
|
|
521
|
+
});
|
|
522
|
+
child.stdout?.pipe(out);
|
|
523
|
+
child.stderr?.pipe(out);
|
|
524
|
+
child.on("exit", () => { if (watchers.get(host) === child)
|
|
525
|
+
watchers.delete(host); });
|
|
526
|
+
watchers.set(host, child);
|
|
527
|
+
return { ok: true };
|
|
528
|
+
}
|
|
529
|
+
catch (e) {
|
|
530
|
+
return { ok: false, error: String(e) };
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
export function stopWatcher(host) {
|
|
534
|
+
const hosts = host ? [host] : [...watchers.keys()];
|
|
535
|
+
for (const h of hosts) {
|
|
536
|
+
const w = watchers.get(h);
|
|
537
|
+
if (!w)
|
|
538
|
+
continue;
|
|
539
|
+
try {
|
|
540
|
+
if (process.platform === "win32")
|
|
541
|
+
spawn("taskkill", ["/PID", String(w.pid), "/T", "/F"], { windowsHide: true });
|
|
542
|
+
else
|
|
543
|
+
w.kill("SIGTERM");
|
|
544
|
+
}
|
|
545
|
+
catch { /* already gone */ }
|
|
546
|
+
watchers.delete(h);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// kill everything the TUI launched (call on app exit)
|
|
550
|
+
export function killAllManaged() {
|
|
551
|
+
for (const m of managed.values())
|
|
552
|
+
killChild(m);
|
|
553
|
+
managed.clear();
|
|
554
|
+
stopWatcher();
|
|
555
|
+
}
|
|
556
|
+
export { LOG_DIR, SERVER_DIR };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { PassThrough } from "node:stream";
|
|
3
|
+
import { render } from "ink";
|
|
4
|
+
import { App } from "./App.js";
|
|
5
|
+
import { killAllManaged } from "./servers.js";
|
|
6
|
+
const stdin = new PassThrough();
|
|
7
|
+
const { unmount, waitUntilExit } = render(_jsx(App, {}), { stdin: stdin, exitOnCtrlC: false });
|
|
8
|
+
setTimeout(() => unmount(), 3500);
|
|
9
|
+
await waitUntilExit();
|
|
10
|
+
console.log("\n[smoke] rendered + unmounted cleanly");
|
|
11
|
+
// The App spawns watcher children on mount (capture default-on); their piped
|
|
12
|
+
// stdio keeps the event loop alive after unmount → the smoke hangs. Real runs
|
|
13
|
+
// clean up in cli.tsx's exit handler; the harness must do it itself.
|
|
14
|
+
killAllManaged();
|
|
15
|
+
process.exit(0);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// theme.ts — colors, type-colors, glyphs, and small formatting helpers.
|
|
2
|
+
// One place to retune the look.
|
|
3
|
+
//
|
|
4
|
+
// DESIGN (TUI v3, 2026-07-02): cool + casual. A Nord-derived palette — slate
|
|
5
|
+
// chrome, one soft frost-blue accent, and MUTED aurora colors reserved for the
|
|
6
|
+
// block types. The chrome stays quiet on purpose: the memory (typed blocks)
|
|
7
|
+
// carries almost all of the color, so the type system itself is the visual
|
|
8
|
+
// language (decision=green, dead_end=red, …) without anything reading neon.
|
|
9
|
+
export const theme = {
|
|
10
|
+
accent: "#88C0D0", // frost blue — selection, keys, wordmark, active view
|
|
11
|
+
title: "#81A1C1", // section headers (calm blue, one step dimmer than accent)
|
|
12
|
+
label: "#7B88A1", // field labels
|
|
13
|
+
value: "#E5E9F0", // field values (soft snow-white)
|
|
14
|
+
border: "#4C566A", // panel borders (idle)
|
|
15
|
+
borderHot: "#88C0D0", // panel borders (attention)
|
|
16
|
+
ok: "#A3BE8C",
|
|
17
|
+
warn: "#EBCB8B",
|
|
18
|
+
danger: "#BF616A",
|
|
19
|
+
dim: "#616E88",
|
|
20
|
+
};
|
|
21
|
+
// Block type → color, keyed to the stance/role the type carries (muted aurora).
|
|
22
|
+
export const typeColor = {
|
|
23
|
+
decision: "#A3BE8C",
|
|
24
|
+
dead_end: "#BF616A",
|
|
25
|
+
constraint: "#EBCB8B",
|
|
26
|
+
insight: "#B48EAD",
|
|
27
|
+
hypothesis: "#88C0D0",
|
|
28
|
+
fact: "#81A1C1",
|
|
29
|
+
blueprint: "#5E81AC",
|
|
30
|
+
preference: "#D08770",
|
|
31
|
+
artifact: "#D8DEE9",
|
|
32
|
+
task: "#8FBCBB",
|
|
33
|
+
project: "#ECEFF4",
|
|
34
|
+
process: "#616E88",
|
|
35
|
+
chain: "#D08770",
|
|
36
|
+
question: "#EBCB8B",
|
|
37
|
+
event: "#7B88A1",
|
|
38
|
+
};
|
|
39
|
+
export const glyph = {
|
|
40
|
+
up: "●",
|
|
41
|
+
paused: "‖",
|
|
42
|
+
down: "●",
|
|
43
|
+
read: "▸",
|
|
44
|
+
chain: "⛓",
|
|
45
|
+
tree: "⌂",
|
|
46
|
+
save: "+",
|
|
47
|
+
block: "▰",
|
|
48
|
+
flag: "⚑",
|
|
49
|
+
warn: "⚠",
|
|
50
|
+
okMark: "✓",
|
|
51
|
+
arrow: "›",
|
|
52
|
+
tick: "⟳",
|
|
53
|
+
};
|
|
54
|
+
export function typeColorOf(type) {
|
|
55
|
+
return (type && typeColor[type]) || theme.value;
|
|
56
|
+
}
|
|
57
|
+
// One symbol per type so block lists scan without reading words.
|
|
58
|
+
export const typeGlyph = {
|
|
59
|
+
constraint: "▣",
|
|
60
|
+
decision: "◆",
|
|
61
|
+
dead_end: "✕",
|
|
62
|
+
preference: "★",
|
|
63
|
+
blueprint: "◇",
|
|
64
|
+
question: "?",
|
|
65
|
+
hypothesis: "≈",
|
|
66
|
+
task: "▸",
|
|
67
|
+
fact: "▰",
|
|
68
|
+
insight: "◐",
|
|
69
|
+
event: "•",
|
|
70
|
+
chain: "⛓",
|
|
71
|
+
project: "⌂",
|
|
72
|
+
};
|
|
73
|
+
export function typeGlyphOf(type) {
|
|
74
|
+
return (type && typeGlyph[type]) || "▪";
|
|
75
|
+
}
|
|
76
|
+
export function relTime(iso) {
|
|
77
|
+
if (!iso)
|
|
78
|
+
return "";
|
|
79
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
80
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
81
|
+
return "now";
|
|
82
|
+
const s = Math.floor(ms / 1000);
|
|
83
|
+
if (s < 60)
|
|
84
|
+
return `${s}s`;
|
|
85
|
+
const m = Math.floor(s / 60);
|
|
86
|
+
if (m < 60)
|
|
87
|
+
return `${m}m`;
|
|
88
|
+
const h = Math.floor(m / 60);
|
|
89
|
+
if (h < 24)
|
|
90
|
+
return `${h}h`;
|
|
91
|
+
return `${Math.floor(h / 24)}d`;
|
|
92
|
+
}
|
|
93
|
+
export function fmtNum(n) {
|
|
94
|
+
if (typeof n !== "number" || !Number.isFinite(n))
|
|
95
|
+
return "—";
|
|
96
|
+
return n.toLocaleString("en-US");
|
|
97
|
+
}
|
|
98
|
+
export function fmtMoney(n) {
|
|
99
|
+
if (typeof n !== "number" || !Number.isFinite(n))
|
|
100
|
+
return "—";
|
|
101
|
+
return `$${n.toFixed(2)}`;
|
|
102
|
+
}
|
|
103
|
+
export function trunc(s, n) {
|
|
104
|
+
const str = (s ?? "").replace(/\s+/g, " ").trim();
|
|
105
|
+
return str.length > n ? str.slice(0, n - 1) + "…" : str;
|
|
106
|
+
}
|