heyspex 0.1.0 → 0.2.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/README.md +21 -0
- package/bin/hs.mjs +931 -98
- package/bin/spexcode.mjs +262 -0
- package/dev-factory/engine/worktree.mjs +740 -0
- package/dev-factory/lib/code-repo-root.mjs +67 -0
- package/dev-factory/lib/context-budget.mjs +84 -0
- package/dev-factory/lib/epic-detect.mjs +190 -0
- package/dev-factory/lib/fleet-queue-file.mjs +846 -0
- package/dev-factory/lib/fleet-queue.mjs +426 -0
- package/dev-factory/lib/fleet-state.mjs +14 -18
- package/dev-factory/lib/model-catalog.mjs +338 -0
- package/dev-factory/lib/seat-context.mjs +199 -0
- package/dev-factory/lib/spec-io.mjs +93 -0
- package/dev-factory/lib/sufficiency-score.mjs +8 -2
- package/dev-factory/lib/worktree-allocator.mjs +744 -0
- package/dev-factory/model-catalog.json +222 -0
- package/dist/cli/hs-check.mjs +1085 -103
- package/dist/cli/hs-claim.mjs +936 -63
- package/dist/cli/hs-connect.mjs +9786 -1910
- package/dist/cli/hs-defer.mjs +5267 -3053
- package/dist/cli/hs-dev.mjs +564 -0
- package/dist/cli/hs-env.mjs +385 -299
- package/dist/cli/hs-index-rebuild.mjs +7129 -0
- package/dist/cli/hs-index-reconcile.mjs +7852 -0
- package/dist/cli/hs-init.mjs +777 -138
- package/dist/cli/hs-install.mjs +6583 -485
- package/dist/cli/hs-join.mjs +10137 -2368
- package/dist/cli/hs-vault-pull.mjs +4659 -5882
- package/dist/cli/hs-vault.mjs +3793 -1537
- package/dist/cli/hs-workspace.mjs +2550 -524
- package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
- package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
- package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
- package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
- package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
- package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
- package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
- package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
- package/dist/cli-templates/harness-commands/claude/hs-ground.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
- package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
- package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
- package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
- package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-view.md +26 -0
- package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
- package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
- package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
- package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
- package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
- package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
- package/dist/cli-templates/harness-commands/cursor/hs-ground.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
- package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
- package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
- package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
- package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-view.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
- package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
- package/docs/mcp-client-config.md +259 -6
- package/package.json +231 -124
- package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
- package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
- package/plugins/heyspex/.mcp.json +12 -0
- package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
- package/plugins/heyspex/bin/run-hook.mjs +62 -0
- package/plugins/heyspex/hooks/cursor-hooks.json +227 -0
- package/plugins/heyspex/hooks/hooks.json +361 -0
- package/plugins/heyspex/mcp.json +13 -0
- package/scripts/check-mcp-freshness.mjs +17 -9
- package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
- package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
- package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
- package/scripts/claude-hooks/_intent-classifier-router.mjs +410 -0
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +351 -0
- package/scripts/claude-hooks/_run.mjs +120 -0
- package/scripts/claude-hooks/_transcript-text.mjs +60 -0
- package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
- package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
- package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
- package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
- package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
- package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
- package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
- package/scripts/claude-hooks/claim-liveness-shared.mjs +575 -0
- package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
- package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
- package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
- package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
- package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
- package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
- package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
- package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
- package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
- package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
- package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
- package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
- package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
- package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
- package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
- package/scripts/claude-hooks/inject-communication-style.mjs +66 -0
- package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
- package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
- package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
- package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
- package/scripts/claude-hooks/intent-classifier-trigger.mjs +247 -0
- package/scripts/claude-hooks/log-session-activity.mjs +341 -0
- package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
- package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
- package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
- package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
- package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
- package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
- package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
- package/scripts/claude-hooks/research-method-guard.mjs +398 -0
- package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
- package/scripts/claude-hooks/session-end.mjs +449 -0
- package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
- package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
- package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
- package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
- package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +298 -0
- package/scripts/cursor-hooks/manifest.mjs +235 -0
- package/scripts/dev-server-status.mjs +34 -13
- package/scripts/dev-server-stop.mjs +313 -0
- package/scripts/heyspex-mode.mjs +268 -0
- package/scripts/hs-auto-resolve.mjs +240 -0
- package/scripts/hs-auto-source.mjs +398 -0
- package/scripts/hs-auto.mjs +134 -0
- package/scripts/hs-capture-cli.mjs +326 -0
- package/scripts/hs-classifier.mjs +136 -0
- package/scripts/hs-companion.mjs +88 -0
- package/scripts/hs-gateway.mjs +115 -0
- package/scripts/hs-mode.mjs +125 -0
- package/scripts/hs-next.mjs +441 -0
- package/scripts/hs-settings.mjs +338 -0
- package/scripts/hs-view.mjs +333 -0
- package/scripts/hs-worktree.mjs +201 -0
- package/scripts/install-claude-code-hooks.mjs +270 -0
- package/scripts/install-cursor-hooks.mjs +471 -0
- package/scripts/lib/agent-env-markers.mjs +38 -0
- package/scripts/lib/console-url-resolver.mjs +197 -60
- package/scripts/lib/customer-kit.mjs +886 -0
- package/scripts/lib/dev-mode.mjs +244 -28
- package/scripts/lib/heyspex-home.mjs +72 -0
- package/scripts/lib/hs-command.mjs +277 -0
- package/scripts/lib/hs-companion-settings.mjs +472 -0
- package/scripts/lib/hs-source-commit.mjs +59 -5
- package/scripts/lib/hs-source-identity.mjs +526 -0
- package/scripts/lib/hs-source-log.mjs +11 -21
- package/scripts/lib/hs-source-push.mjs +107 -58
- package/scripts/lib/hs-worktree-args.mjs +115 -0
- package/scripts/lib/shared-path-helpers.mjs +134 -0
- package/scripts/lib/standalone-doctor.mjs +540 -3
- package/scripts/lib/subagent-git-lock.mjs +109 -0
- package/scripts/lib/template-pin.mjs +34 -0
- package/scripts/provenance-lib.mjs +91 -0
- package/scripts/stamp-provenance-on-edit.mjs +222 -0
- package/scripts/wip-ledger-health.mjs +653 -0
- package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
- package/spexharness/src/adapters/audit.mjs +197 -0
- package/spexharness/src/adapters/claude-code.mjs +441 -0
- package/spexharness/src/chat/tui/ansi.mjs +448 -0
- package/spexharness/src/chat/tui/palette.mjs +22 -0
- package/spexharness/src/chat/tui/select.mjs +208 -0
- package/spexharness/src/config-root.mjs +54 -0
- package/spexharness/src/context-budget.mjs +141 -0
- package/spexharness/src/dev-server-control.mjs +389 -0
- package/spexharness/src/doctrine/behaviour.mjs +371 -0
- package/spexharness/src/doctrine/corpus.mjs +733 -0
- package/spexharness/src/doctrine/inject.mjs +338 -0
- package/spexharness/src/doctrine/merge.mjs +184 -0
- package/spexharness/src/doctrine/sources.mjs +252 -0
- package/spexharness/src/doctrine/synthesize.mjs +401 -0
- package/spexharness/src/doctrine/vault-source.mjs +574 -0
- package/spexharness/src/doctrine/writer.mjs +176 -0
- package/spexharness/src/events.mjs +84 -0
- package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
- package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
- package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
- package/spexharness/src/gates/deep-research-cost-gate.mjs +274 -0
- package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
- package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
- package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
- package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
- package/spexharness/src/gates/git-identity-guard.mjs +61 -0
- package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
- package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
- package/spexharness/src/gates/git-stash-gate.mjs +159 -0
- package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
- package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
- package/spexharness/src/gates/inject-vault-context.mjs +239 -0
- package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
- package/spexharness/src/gates/plan-review-gate.mjs +233 -0
- package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
- package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
- package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
- package/spexharness/src/gates/settings-write-gate.mjs +104 -0
- package/spexharness/src/git-reflog-audit.mjs +294 -0
- package/spexharness/src/intent-classifier.mjs +194 -0
- package/spexharness/src/intent-directive.mjs +541 -0
- package/spexharness/src/profile/learn.mjs +551 -0
- package/spexharness/src/profile/proposals.mjs +357 -0
- package/spexharness/src/profile/vault-inclusion.mjs +386 -0
- package/spexharness/src/redact.mjs +151 -0
- package/spexharness/src/rule-table.mjs +819 -0
- package/spexharness/src/seat-context.mjs +122 -0
- package/spexharness/src/session-bootstrap.mjs +512 -0
- package/spexharness/src/session-record.mjs +815 -0
- package/spexharness/src/token-inventory.mjs +207 -0
- package/vendor/heyspex-mcp/account-switch.mjs +95 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
- package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
- package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +46 -5
- package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
- package/vendor/heyspex-mcp/capture-console-client.mjs +80 -1
- package/vendor/heyspex-mcp/capture-focus.mjs +1035 -29
- package/vendor/heyspex-mcp/capture-outbox.mjs +5 -1
- package/vendor/heyspex-mcp/capture-tools.mjs +1219 -0
- package/vendor/heyspex-mcp/claim-tools.mjs +1124 -0
- package/vendor/heyspex-mcp/cli-verbs.mjs +2076 -0
- package/vendor/heyspex-mcp/cli.mjs +201 -15524
- package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +412 -0
- package/vendor/heyspex-mcp/config-seed.yaml +15 -0
- package/vendor/heyspex-mcp/config-settings.mjs +290 -7
- package/vendor/heyspex-mcp/console-fetch.mjs +657 -16
- package/vendor/heyspex-mcp/console-transport.mjs +166 -0
- package/vendor/heyspex-mcp/credentials.mjs +584 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
- package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
- package/vendor/heyspex-mcp/design-tools.mjs +4 -1
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
- package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
- package/vendor/heyspex-mcp/dev-tools.mjs +124 -0
- package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
- package/vendor/heyspex-mcp/events-tools.mjs +207 -0
- package/vendor/heyspex-mcp/filing-tools.mjs +688 -0
- package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +69 -14
- package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
- package/vendor/heyspex-mcp/folder-trust-store.mjs +59 -0
- package/vendor/heyspex-mcp/http-transport.mjs +300 -0
- package/vendor/heyspex-mcp/list-tools.mjs +257 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
- package/vendor/heyspex-mcp/mcp-config.mjs +94 -0
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
- package/vendor/heyspex-mcp/occupied-keys.mjs +141 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +3360 -0
- package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
- package/vendor/heyspex-mcp/package-root.mjs +32 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
- package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
- package/vendor/heyspex-mcp/plan-push.mjs +906 -21
- package/vendor/heyspex-mcp/plan-review.mjs +124 -30
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-handlers.mjs +325 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +540 -351
- package/vendor/heyspex-mcp/promote-capture.mjs +386 -69
- package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
- package/vendor/heyspex-mcp/promote-research.mjs +660 -53
- package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
- package/vendor/heyspex-mcp/prompts.mjs +1460 -0
- package/vendor/heyspex-mcp/read-path.mjs +29 -9
- package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
- package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
- package/vendor/heyspex-mcp/register-spec.mjs +266 -54
- package/vendor/heyspex-mcp/request-context.mjs +59 -0
- package/vendor/heyspex-mcp/research-focus.mjs +207 -4
- package/vendor/heyspex-mcp/research-handlers.mjs +623 -0
- package/vendor/heyspex-mcp/research-tools.mjs +4 -4
- package/vendor/heyspex-mcp/response-budget.mjs +348 -0
- package/vendor/heyspex-mcp/review-focus.mjs +25 -1
- package/vendor/heyspex-mcp/search-specs.mjs +280 -0
- package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
- package/vendor/heyspex-mcp/session-owner.mjs +108 -0
- package/vendor/heyspex-mcp/session-tools.mjs +152 -0
- package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
- package/vendor/heyspex-mcp/spec-edit-tools.mjs +446 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
- package/vendor/heyspex-mcp/split-research.mjs +301 -56
- package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +132 -6
- package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
- package/vendor/heyspex-mcp/summary-write-tools.mjs +224 -0
- package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +108 -1
- package/vendor/heyspex-mcp/tool-catalog.mjs +2813 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +639 -0
- package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
- package/vendor/heyspex-mcp/vault-io.mjs +724 -0
- package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +366 -68
- package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
- package/vendor/heyspex-mcp/whoami.mjs +91 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hs companion settings — the CLI face of every Companion setting
|
|
3
|
+
* ([[HS-UDH1]] "every Companion setting and every console surface has three
|
|
4
|
+
* faces": settings screen, CLI, MCP).
|
|
5
|
+
*
|
|
6
|
+
* Talks to the SAME console endpoint the Settings > Companion screen uses
|
|
7
|
+
* (`GET`/`PUT /api/companion/settings`, see
|
|
8
|
+
* spex-console/src/app/api/companion/settings/route.ts) and sends the same
|
|
9
|
+
* partial-patch body the screen's `persist()` sends. Nothing here touches
|
|
10
|
+
* companion-settings.json directly — the console owns the file, its cache,
|
|
11
|
+
* and the normalisation.
|
|
12
|
+
*
|
|
13
|
+
* Transport: the shared vendor `makeConsoleFetch` (timeouts, loopback-host
|
|
14
|
+
* variants, dev-restart backoff, sign-in classification) bound to the ONE
|
|
15
|
+
* console-url resolver. Bearer ladder: `COMPANION_UI_TOKEN` env (the local
|
|
16
|
+
* route's optional loopback token) → the vendor `getAuthHeader` ladder
|
|
17
|
+
* (`HEYSPEX_MCP_TOKEN` env → `~/.heyspex/credentials.yaml` `mcp_token`).
|
|
18
|
+
*
|
|
19
|
+
* The console's store SILENTLY falls back to the previous value on a bad
|
|
20
|
+
* enum/type (sanitize() in companion-settings-store.ts — the route only
|
|
21
|
+
* 400s on a non-object body), so the CLI validates against the key table
|
|
22
|
+
* below BEFORE sending and then compares the echoed value with what it
|
|
23
|
+
* asked for; a console-side normalisation is reported, never hidden.
|
|
24
|
+
*
|
|
25
|
+
* Pure module: every function here is I/O-free except through the injected
|
|
26
|
+
* `consoleFetch`, so scripts/lib/hs-companion-settings.test.mjs can drive
|
|
27
|
+
* the whole thing with a fake. scripts/hs-companion.mjs is the process face.
|
|
28
|
+
*/
|
|
29
|
+
import process from "node:process";
|
|
30
|
+
|
|
31
|
+
import { getAuthHeader, makeConsoleFetch } from "../../vendor/heyspex-mcp/console-fetch.mjs";
|
|
32
|
+
import { resolveConsoleUrl } from "./console-url-resolver.mjs";
|
|
33
|
+
|
|
34
|
+
export const SETTINGS_PATH = "/api/companion/settings";
|
|
35
|
+
|
|
36
|
+
/** `spexCrawl.userAgentPreset` ids — mirrors SPEXCRAWL_UA_PRESETS in
|
|
37
|
+
* spex-console/src/lib/spexcrawl-user-agents.ts, plus the "custom" escape. */
|
|
38
|
+
const SPEXCRAWL_UA_IDS = [
|
|
39
|
+
"googlebot-desktop", "googlebot-smartphone", "googlebot-image", "googlebot-news",
|
|
40
|
+
"bingbot", "bingbot-mobile", "duckduckbot", "yandexbot", "baiduspider",
|
|
41
|
+
"chrome-desktop", "chrome-android", "safari-macos", "safari-ios",
|
|
42
|
+
"firefox-desktop", "edge-desktop", "spexcrawl", "custom",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* One row per settable leaf. Mirrors `CompanionSettings` +
|
|
47
|
+
* `DEFAULT_COMPANION_SETTINGS` in spex-console/src/lib/companion-settings-defaults.ts
|
|
48
|
+
* and the accept-rules in companion-settings-store.ts `sanitize()`. Nested
|
|
49
|
+
* objects are flattened to dotted keys; `set` re-nests them into the partial
|
|
50
|
+
* patch the store merges over the current value.
|
|
51
|
+
*
|
|
52
|
+
* type enum | boolean | integer | number | string | list
|
|
53
|
+
* values enum — accepted case-insensitively, sent in canonical case
|
|
54
|
+
* min/max integer / number range (inclusive)
|
|
55
|
+
* nullable `null` / `none` clears the value
|
|
56
|
+
* readOnly companion-owned telemetry; listed, never settable
|
|
57
|
+
*/
|
|
58
|
+
export const COMPANION_SETTING_KEYS = [
|
|
59
|
+
{ key: "speechEngine", type: "enum", values: ["localParakeet", "localWhisper", "http", "pulse"] },
|
|
60
|
+
{ key: "whisperModel", type: "enum", values: ["tiny.en", "base.en", "small.en"] },
|
|
61
|
+
{ key: "micDevice", type: "string", note: "display label; selection is by micDeviceId" },
|
|
62
|
+
{ key: "micDeviceId", type: "string", note: "capture device id (`list --json` shows devices)" },
|
|
63
|
+
{ key: "pttHotkey", type: "string", note: "chord, e.g. Alt+Space" },
|
|
64
|
+
{ key: "wakeWordEnabled", type: "boolean" },
|
|
65
|
+
{ key: "activationMode", type: "enum", values: ["auto", "desktop", "vr"] },
|
|
66
|
+
{ key: "vrPttHotkey", type: "string", note: "chord, or Mouse 4 / Mouse 5" },
|
|
67
|
+
{ key: "vrWakeWordEnabled", type: "boolean" },
|
|
68
|
+
{ key: "wakeWordThreshold", type: "number", min: 0, max: 1, note: "peak level treated as speech" },
|
|
69
|
+
{ key: "confirmBeforeCapture", type: "boolean" },
|
|
70
|
+
{ key: "orbVisibility", type: "enum", values: ["always", "while-listening", "hidden"] },
|
|
71
|
+
{ key: "orbSize", type: "enum", values: ["small", "medium", "large"] },
|
|
72
|
+
{ key: "theme", type: "enum", values: ["dark", "light", "system"] },
|
|
73
|
+
{ key: "voiceProfile.present", type: "boolean", readOnly: true },
|
|
74
|
+
{ key: "voiceProfile.lastCalibratedAt", type: "string", nullable: true, readOnly: true },
|
|
75
|
+
{ key: "soundCuesEnabled", type: "boolean" },
|
|
76
|
+
{ key: "soundCuesVolume", type: "integer", min: 0, max: 100 },
|
|
77
|
+
{ key: "orbPositionX", type: "number", nullable: true, note: "screen x; null = no default" },
|
|
78
|
+
{ key: "orbPositionY", type: "number", nullable: true, note: "screen y; null = no default" },
|
|
79
|
+
{ key: "orbPositionLocked", type: "boolean" },
|
|
80
|
+
{ key: "captureAutoLaunch.enabled", type: "boolean" },
|
|
81
|
+
{ key: "captureAutoLaunch.defaultAgent", type: "string", nullable: true, note: "model-registry entry id" },
|
|
82
|
+
{ key: "captureAutoLaunch.fallbackAgents", type: "list", note: "comma-separated entry ids" },
|
|
83
|
+
{ key: "spexCrawl.userAgentPreset", type: "enum", values: SPEXCRAWL_UA_IDS },
|
|
84
|
+
{ key: "spexCrawl.customUserAgent", type: "string", note: "used only when userAgentPreset=custom" },
|
|
85
|
+
{ key: "spexCrawl.respectRobotsTxt", type: "boolean" },
|
|
86
|
+
{ key: "spexCrawl.searxngBaseUrl", type: "string", nullable: true, note: "http(s) base URL of a SearXNG instance" },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Short verbs — each is `settings set <key> <value>` under the hood. A row
|
|
91
|
+
* with `value` takes no argument (`mute`); a row with `arg` takes one, and
|
|
92
|
+
* called WITHOUT it reads the key instead (`hs companion volume` prints the
|
|
93
|
+
* volume). Adding an alias is one line here; help and dispatch derive from
|
|
94
|
+
* this table.
|
|
95
|
+
*/
|
|
96
|
+
export const COMPANION_ALIASES = {
|
|
97
|
+
mute: { key: "soundCuesEnabled", value: "false", help: "Silence feedback cues" },
|
|
98
|
+
unmute: { key: "soundCuesEnabled", value: "true", help: "Feedback cues back on" },
|
|
99
|
+
volume: { key: "soundCuesVolume", arg: "<0-100>", help: "Feedback-cue loudness" },
|
|
100
|
+
orb: { key: "orbVisibility", arg: "always|while-listening|hidden", help: "Idle orb visibility" },
|
|
101
|
+
wake: { key: "wakeWordEnabled", arg: "on|off", help: "Wake-word listening" },
|
|
102
|
+
activation: { key: "activationMode", arg: "auto|desktop|vr", help: "Activation profile (desktop / VR goggle)" },
|
|
103
|
+
hotkey: { key: "pttHotkey", arg: "<chord>", help: "Push-to-talk chord, e.g. Alt+Space" },
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const HOST_COMMANDS = ["start", "stop", "status", "build"];
|
|
107
|
+
|
|
108
|
+
const TRUE_WORDS = new Set(["true", "on", "yes", "1", "enabled"]);
|
|
109
|
+
const FALSE_WORDS = new Set(["false", "off", "no", "0", "disabled"]);
|
|
110
|
+
const NULL_WORDS = new Set(["null", "none"]);
|
|
111
|
+
|
|
112
|
+
/** Find a key-table row by exact or case-insensitive name. */
|
|
113
|
+
export function findSettingKey(name) {
|
|
114
|
+
if (typeof name !== "string" || !name) return null;
|
|
115
|
+
const exact = COMPANION_SETTING_KEYS.find((k) => k.key === name);
|
|
116
|
+
if (exact) return exact;
|
|
117
|
+
const lower = name.toLowerCase();
|
|
118
|
+
return COMPANION_SETTING_KEYS.find((k) => k.key.toLowerCase() === lower) ?? null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Human form of a row's constraint, for `list`/`get` and help. */
|
|
122
|
+
export function describeConstraint(entry) {
|
|
123
|
+
if (!entry) return "";
|
|
124
|
+
const parts = [];
|
|
125
|
+
if (entry.type === "enum") parts.push(entry.values.join("|"));
|
|
126
|
+
else if (entry.type === "boolean") parts.push("true|false");
|
|
127
|
+
else if (entry.type === "integer" || entry.type === "number") {
|
|
128
|
+
parts.push(
|
|
129
|
+
entry.min != null && entry.max != null
|
|
130
|
+
? `${entry.min}-${entry.max}`
|
|
131
|
+
: entry.type,
|
|
132
|
+
);
|
|
133
|
+
} else parts.push(entry.type);
|
|
134
|
+
if (entry.nullable) parts.push("or null");
|
|
135
|
+
if (entry.readOnly) parts.push("(read-only)");
|
|
136
|
+
if (entry.note) parts.push(`— ${entry.note}`);
|
|
137
|
+
return parts.join(" ");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Parse the string a user typed into the JSON value the route expects.
|
|
142
|
+
* Returns `{ ok: true, value }` or `{ ok: false, error }` — the error is the
|
|
143
|
+
* message the CLI prints (and the reason the request is never sent).
|
|
144
|
+
*/
|
|
145
|
+
export function coerceValue(entry, raw) {
|
|
146
|
+
const text = String(raw ?? "").trim();
|
|
147
|
+
const lower = text.toLowerCase();
|
|
148
|
+
const { key } = entry;
|
|
149
|
+
|
|
150
|
+
if (entry.nullable && NULL_WORDS.has(lower)) return { ok: true, value: null };
|
|
151
|
+
|
|
152
|
+
switch (entry.type) {
|
|
153
|
+
case "boolean": {
|
|
154
|
+
if (TRUE_WORDS.has(lower)) return { ok: true, value: true };
|
|
155
|
+
if (FALSE_WORDS.has(lower)) return { ok: true, value: false };
|
|
156
|
+
return { ok: false, error: `${key} expects true|false (also on|off, yes|no); got "${text}"` };
|
|
157
|
+
}
|
|
158
|
+
case "enum": {
|
|
159
|
+
const match = entry.values.find((v) => v.toLowerCase() === lower);
|
|
160
|
+
if (match) return { ok: true, value: match };
|
|
161
|
+
return { ok: false, error: `${key} expects one of ${entry.values.join("|")}; got "${text}"` };
|
|
162
|
+
}
|
|
163
|
+
case "integer":
|
|
164
|
+
case "number": {
|
|
165
|
+
const n = Number(text);
|
|
166
|
+
if (text === "" || !Number.isFinite(n)) {
|
|
167
|
+
return { ok: false, error: `${key} expects a ${entry.type}; got "${text}"` };
|
|
168
|
+
}
|
|
169
|
+
if (entry.type === "integer" && !Number.isInteger(n)) {
|
|
170
|
+
return { ok: false, error: `${key} expects a whole number; got "${text}"` };
|
|
171
|
+
}
|
|
172
|
+
if ((entry.min != null && n < entry.min) || (entry.max != null && n > entry.max)) {
|
|
173
|
+
return { ok: false, error: `${key} must be between ${entry.min} and ${entry.max}; got ${text}` };
|
|
174
|
+
}
|
|
175
|
+
return { ok: true, value: n };
|
|
176
|
+
}
|
|
177
|
+
case "list": {
|
|
178
|
+
const items = text.split(",").map((s) => s.trim()).filter(Boolean);
|
|
179
|
+
return { ok: true, value: items };
|
|
180
|
+
}
|
|
181
|
+
case "string":
|
|
182
|
+
default:
|
|
183
|
+
return { ok: true, value: text };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** `"captureAutoLaunch.enabled", true` → `{ captureAutoLaunch: { enabled: true } }`. */
|
|
188
|
+
export function buildPatch(key, value) {
|
|
189
|
+
const parts = key.split(".");
|
|
190
|
+
const patch = {};
|
|
191
|
+
let cursor = patch;
|
|
192
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
193
|
+
cursor[parts[i]] = {};
|
|
194
|
+
cursor = cursor[parts[i]];
|
|
195
|
+
}
|
|
196
|
+
cursor[parts[parts.length - 1]] = value;
|
|
197
|
+
return patch;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Read a dotted key out of the settings object; `undefined` when absent. */
|
|
201
|
+
export function getPath(obj, key) {
|
|
202
|
+
let cur = obj;
|
|
203
|
+
for (const part of key.split(".")) {
|
|
204
|
+
if (cur == null || typeof cur !== "object") return undefined;
|
|
205
|
+
cur = cur[part];
|
|
206
|
+
}
|
|
207
|
+
return cur;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Flatten nested settings objects to dotted keys (arrays stay values). */
|
|
211
|
+
export function flattenSettings(settings, prefix = "") {
|
|
212
|
+
const out = {};
|
|
213
|
+
for (const [k, v] of Object.entries(settings ?? {})) {
|
|
214
|
+
const full = prefix ? `${prefix}.${k}` : k;
|
|
215
|
+
if (v && typeof v === "object" && !Array.isArray(v)) Object.assign(out, flattenSettings(v, full));
|
|
216
|
+
else out[full] = v;
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Parse everything after `hs companion`. Returns one of:
|
|
223
|
+
* { kind: "help", topic: "companion"|"settings" }
|
|
224
|
+
* { kind: "host", sub, rest } — start|stop|status|build
|
|
225
|
+
* { kind: "settings", action: "list", json, url }
|
|
226
|
+
* { kind: "settings", action: "get", key, json, url }
|
|
227
|
+
* { kind: "settings", action: "set", key, value, json, url }
|
|
228
|
+
* { error }
|
|
229
|
+
*/
|
|
230
|
+
export function parseCompanionArgs(argv) {
|
|
231
|
+
const [head, ...tail] = argv;
|
|
232
|
+
if (head === undefined) return { kind: "host", sub: "start", rest: [] };
|
|
233
|
+
if (head === "--help" || head === "-h" || head === "help") return { kind: "help", topic: "companion" };
|
|
234
|
+
if (HOST_COMMANDS.includes(head)) return { kind: "host", sub: head, rest: tail };
|
|
235
|
+
|
|
236
|
+
const flags = { json: false, url: null, help: false };
|
|
237
|
+
const positional = [];
|
|
238
|
+
for (let i = 0; i < tail.length; i++) {
|
|
239
|
+
const a = tail[i];
|
|
240
|
+
if (a === "--json") flags.json = true;
|
|
241
|
+
else if (a === "--url") flags.url = tail[++i] ?? null;
|
|
242
|
+
else if (a.startsWith("--url=")) flags.url = a.slice("--url=".length);
|
|
243
|
+
else if (a === "--help" || a === "-h" || a === "help") flags.help = true;
|
|
244
|
+
else if (a.startsWith("--")) return { error: `unknown flag ${a}` };
|
|
245
|
+
else positional.push(a);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (head === "settings") {
|
|
249
|
+
if (flags.help) return { kind: "help", topic: "settings" };
|
|
250
|
+
const [action = "list", ...args] = positional;
|
|
251
|
+
if (action === "list") return { kind: "settings", action: "list", json: flags.json, url: flags.url };
|
|
252
|
+
if (action === "get") {
|
|
253
|
+
if (!args[0]) return { error: "settings get needs a key (hs companion settings get <key>)" };
|
|
254
|
+
return { kind: "settings", action: "get", key: args[0], json: flags.json, url: flags.url };
|
|
255
|
+
}
|
|
256
|
+
if (action === "set") {
|
|
257
|
+
// `set key value` and `set key=value` both read naturally.
|
|
258
|
+
let [key, value] = args;
|
|
259
|
+
if (key && value === undefined && key.includes("=")) {
|
|
260
|
+
const eq = key.indexOf("=");
|
|
261
|
+
value = key.slice(eq + 1);
|
|
262
|
+
key = key.slice(0, eq);
|
|
263
|
+
}
|
|
264
|
+
if (!key || value === undefined) {
|
|
265
|
+
return { error: "settings set needs a key and a value (hs companion settings set <key> <value>)" };
|
|
266
|
+
}
|
|
267
|
+
return { kind: "settings", action: "set", key, value, json: flags.json, url: flags.url };
|
|
268
|
+
}
|
|
269
|
+
return { error: `unknown settings subcommand \`${action}\` (list|get|set)` };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const alias = COMPANION_ALIASES[head];
|
|
273
|
+
if (alias) {
|
|
274
|
+
if (flags.help) return { kind: "help", topic: "settings" };
|
|
275
|
+
const value = alias.value ?? positional[0];
|
|
276
|
+
if (value === undefined) {
|
|
277
|
+
return { kind: "settings", action: "get", key: alias.key, json: flags.json, url: flags.url, alias: head };
|
|
278
|
+
}
|
|
279
|
+
return { kind: "settings", action: "set", key: alias.key, value, json: flags.json, url: flags.url, alias: head };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { error: `unknown subcommand \`${head}\`` };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Bearer for the settings route: loopback UI token first, then the shared MCP ladder. */
|
|
286
|
+
export function companionAuthHeader({ env = process.env, homeDir } = {}) {
|
|
287
|
+
const uiToken = env.COMPANION_UI_TOKEN?.trim();
|
|
288
|
+
if (uiToken) return { Authorization: `Bearer ${uiToken}` };
|
|
289
|
+
return getAuthHeader(homeDir ? { env, homeDir } : { env });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The console fetch this command uses: vendor transport + shared URL
|
|
294
|
+
* resolver + the bearer ladder above. `url` (from `--url`) short-circuits
|
|
295
|
+
* the resolver; `fetchImpl` is for tests.
|
|
296
|
+
*/
|
|
297
|
+
export function makeCompanionConsoleFetch({ url = null, env = process.env, fetchImpl, repoRoot } = {}) {
|
|
298
|
+
const getConsoleUrl = () =>
|
|
299
|
+
(url ?? resolveConsoleUrl({ repoRoot: repoRoot ?? env.HS_VAULT_ROOT ?? process.cwd(), env }))
|
|
300
|
+
.replace(/\/+$/, "");
|
|
301
|
+
return makeConsoleFetch({
|
|
302
|
+
getConsoleUrl,
|
|
303
|
+
...(fetchImpl ? { fetchImpl } : {}),
|
|
304
|
+
getAuthHeader: () => companionAuthHeader({ env }),
|
|
305
|
+
}).consoleFetch;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** One-line failure text for a non-ok transport result; route errors verbatim. */
|
|
309
|
+
export function describeFailure(result, verb = "GET") {
|
|
310
|
+
if (!result || result.status === 0) {
|
|
311
|
+
const detail = result?.error ? ` (${result.error})` : "";
|
|
312
|
+
return result?.authMessage ?? `console unreachable at ${result?.url ?? "?"}${detail}`;
|
|
313
|
+
}
|
|
314
|
+
const body = result.body;
|
|
315
|
+
const msg =
|
|
316
|
+
(body && typeof body === "object" && (body.message ?? body.error ?? body.raw)) ??
|
|
317
|
+
(typeof body === "string" ? body : JSON.stringify(body));
|
|
318
|
+
return `${verb} ${SETTINGS_PATH} failed ${result.status}: ${msg}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function formatValue(v) {
|
|
322
|
+
if (v === undefined) return "(not reported by this console)";
|
|
323
|
+
if (v === null) return "null";
|
|
324
|
+
if (Array.isArray(v)) return v.length ? v.join(",") : "(empty)";
|
|
325
|
+
if (typeof v === "string") return v === "" ? '""' : v;
|
|
326
|
+
return String(v);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function pad(s, n) {
|
|
330
|
+
return s.length >= n ? s : s + " ".repeat(n - s.length);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Rows for `list`: the key table ∪ whatever the console reported. */
|
|
334
|
+
export function listRows(settings) {
|
|
335
|
+
const flat = flattenSettings(settings);
|
|
336
|
+
const rows = COMPANION_SETTING_KEYS.map((entry) => ({
|
|
337
|
+
key: entry.key,
|
|
338
|
+
value: flat[entry.key],
|
|
339
|
+
constraint: describeConstraint(entry),
|
|
340
|
+
entry,
|
|
341
|
+
}));
|
|
342
|
+
const known = new Set(COMPANION_SETTING_KEYS.map((k) => k.key));
|
|
343
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
344
|
+
if (!known.has(key)) rows.push({ key, value, constraint: "(not in the CLI key table)", entry: null });
|
|
345
|
+
}
|
|
346
|
+
return rows;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function renderList(rows, { consoleUrl, connected }) {
|
|
350
|
+
const width = Math.max(...rows.map((r) => r.key.length)) + 2;
|
|
351
|
+
const valueWidth = Math.max(...rows.map((r) => formatValue(r.value).length)) + 2;
|
|
352
|
+
const lines = [
|
|
353
|
+
`companion settings @ ${consoleUrl} (companion ${connected ? "connected" : "not connected"})`,
|
|
354
|
+
"",
|
|
355
|
+
...rows.map((r) => `${pad(r.key, width)}${pad(formatValue(r.value), valueWidth)}${r.constraint}`),
|
|
356
|
+
"",
|
|
357
|
+
"set: hs companion settings set <key> <value> shortcuts: hs companion --help",
|
|
358
|
+
];
|
|
359
|
+
return lines.join("\n");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function constraintJson(entry) {
|
|
363
|
+
if (!entry) return null;
|
|
364
|
+
const c = { type: entry.type };
|
|
365
|
+
if (entry.values) c.values = entry.values;
|
|
366
|
+
if (entry.min != null) c.min = entry.min;
|
|
367
|
+
if (entry.max != null) c.max = entry.max;
|
|
368
|
+
if (entry.nullable) c.nullable = true;
|
|
369
|
+
if (entry.readOnly) c.readOnly = true;
|
|
370
|
+
if (entry.note) c.note = entry.note;
|
|
371
|
+
return c;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Execute a parsed `settings` command against `consoleFetch` (vendor shape:
|
|
376
|
+
* `consoleFetch(path, { method, body })` → `{ ok, status, body, url }`).
|
|
377
|
+
* Returns `{ ok: true, stdout, exitCode: 0 }` or `{ ok: false, stderr, exitCode }`
|
|
378
|
+
* — the process face prints; nothing here writes to stdio.
|
|
379
|
+
*/
|
|
380
|
+
export async function runCompanionSettings(cmd, { consoleFetch }) {
|
|
381
|
+
const fail = (stderr, exitCode = 1) => ({ ok: false, stderr, exitCode });
|
|
382
|
+
const emit = (stdout, json) => ({ ok: true, stdout: cmd.json ? JSON.stringify(json, null, 2) : stdout, exitCode: 0 });
|
|
383
|
+
|
|
384
|
+
if (cmd.action === "list") {
|
|
385
|
+
const res = await consoleFetch(SETTINGS_PATH);
|
|
386
|
+
if (!res.ok) return fail(describeFailure(res, "GET"));
|
|
387
|
+
const consoleUrl = res.url.replace(SETTINGS_PATH, "");
|
|
388
|
+
const rows = listRows(res.body?.settings ?? {});
|
|
389
|
+
const json = {
|
|
390
|
+
consoleUrl,
|
|
391
|
+
connected: res.body?.connected ?? false,
|
|
392
|
+
settings: Object.fromEntries(rows.map((r) => [r.key, r.value ?? null])),
|
|
393
|
+
constraints: Object.fromEntries(rows.filter((r) => r.entry).map((r) => [r.key, constraintJson(r.entry)])),
|
|
394
|
+
devices: res.body?.devices ?? [],
|
|
395
|
+
activation: res.body?.activation ?? null,
|
|
396
|
+
};
|
|
397
|
+
return emit(renderList(rows, json), json);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// get / set both need the key resolved.
|
|
401
|
+
const entry = findSettingKey(cmd.key);
|
|
402
|
+
if (cmd.action === "set") {
|
|
403
|
+
if (!entry) return fail(`unknown setting \`${cmd.key}\`\nkeys: ${COMPANION_SETTING_KEYS.filter((k) => !k.readOnly).map((k) => k.key).join(", ")}`, 2);
|
|
404
|
+
if (entry.readOnly) return fail(`${entry.key} is companion-owned (read-only)`, 2);
|
|
405
|
+
const coerced = coerceValue(entry, cmd.value);
|
|
406
|
+
if (!coerced.ok) return fail(coerced.error, 2);
|
|
407
|
+
|
|
408
|
+
const res = await consoleFetch(SETTINGS_PATH, { method: "PUT", body: buildPatch(entry.key, coerced.value) });
|
|
409
|
+
if (!res.ok) return fail(describeFailure(res, "PUT"));
|
|
410
|
+
const actual = getPath(res.body?.settings ?? {}, entry.key);
|
|
411
|
+
const same = JSON.stringify(actual) === JSON.stringify(coerced.value);
|
|
412
|
+
const json = { key: entry.key, value: actual ?? null, requested: coerced.value, normalized: !same };
|
|
413
|
+
if (!same) {
|
|
414
|
+
return fail(
|
|
415
|
+
`${entry.key} = ${formatValue(actual)} — the console kept/normalised the value instead of ${formatValue(coerced.value)}` +
|
|
416
|
+
(actual === undefined ? " (this console does not report that key; is it running the latest build?)" : ""),
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
return emit(`${entry.key} = ${formatValue(actual)}`, json);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// get
|
|
423
|
+
const res = await consoleFetch(SETTINGS_PATH);
|
|
424
|
+
if (!res.ok) return fail(describeFailure(res, "GET"));
|
|
425
|
+
const key = entry?.key ?? cmd.key;
|
|
426
|
+
const value = getPath(res.body?.settings ?? {}, key);
|
|
427
|
+
if (!entry && value === undefined) {
|
|
428
|
+
return fail(`unknown setting \`${cmd.key}\`\nkeys: ${COMPANION_SETTING_KEYS.map((k) => k.key).join(", ")}`, 2);
|
|
429
|
+
}
|
|
430
|
+
const json = { key, value: value ?? null, constraint: constraintJson(entry) };
|
|
431
|
+
const constraint = entry ? ` ${describeConstraint(entry)}` : "";
|
|
432
|
+
return emit(`${key} = ${formatValue(value)}${constraint}`, json);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Help text derived from the two tables so it cannot drift from them. */
|
|
436
|
+
export function renderHelp(topic = "companion") {
|
|
437
|
+
const settingsLines = [
|
|
438
|
+
"Settings (console-backed; --json on list/get for agents):",
|
|
439
|
+
" hs companion settings [list] [--json] Every key: current value + allowed values",
|
|
440
|
+
" hs companion settings get <key> [--json] One key",
|
|
441
|
+
" hs companion settings set <key> <value> Change one key (enums case-insensitive; booleans on|off|true|false; null clears)",
|
|
442
|
+
" hs companion settings --help Key table",
|
|
443
|
+
"",
|
|
444
|
+
"Shortcuts (each is `settings set` under the hood; omit the value to read it):",
|
|
445
|
+
...Object.entries(COMPANION_ALIASES).map(([name, a]) => {
|
|
446
|
+
const usage = `hs companion ${name}${a.arg ? ` ${a.arg}` : ""}`;
|
|
447
|
+
return ` ${pad(usage, 50)}${a.help} (${a.key})`;
|
|
448
|
+
}),
|
|
449
|
+
"",
|
|
450
|
+
" --url <console> Override the resolved console URL for this call",
|
|
451
|
+
];
|
|
452
|
+
if (topic === "settings") {
|
|
453
|
+
const width = Math.max(...COMPANION_SETTING_KEYS.map((k) => k.key.length)) + 2;
|
|
454
|
+
return [
|
|
455
|
+
...settingsLines,
|
|
456
|
+
"",
|
|
457
|
+
"Keys:",
|
|
458
|
+
...COMPANION_SETTING_KEYS.map((k) => ` ${pad(k.key, width)}${describeConstraint(k)}`),
|
|
459
|
+
].join("\n");
|
|
460
|
+
}
|
|
461
|
+
return [
|
|
462
|
+
"Usage:",
|
|
463
|
+
" hs companion start [--build] Start the tray app (build first if missing)",
|
|
464
|
+
" hs companion stop Quit the tray app",
|
|
465
|
+
" hs companion status Is it running?",
|
|
466
|
+
" hs companion build dotnet build Debug",
|
|
467
|
+
"",
|
|
468
|
+
...settingsLines,
|
|
469
|
+
"",
|
|
470
|
+
"Shortcut: npm run companion",
|
|
471
|
+
].join("\n");
|
|
472
|
+
}
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* the colocated test for the enforcement proof.
|
|
14
14
|
*/
|
|
15
15
|
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { existsSync } from 'node:fs';
|
|
16
17
|
import path from 'node:path';
|
|
17
|
-
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
18
19
|
|
|
19
20
|
import {
|
|
20
21
|
getTreeStatus,
|
|
@@ -22,10 +23,40 @@ import {
|
|
|
22
23
|
computeDirtyFingerprint,
|
|
23
24
|
REPO_ROOT,
|
|
24
25
|
} from './hs-source-preflight.mjs';
|
|
25
|
-
import { createSessionBranch } from './session-branch.mjs';
|
|
26
26
|
|
|
27
27
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
28
28
|
const OWNER_COMMIT_SCRIPT = path.join(REPO_ROOT, 'scripts', 'owner-commit.mjs');
|
|
29
|
+
const SESSION_BRANCH_MODULE = path.join(__dirname, 'session-branch.mjs');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `--session` (HS-6N2Z) statically imported `./session-branch.mjs`, which is
|
|
33
|
+
* a dev-only module — never in the package's `files` (RUNTIME_FILES /
|
|
34
|
+
* `package.json`) — so a static import crashed EVERY `hs source commit`
|
|
35
|
+
* invocation with a raw `ERR_MODULE_NOT_FOUND` on a published npm install,
|
|
36
|
+
* not just ones that pass `--session`. Guarded, on-demand load instead: only
|
|
37
|
+
* attempted when `--session` is actually given (see `invokedDirectly()`
|
|
38
|
+
* below), so the no-flag path never even tries to resolve it, and a
|
|
39
|
+
* resolution failure prints a clear refusal instead of a stack trace. Mirrors
|
|
40
|
+
* the shape of `hs doctor`'s own published-install guard in `bin/hs.mjs`
|
|
41
|
+
* (`existsSync(script)` before ever importing/spawning it).
|
|
42
|
+
*
|
|
43
|
+
* @returns {Promise<import('./session-branch.mjs').createSessionBranch | null>}
|
|
44
|
+
*/
|
|
45
|
+
export async function loadCreateSessionBranch() {
|
|
46
|
+
if (!existsSync(SESSION_BRANCH_MODULE)) return null;
|
|
47
|
+
try {
|
|
48
|
+
const mod = await import(pathToFileURL(SESSION_BRANCH_MODULE).href);
|
|
49
|
+
return mod.createSessionBranch ?? null;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const SESSION_BRANCH_UNAVAILABLE_MESSAGE =
|
|
56
|
+
'hs source commit --session: refusing — scripts/lib/session-branch.mjs is not available on ' +
|
|
57
|
+
'this install. It ships only inside a full source checkout of the HeySpex repo, not a ' +
|
|
58
|
+
'published npm install, so `--session` is unavailable here. Run `hs source commit` without ' +
|
|
59
|
+
'`--session`, or use a source checkout.';
|
|
29
60
|
|
|
30
61
|
/**
|
|
31
62
|
* Default subprocess runner — injectable so tests can spy without ever
|
|
@@ -150,15 +181,24 @@ export function commitFromPlan({
|
|
|
150
181
|
* no-flag path completely unchanged: `createSessionBranch` is never called,
|
|
151
182
|
* so there is no branch-creation/selection side effect at all (AC2).
|
|
152
183
|
*
|
|
184
|
+
* `createSessionBranchFn` has NO default here (unlike before this task) —
|
|
185
|
+
* see `invokedDirectly()` below for why: the direct-invocation entry point is
|
|
186
|
+
* the only caller that ever needed the real `session-branch.mjs`, and it now
|
|
187
|
+
* resolves it itself via the guarded `loadCreateSessionBranch()` above,
|
|
188
|
+
* on-demand, only when `--session` is actually in argv. A caller (test or
|
|
189
|
+
* otherwise) that reaches this function with `--session` set but no
|
|
190
|
+
* `createSessionBranchFn` gets the same graceful refusal a missing-module
|
|
191
|
+
* install would produce, never a crash.
|
|
192
|
+
*
|
|
153
193
|
* @param {string[]} argv (verb-stripped: just the flags/args after `commit`)
|
|
154
194
|
* @param {{
|
|
155
195
|
* cwd?: string,
|
|
156
196
|
* run?: (cmd: string, args: string[], opts?: object) => { status: number, stdout?: string, stderr?: string },
|
|
157
|
-
* createSessionBranchFn?:
|
|
197
|
+
* createSessionBranchFn?: (opts: object) => { ok: boolean, code?: string, message?: string, branch?: string, reused?: boolean },
|
|
158
198
|
* }} [opts]
|
|
159
199
|
* @returns {number} process exit code
|
|
160
200
|
*/
|
|
161
|
-
export function runCommitCommand(argv, { cwd = process.cwd(), run, createSessionBranchFn
|
|
201
|
+
export function runCommitCommand(argv, { cwd = process.cwd(), run, createSessionBranchFn } = {}) {
|
|
162
202
|
let message = '';
|
|
163
203
|
let sessionId;
|
|
164
204
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -172,6 +212,10 @@ export function runCommitCommand(argv, { cwd = process.cwd(), run, createSession
|
|
|
172
212
|
}
|
|
173
213
|
|
|
174
214
|
if (sessionId !== undefined) {
|
|
215
|
+
if (typeof createSessionBranchFn !== 'function') {
|
|
216
|
+
console.error(SESSION_BRANCH_UNAVAILABLE_MESSAGE);
|
|
217
|
+
return 1;
|
|
218
|
+
}
|
|
175
219
|
const branchResult = createSessionBranchFn({ sessionId, cwd, run });
|
|
176
220
|
if (!branchResult.ok) {
|
|
177
221
|
console.error(branchResult.message);
|
|
@@ -203,5 +247,15 @@ function invokedDirectly() {
|
|
|
203
247
|
|
|
204
248
|
if (invokedDirectly()) {
|
|
205
249
|
// argv: [node, script, "commit", ...flags] — drop the leading verb token.
|
|
206
|
-
process.
|
|
250
|
+
const argv = process.argv.slice(3);
|
|
251
|
+
// Only ever attempt to resolve the dev-only session-branch.mjs when
|
|
252
|
+
// `--session` is actually requested — the no-flag path (the overwhelming
|
|
253
|
+
// majority of invocations, including every published-install one) never
|
|
254
|
+
// touches it, exactly as before this task.
|
|
255
|
+
const needsSessionBranch = argv.includes('--session');
|
|
256
|
+
(needsSessionBranch ? loadCreateSessionBranch() : Promise.resolve(undefined)).then(
|
|
257
|
+
(createSessionBranchFn) => {
|
|
258
|
+
process.exit(runCommitCommand(argv, { createSessionBranchFn: createSessionBranchFn ?? undefined }));
|
|
259
|
+
},
|
|
260
|
+
);
|
|
207
261
|
}
|