even-keel 0.0.2 → 0.2.0-rc.2
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/.claude-plugin/marketplace.json +52 -0
- package/.env.example +66 -0
- package/CHANGELOG.md +315 -0
- package/LICENSE +21 -0
- package/README.md +199 -3
- package/dist/src/account/apps.js +100 -0
- package/dist/src/account/credit.js +117 -0
- package/dist/src/account/current.js +13 -0
- package/dist/src/account/daemon.js +65 -0
- package/dist/src/account/globals.js +25 -0
- package/dist/src/account/header-levels.js +22 -0
- package/dist/src/account/homes.js +332 -0
- package/dist/src/account/keychain.js +134 -0
- package/dist/src/account/keycheck.js +67 -0
- package/dist/src/account/ledger.js +92 -0
- package/dist/src/account/model.js +234 -0
- package/dist/src/account/network.js +51 -0
- package/dist/src/account/organizations.js +17 -0
- package/dist/src/account/service-defaults.js +17 -0
- package/dist/src/account/share.js +275 -0
- package/dist/src/account/transport.js +29 -0
- package/dist/src/account/vscode.js +84 -0
- package/dist/src/account/watcher.js +451 -0
- package/dist/src/agent/adapters.js +10 -0
- package/dist/src/agent/cli-state.js +1 -0
- package/dist/src/agent/contract.js +7 -0
- package/dist/src/agent/copies.js +38 -0
- package/dist/src/agent/copy.js +42 -0
- package/dist/src/agent/download.js +39 -0
- package/dist/src/agent/env.js +23 -0
- package/dist/src/agent/models.js +90 -0
- package/dist/src/agent/npm.js +55 -0
- package/dist/src/agent/oneturn.js +48 -0
- package/dist/src/agent/pinned.js +79 -0
- package/dist/src/agent/process.js +76 -0
- package/dist/src/agent/projectRules.js +54 -0
- package/dist/src/agent/providers.js +39 -0
- package/dist/src/agent/versions.js +89 -0
- package/dist/src/agent/windows.js +39 -0
- package/dist/src/ansi.js +6 -0
- package/dist/src/autostart.js +80 -0
- package/dist/src/chain/answers.js +26 -0
- package/dist/src/chain/archive.js +25 -0
- package/dist/src/chain/carry.js +97 -0
- package/dist/src/chain/cleanup.js +134 -0
- package/dist/src/chain/dispatcher.js +800 -0
- package/dist/src/chain/fix.js +55 -0
- package/dist/src/chain/forward.js +195 -0
- package/dist/src/chain/handover.js +350 -0
- package/dist/src/chain/launches.js +193 -0
- package/dist/src/chain/limitNotice.js +30 -0
- package/dist/src/chain/lists.js +174 -0
- package/dist/src/chain/log.js +23 -0
- package/dist/src/chain/maincheck.js +449 -0
- package/dist/src/chain/mainci.js +116 -0
- package/dist/src/chain/mainfix.js +180 -0
- package/dist/src/chain/mainwait.js +73 -0
- package/dist/src/chain/milestones.js +67 -0
- package/dist/src/chain/model.js +6 -0
- package/dist/src/chain/moves.js +100 -0
- package/dist/src/chain/order.js +266 -0
- package/dist/src/chain/overage.js +44 -0
- package/dist/src/chain/pauses.js +81 -0
- package/dist/src/chain/places.js +42 -0
- package/dist/src/chain/prompts.js +199 -0
- package/dist/src/chain/relay.js +153 -0
- package/dist/src/chain/restartGuard.js +17 -0
- package/dist/src/chain/reviews.js +316 -0
- package/dist/src/chain/rules.js +59 -0
- package/dist/src/chain/shelved.js +33 -0
- package/dist/src/chain/skippedown.js +20 -0
- package/dist/src/chain/snapshots.js +37 -0
- package/dist/src/chain/start.js +52 -0
- package/dist/src/chain/store.js +146 -0
- package/dist/src/chain/unchanged.js +71 -0
- package/dist/src/changelog/diff.js +118 -0
- package/dist/src/changelog/draft.js +59 -0
- package/dist/src/changelog/drafts.js +121 -0
- package/dist/src/changelog/file.js +146 -0
- package/dist/src/changelog/store.js +75 -0
- package/dist/src/ci/adapters.js +65 -0
- package/dist/src/ci/forgejo-profile.js +86 -0
- package/dist/src/ci/forgejo.js +62 -0
- package/dist/src/ci/github.js +53 -0
- package/dist/src/ci/gitlab.js +62 -0
- package/dist/src/ci/host.js +14 -0
- package/dist/src/ci/http.js +71 -0
- package/dist/src/ci/model.js +41 -0
- package/dist/src/ci/protection-audit.js +49 -0
- package/dist/src/ci/protection-model.js +1 -0
- package/dist/src/ci/protection.js +75 -0
- package/dist/src/ci/remote.js +105 -0
- package/dist/src/ci/report.js +114 -0
- package/dist/src/ci/shared.js +74 -0
- package/dist/src/ci/verdict.js +61 -0
- package/dist/src/ci/watcher.js +188 -0
- package/dist/src/ci/watches.js +116 -0
- package/dist/src/ci/wiring.js +26 -0
- package/dist/src/claude/adapter.js +174 -0
- package/dist/src/claude/calls.js +88 -0
- package/dist/src/claude/cli.js +451 -0
- package/dist/src/claude/context.js +33 -0
- package/dist/src/claude/instructions.js +12 -0
- package/dist/src/claude/leftovers.js +32 -0
- package/dist/src/claude/name.js +2 -0
- package/dist/src/claude/release.js +69 -0
- package/dist/src/claude/sdk/executor.js +214 -0
- package/dist/src/claude/sdk/host.js +422 -0
- package/dist/src/claude/sdk/pipe.js +134 -0
- package/dist/src/claude/sdk/registry.js +88 -0
- package/dist/src/claude/sdk/release.js +26 -0
- package/dist/src/claude/sdk/types.js +1 -0
- package/dist/src/claude/tree.js +103 -0
- package/dist/src/claude/trust.js +12 -0
- package/dist/src/cli-args.js +36 -0
- package/dist/src/cli-check/lease.js +35 -0
- package/dist/src/cli-check/run.js +381 -0
- package/dist/src/cli-check/scheduler.js +134 -0
- package/dist/src/cli-check/support.js +105 -0
- package/dist/src/cli.js +196 -0
- package/dist/src/codex/adapter.js +112 -0
- package/dist/src/codex/appserver.js +68 -0
- package/dist/src/codex/cli.js +360 -0
- package/dist/src/codex/mcp.js +8 -0
- package/dist/src/codex/name.js +2 -0
- package/dist/src/codex/plugins.js +137 -0
- package/dist/src/codex/process.js +81 -0
- package/dist/src/codex/protection.js +32 -0
- package/dist/src/codex/release.js +33 -0
- package/dist/src/codex/rollout.js +478 -0
- package/dist/src/codex/rolloutImages.js +49 -0
- package/dist/src/codex/rules.js +141 -0
- package/dist/src/codex/tail.js +53 -0
- package/dist/src/codex/turns.js +140 -0
- package/dist/src/codex/usage.js +88 -0
- package/dist/src/codex/version.js +2 -0
- package/dist/src/config-form.js +359 -0
- package/dist/src/config-store.js +184 -0
- package/dist/src/config.js +524 -0
- package/dist/src/copilot/adapter.js +121 -0
- package/dist/src/copilot/cli.js +298 -0
- package/dist/src/copilot/errors.js +50 -0
- package/dist/src/copilot/home.js +84 -0
- package/dist/src/copilot/machine.js +41 -0
- package/dist/src/copilot/name.js +2 -0
- package/dist/src/copilot/release.js +30 -0
- package/dist/src/copilot/tail.js +57 -0
- package/dist/src/copilot/transcript.js +385 -0
- package/dist/src/diagnostics/start-watch.js +77 -0
- package/dist/src/env-file.js +91 -0
- package/dist/src/events.js +150 -0
- package/dist/src/git/changelog.js +38 -0
- package/dist/src/git/changes.js +10 -0
- package/dist/src/git/merged.js +36 -0
- package/dist/src/git/run.js +25 -0
- package/dist/src/git/setup.js +100 -0
- package/dist/src/git/slot.js +99 -0
- package/dist/src/git/sync.js +72 -0
- package/dist/src/git/types.js +1 -0
- package/dist/src/git/work.js +119 -0
- package/dist/src/git/worktree.js +216 -0
- package/dist/src/github/adapter.js +30 -0
- package/dist/src/github/model.js +104 -0
- package/dist/src/github/profile.js +50 -0
- package/dist/src/github/source.js +438 -0
- package/dist/src/github/trust.js +25 -0
- package/dist/src/github/uploads.js +36 -0
- package/dist/src/gitlab/adapter.js +51 -0
- package/dist/src/gitlab/api.js +71 -0
- package/dist/src/gitlab/batch.js +114 -0
- package/dist/src/gitlab/hierarchy.js +59 -0
- package/dist/src/gitlab/model.js +56 -0
- package/dist/src/gitlab/profile.js +25 -0
- package/dist/src/gitlab/source.js +246 -0
- package/dist/src/gitlab/uploads.js +65 -0
- package/dist/src/guard.js +141 -0
- package/dist/src/huly/adapter.js +34 -0
- package/dist/src/huly/client.js +158 -0
- package/dist/src/huly/credentials.js +27 -0
- package/dist/src/huly/ids.js +16 -0
- package/dist/src/huly/labels.js +32 -0
- package/dist/src/huly/model.js +191 -0
- package/dist/src/huly/profile.js +73 -0
- package/dist/src/huly/quiet.js +17 -0
- package/dist/src/huly/source.js +327 -0
- package/dist/src/intake/github.js +73 -0
- package/dist/src/intake/intake.js +113 -0
- package/dist/src/intake/model.js +72 -0
- package/dist/src/jira/adapter.js +35 -0
- package/dist/src/jira/adf.js +253 -0
- package/dist/src/jira/api.js +96 -0
- package/dist/src/jira/mcp.js +134 -0
- package/dist/src/jira/model.js +111 -0
- package/dist/src/jira/profile.js +17 -0
- package/dist/src/jira/source.js +320 -0
- package/dist/src/jsonc.js +15 -0
- package/dist/src/language.js +4 -0
- package/dist/src/limits/chatgpt.js +144 -0
- package/dist/src/limits/copilot.js +117 -0
- package/dist/src/limits/model.js +183 -0
- package/dist/src/limits/overage-session.js +9 -0
- package/dist/src/limits/overage.js +74 -0
- package/dist/src/limits/recovery.js +23 -0
- package/dist/src/limits/reset.js +81 -0
- package/dist/src/limits/retry.js +45 -0
- package/dist/src/limits/saved.js +17 -0
- package/dist/src/limits/spending.js +15 -0
- package/dist/src/limits/usage.js +53 -0
- package/dist/src/limits/watcher.js +269 -0
- package/dist/src/linear/adapter.js +50 -0
- package/dist/src/linear/model.js +89 -0
- package/dist/src/linear/profile.js +45 -0
- package/dist/src/linear/source.js +423 -0
- package/dist/src/locations.js +53 -0
- package/dist/src/main.js +76 -0
- package/dist/src/notice/en.js +489 -0
- package/dist/src/notice/notice.js +93 -0
- package/dist/src/opencode/adapter.js +121 -0
- package/dist/src/opencode/cli.js +433 -0
- package/dist/src/opencode/db.js +103 -0
- package/dist/src/opencode/home.js +85 -0
- package/dist/src/opencode/log.js +87 -0
- package/dist/src/opencode/models.js +51 -0
- package/dist/src/opencode/name.js +2 -0
- package/dist/src/opencode/protection-plugin.js +27 -0
- package/dist/src/opencode/release.js +29 -0
- package/dist/src/opencode/support.js +14 -0
- package/dist/src/opencode/tail.js +42 -0
- package/dist/src/opencode/transcript.js +331 -0
- package/dist/src/opencode/usage.js +41 -0
- package/dist/src/packages/check.js +146 -0
- package/dist/src/packages/gateway.js +496 -0
- package/dist/src/packages/keys.js +78 -0
- package/dist/src/packages/launch.js +99 -0
- package/dist/src/packages/model.js +61 -0
- package/dist/src/packages/npm.js +115 -0
- package/dist/src/packages/plugins.js +162 -0
- package/dist/src/packages/service.js +615 -0
- package/dist/src/packages/settings.js +25 -0
- package/dist/src/packages/skills.js +132 -0
- package/dist/src/packages/store.js +39 -0
- package/dist/src/packages/version.js +26 -0
- package/dist/src/patch.js +1 -0
- package/dist/src/paths.js +83 -0
- package/dist/src/profiles/control.js +32 -0
- package/dist/src/profiles/mcp.js +29 -0
- package/dist/src/profiles/model.js +97 -0
- package/dist/src/profiles/oauth-error.js +21 -0
- package/dist/src/profiles/oauth-protocol.js +106 -0
- package/dist/src/profiles/oauth-response.js +12 -0
- package/dist/src/profiles/oauth.js +228 -0
- package/dist/src/profiles/request.js +20 -0
- package/dist/src/profiles/routes.js +31 -0
- package/dist/src/profiles/rules.js +54 -0
- package/dist/src/profiles/service.js +471 -0
- package/dist/src/profiles/store.js +125 -0
- package/dist/src/projects/add.js +11 -0
- package/dist/src/projects/check.js +174 -0
- package/dist/src/projects/colors.js +16 -0
- package/dist/src/projects/folder.js +30 -0
- package/dist/src/projects/remotes.js +109 -0
- package/dist/src/projects/removal.js +14 -0
- package/dist/src/projects/standards.js +31 -0
- package/dist/src/review/events.js +27 -0
- package/dist/src/review/feedback.js +64 -0
- package/dist/src/review/forgejo.js +110 -0
- package/dist/src/review/github.js +69 -0
- package/dist/src/review/gitlab.js +118 -0
- package/dist/src/review/model.js +25 -0
- package/dist/src/review/open.js +141 -0
- package/dist/src/review/refusals.js +20 -0
- package/dist/src/review/wiring.js +93 -0
- package/dist/src/secrets/catalog.js +34 -0
- package/dist/src/secrets/service.js +168 -0
- package/dist/src/server-guard.js +51 -0
- package/dist/src/server-request.js +360 -0
- package/dist/src/server.js +576 -0
- package/dist/src/service/accounts.js +584 -0
- package/dist/src/service/chain.js +642 -0
- package/dist/src/service/cli-check.js +58 -0
- package/dist/src/service/hosting.js +508 -0
- package/dist/src/service/lifecycle.js +59 -0
- package/dist/src/service/stats.js +72 -0
- package/dist/src/service/trackers.js +239 -0
- package/dist/src/service.js +699 -0
- package/dist/src/sessions/activity.js +32 -0
- package/dist/src/sessions/agents.js +217 -0
- package/dist/src/sessions/approvals.js +105 -0
- package/dist/src/sessions/ask.js +234 -0
- package/dist/src/sessions/cache.js +14 -0
- package/dist/src/sessions/chat.js +60 -0
- package/dist/src/sessions/commandPrompt.js +31 -0
- package/dist/src/sessions/commands.js +29 -0
- package/dist/src/sessions/entry.js +104 -0
- package/dist/src/sessions/history.js +88 -0
- package/dist/src/sessions/holds.js +63 -0
- package/dist/src/sessions/inbox.js +101 -0
- package/dist/src/sessions/keepwarm.js +110 -0
- package/dist/src/sessions/launch.js +32 -0
- package/dist/src/sessions/lines.js +90 -0
- package/dist/src/sessions/lingering.js +26 -0
- package/dist/src/sessions/mail.js +36 -0
- package/dist/src/sessions/mailRelay.js +88 -0
- package/dist/src/sessions/mailbox.js +136 -0
- package/dist/src/sessions/model-prompts.js +16 -0
- package/dist/src/sessions/monitor.js +215 -0
- package/dist/src/sessions/note.js +30 -0
- package/dist/src/sessions/occupants.js +15 -0
- package/dist/src/sessions/owners.js +59 -0
- package/dist/src/sessions/paths.js +24 -0
- package/dist/src/sessions/permission.js +58 -0
- package/dist/src/sessions/place.js +37 -0
- package/dist/src/sessions/prompt.js +16 -0
- package/dist/src/sessions/protection.js +88 -0
- package/dist/src/sessions/remote.js +41 -0
- package/dist/src/sessions/setup.js +57 -0
- package/dist/src/sessions/share.js +6 -0
- package/dist/src/sessions/shareFile.js +32 -0
- package/dist/src/sessions/silence.js +25 -0
- package/dist/src/sessions/tail.js +79 -0
- package/dist/src/sessions/title.js +67 -0
- package/dist/src/sessions/transcript.js +477 -0
- package/dist/src/sessions/transient.js +4 -0
- package/dist/src/sessions/watch.js +78 -0
- package/dist/src/settings/changes.js +15 -0
- package/dist/src/settings/defaults.js +25 -0
- package/dist/src/settings/file.js +197 -0
- package/dist/src/settings/import.js +93 -0
- package/dist/src/settings/registry.js +422 -0
- package/dist/src/settings/resolve.js +80 -0
- package/dist/src/settings/store.js +102 -0
- package/dist/src/setup.js +7 -0
- package/dist/src/state.js +1 -0
- package/dist/src/stats/base-sql.js +58 -0
- package/dist/src/stats/break-labels.js +53 -0
- package/dist/src/stats/calibration.js +43 -0
- package/dist/src/stats/checkpoint.js +66 -0
- package/dist/src/stats/connect.js +49 -0
- package/dist/src/stats/diff.js +88 -0
- package/dist/src/stats/mode.js +25 -0
- package/dist/src/stats/model.js +108 -0
- package/dist/src/stats/reader.js +563 -0
- package/dist/src/stats/rule-imports.js +106 -0
- package/dist/src/stats/rules-reader.js +257 -0
- package/dist/src/stats/rules.js +324 -0
- package/dist/src/stats/steps-reader.js +108 -0
- package/dist/src/stats/steps.js +159 -0
- package/dist/src/stats/tally-schema.js +13 -0
- package/dist/src/stats/tally.js +139 -0
- package/dist/src/stats/task-checkpoint.js +9 -0
- package/dist/src/storage/atomic.js +41 -0
- package/dist/src/storage/claim.js +139 -0
- package/dist/src/storage/copy.js +69 -0
- package/dist/src/storage/database.js +405 -0
- package/dist/src/storage/json-folder.js +47 -0
- package/dist/src/storage/memory.js +15 -0
- package/dist/src/storage/migrations.js +373 -0
- package/dist/src/storage/owner.js +114 -0
- package/dist/src/storage/package-settings.js +33 -0
- package/dist/src/storage/projects.js +23 -0
- package/dist/src/storage/schema.js +21 -0
- package/dist/src/storage/service-state.js +9 -0
- package/dist/src/storage/state-files.js +222 -0
- package/dist/src/tools/model.js +1 -0
- package/dist/src/tools/panel.js +197 -0
- package/dist/src/tools/received.js +114 -0
- package/dist/src/tools/sessionSettings.js +15 -0
- package/dist/src/tools/settings.js +109 -0
- package/dist/src/tracker/adapters.js +28 -0
- package/dist/src/tracker/card.js +32 -0
- package/dist/src/tracker/closer.js +76 -0
- package/dist/src/tracker/closes.js +45 -0
- package/dist/src/tracker/comments.js +24 -0
- package/dist/src/tracker/control.js +138 -0
- package/dist/src/tracker/diagnostics.js +38 -0
- package/dist/src/tracker/health.js +1 -0
- package/dist/src/tracker/labels.js +25 -0
- package/dist/src/tracker/link.js +125 -0
- package/dist/src/tracker/model.js +82 -0
- package/dist/src/tracker/move-check.js +14 -0
- package/dist/src/tracker/mover.js +67 -0
- package/dist/src/tracker/parents.js +32 -0
- package/dist/src/tracker/poller.js +60 -0
- package/dist/src/tracker/questions.js +127 -0
- package/dist/src/tracker/readers.js +95 -0
- package/dist/src/tracker/source.js +66 -0
- package/dist/src/tracker/start.js +256 -0
- package/dist/src/tracker/statuses.js +22 -0
- package/dist/src/tracker/trackers.js +148 -0
- package/dist/src/tracker/vocabulary.js +33 -0
- package/dist/src/updates/checkout.js +16 -0
- package/dist/src/updates/control.js +139 -0
- package/dist/src/updates/launch.js +40 -0
- package/dist/src/updates/model.js +105 -0
- package/dist/src/updates/prefix.js +19 -0
- package/dist/src/updates/release.js +100 -0
- package/dist/src/updates/run.js +64 -0
- package/dist/src/updates/runner.js +183 -0
- package/dist/src/updates/settle.js +18 -0
- package/dist/src/updates/steps.js +63 -0
- package/dist/src/updates/updater.js +205 -0
- package/dist/src/updates/watcher.js +86 -0
- package/dist/tally/src/classify.js +144 -0
- package/dist/tally/src/closed-tasks.js +25 -0
- package/dist/tally/src/collect-worker.js +17 -0
- package/dist/tally/src/collect.js +212 -0
- package/dist/tally/src/compact.js +33 -0
- package/dist/tally/src/config.js +150 -0
- package/dist/tally/src/copilot-turns.js +101 -0
- package/dist/tally/src/copilot.js +173 -0
- package/dist/tally/src/db.js +562 -0
- package/dist/tally/src/handoffs.js +126 -0
- package/dist/tally/src/host-worker.js +18 -0
- package/dist/tally/src/huly-profiles.js +38 -0
- package/dist/tally/src/huly.js +123 -0
- package/dist/tally/src/limits.js +21 -0
- package/dist/tally/src/load.js +89 -0
- package/dist/tally/src/local.js +47 -0
- package/dist/tally/src/main.js +39 -0
- package/dist/tally/src/migrations/2-false-rule-deletions.sql +41 -0
- package/dist/tally/src/migrations/3-gpt-family.sql +20 -0
- package/dist/tally/src/migrations/4-compactions.sql +49 -0
- package/dist/tally/src/migrations/5-milestones.sql +25 -0
- package/dist/tally/src/migrations/6-task-sources.sql +6 -0
- package/dist/tally/src/migrations/7-task-followups.sql +28 -0
- package/dist/tally/src/migrations/8-handoff-headings.sql +12 -0
- package/dist/tally/src/migrations/9-codex-stray-counts.sql +15 -0
- package/dist/tally/src/migrations.js +80 -0
- package/dist/tally/src/opencode-turns.js +79 -0
- package/dist/tally/src/opencode.js +115 -0
- package/dist/tally/src/parse.js +192 -0
- package/dist/tally/src/prompt.js +19 -0
- package/dist/tally/src/rollout-turns.js +181 -0
- package/dist/tally/src/rollout.js +159 -0
- package/dist/tally/src/rule-changes.js +39 -0
- package/dist/tally/src/rule-history.js +88 -0
- package/dist/tally/src/rule-hook-size.js +23 -0
- package/dist/tally/src/rule-hook.js +177 -0
- package/dist/tally/src/rule-imports.js +76 -0
- package/dist/tally/src/rule-parse.js +178 -0
- package/dist/tally/src/rule-repair.js +27 -0
- package/dist/tally/src/rule-scan.js +179 -0
- package/dist/tally/src/rule-steps.js +144 -0
- package/dist/tally/src/rules.js +230 -0
- package/dist/tally/src/scan.js +59 -0
- package/dist/tally/src/schema.sql +702 -0
- package/dist/tally/src/signs.js +179 -0
- package/dist/tally/src/step-runs.js +274 -0
- package/dist/tally/src/steps.js +140 -0
- package/dist/tally/src/stream.js +73 -0
- package/dist/tally/src/task.js +16 -0
- package/dist/tally/src/turns.js +114 -0
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/assets/Loader-BoP9PE7x.js +31 -0
- package/dist/web/assets/Loader-DJJTBsaF.css +1 -0
- package/dist/web/assets/TaskCard-Br2QIllc.css +1 -0
- package/dist/web/assets/TaskCard-DNn-Qm8d.js +2 -0
- package/dist/web/assets/TranscriptWindow-2afYjYo9.css +1 -0
- package/dist/web/assets/TranscriptWindow-CFE2kEuL.js +4 -0
- package/dist/web/assets/feedLine-C_AM4QQT.js +1 -0
- package/dist/web/assets/index-BY7ediF2.js +25 -0
- package/dist/web/assets/index-Di7tEbPf.css +1 -0
- package/dist/web/assets/media-DR8nAbPy.css +1 -0
- package/dist/web/assets/media-DfHUEnGn.js +1 -0
- package/dist/web/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/dist/web/assets/useModels-1oIk0mDX.css +1 -0
- package/dist/web/assets/useModels-BmTGsRQz.js +15 -0
- package/dist/web/favicon.svg +1 -0
- package/dist/web/icon-192.png +0 -0
- package/dist/web/icon-512.png +0 -0
- package/dist/web/icon-maskable-512.png +0 -0
- package/dist/web/index.html +25 -0
- package/dist/web/manifest.webmanifest +16 -0
- package/dist/web/stats-preview.png +0 -0
- package/even-keel.config.example.json +59 -0
- package/package.json +118 -7
- package/plugins/checkpoint/.claude-plugin/plugin.json +9 -0
- package/plugins/checkpoint/README.md +85 -0
- package/plugins/checkpoint/hooks/checkpoint-mark.mjs +26 -0
- package/plugins/checkpoint/hooks/context-checkpoint.mjs +28 -0
- package/plugins/checkpoint/hooks/evenkeel.mjs +117 -0
- package/plugins/checkpoint/hooks/handoff.mjs +9 -0
- package/plugins/checkpoint/hooks/hooks.json +120 -0
- package/plugins/checkpoint/hooks/mailbox.mjs +36 -0
- package/plugins/checkpoint/hooks/merge-check.mjs +28 -0
- package/plugins/checkpoint/hooks/merges.mjs +103 -0
- package/plugins/checkpoint/hooks/nudges.mjs +78 -0
- package/plugins/checkpoint/hooks/permission.mjs +49 -0
- package/plugins/checkpoint/hooks/schema-hint.mjs +53 -0
- package/plugins/checkpoint/hooks/standard-gate.mjs +51 -0
- package/plugins/checkpoint/hooks/standards.mjs +258 -0
- package/plugins/checkpoint/opencode/checkpoint.mjs +85 -0
- package/plugins/github/.claude-plugin/plugin.json +9 -0
- package/plugins/github/.mcp.json +12 -0
- package/plugins/github/README.md +73 -0
- package/plugins/github/skills/task/SKILL.md +62 -0
- package/plugins/gitlab/.claude-plugin/plugin.json +7 -0
- package/plugins/gitlab/.mcp.json +13 -0
- package/plugins/gitlab/README.md +29 -0
- package/plugins/gitlab/skills/task/SKILL.md +55 -0
- package/plugins/huly/.claude-plugin/plugin.json +9 -0
- package/plugins/huly/.mcp.json +11 -0
- package/plugins/huly/README.md +60 -0
- package/plugins/huly/hooks/calibration-context.mjs +44 -0
- package/plugins/huly/hooks/calibration.mjs +80 -0
- package/plugins/huly/hooks/check-marks.mjs +23 -0
- package/plugins/huly/hooks/hooks.json +26 -0
- package/plugins/huly/hooks/marks.mjs +58 -0
- package/plugins/huly/hooks/show-calibration.mjs +19 -0
- package/plugins/huly/opencode/huly.mjs +31 -0
- package/plugins/huly/skills/setup/SKILL.md +51 -0
- package/plugins/huly/skills/split/SKILL.md +53 -0
- package/plugins/huly/skills/task/SKILL.md +81 -0
- package/plugins/jira/.claude-plugin/plugin.json +7 -0
- package/plugins/jira/.mcp.json +11 -0
- package/plugins/jira/README.md +39 -0
- package/plugins/jira/skills/task/SKILL.md +63 -0
- package/plugins/linear/.claude-plugin/plugin.json +9 -0
- package/plugins/linear/.mcp.json +11 -0
- package/plugins/linear/README.md +71 -0
- package/plugins/linear/skills/task/SKILL.md +62 -0
- package/plugins/tally/.claude-plugin/plugin.json +9 -0
- package/plugins/tally/README.md +42 -0
- package/plugins/tally/bin/locate.mjs +48 -0
- package/plugins/tally/bin/sql.mjs +21 -0
- package/plugins/tally/skills/session-audit/SKILL.md +333 -0
- package/service/EvenKeelHost.cs +133 -0
- package/service/EvenKeelService.cs +271 -0
- package/service/EvenKeelTray.cs +258 -0
- package/service/install.ps1 +95 -0
- package/service/install.sh +143 -0
- package/service/uninstall.ps1 +19 -0
- package/service/uninstall.sh +37 -0
- package/statusline/statusline.mjs +46 -0
- package/tally/bin/rule-hook.mjs +26 -0
- package/tally/bin/sql.mjs +166 -0
- package/tally/tally.config.json +59 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { CLAUDE } from '../claude/name.js';
|
|
5
|
+
import { CODEX } from '../codex/name.js';
|
|
6
|
+
import { tokenOf, writeToken } from '../copilot/home.js';
|
|
7
|
+
import { COPILOT } from '../copilot/name.js';
|
|
8
|
+
import { fail } from '../notice/notice.js';
|
|
9
|
+
import { keyOf, keyVendor, replaceKey, writeKey } from '../opencode/home.js';
|
|
10
|
+
import { OPENCODE } from '../opencode/name.js';
|
|
11
|
+
import { samePath } from '../paths.js';
|
|
12
|
+
import { KeychainCredentials, keychainService } from './keychain.js';
|
|
13
|
+
import { CODEX_ACCOUNT, folderName, GLOBAL_ACCOUNT, parseCredentialTimes } from './model.js';
|
|
14
|
+
import { API_KEY_VARIABLE, shareConfig } from './share.js';
|
|
15
|
+
const PENDING_PREFIX = '.pending-';
|
|
16
|
+
const LOCK_SUFFIX = '.lock';
|
|
17
|
+
const APPROVED_SUFFIX = 20;
|
|
18
|
+
const API_KEY_PATTERN = new RegExp(`^\\S{${APPROVED_SUFFIX + 1},}$`);
|
|
19
|
+
const RESERVED = [GLOBAL_ACCOUNT, CODEX_ACCOUNT];
|
|
20
|
+
const CODEX_AUTH = 'auth.json';
|
|
21
|
+
const CODEX_KEY_FIELD = 'OPENAI_API_KEY';
|
|
22
|
+
/**
|
|
23
|
+
* The account folders: explicitly registered global folders and one `CLAUDE_CONFIG_DIR` per account under the
|
|
24
|
+
* root; Codex's own folder (`CODEX_HOME` or `~/.codex`) when registered, and one
|
|
25
|
+
* `CODEX_HOME` per account under the Codex root (BTN-176). A folder's name is its key across both roots: a Codex folder
|
|
26
|
+
* named like a Claude one is left out, and a new one is named apart from both.
|
|
27
|
+
*/
|
|
28
|
+
export class AccountHomes {
|
|
29
|
+
env;
|
|
30
|
+
registration;
|
|
31
|
+
keychain;
|
|
32
|
+
global;
|
|
33
|
+
codex;
|
|
34
|
+
roots = {};
|
|
35
|
+
constructor(env, root, codexRoot = codexAccountsRoot(env), openCodeRoot = openCodeAccountsRoot(env), copilotRoot = copilotAccountsRoot(env), registration = { keys: new Set(), save: async (_key, _added) => { } },
|
|
36
|
+
/** Where a Claude sign-in is when the folder has no `.credentials.json`: the Keychain on macOS, nowhere elsewhere. */
|
|
37
|
+
keychain = process.platform === 'darwin' ? new KeychainCredentials() : null) {
|
|
38
|
+
this.env = env;
|
|
39
|
+
this.registration = registration;
|
|
40
|
+
this.keychain = keychain;
|
|
41
|
+
this.global = { key: GLOBAL_ACCOUNT, dir: env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'), global: true, agent: CLAUDE };
|
|
42
|
+
this.codex = { key: CODEX_ACCOUNT, dir: env.CODEX_HOME ?? join(homedir(), '.codex'), global: true, agent: CODEX };
|
|
43
|
+
this.setRoots(root, codexRoot, openCodeRoot, copilotRoot);
|
|
44
|
+
}
|
|
45
|
+
/** The folders under the new roots count from the next `list`: `EVEN_KEEL_ACCOUNTS_DIR` changes on the fly. */
|
|
46
|
+
setRoots(root, codexRoot, openCodeRoot = openCodeAccountsRoot(this.env), copilotRoot = copilotAccountsRoot(this.env)) {
|
|
47
|
+
this.roots = { [CLAUDE]: root, [CODEX]: codexRoot, [OPENCODE]: openCodeRoot, [COPILOT]: copilotRoot };
|
|
48
|
+
this.forget();
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* What `list` and `times` read, kept until the current synchronous run ends (BTN-680): one state broadcast or limits
|
|
52
|
+
* publish asks for it per account and per project, and each read is four `readdirSync` on the event loop. A change
|
|
53
|
+
* made through this class forgets it at once; one made by another process shows from the next run.
|
|
54
|
+
*/
|
|
55
|
+
memo;
|
|
56
|
+
remembered() {
|
|
57
|
+
if (this.memo === undefined) {
|
|
58
|
+
const memo = { times: new Map() };
|
|
59
|
+
this.memo = memo;
|
|
60
|
+
queueMicrotask(() => {
|
|
61
|
+
if (this.memo === memo)
|
|
62
|
+
this.memo = undefined;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return this.memo;
|
|
66
|
+
}
|
|
67
|
+
forget() {
|
|
68
|
+
this.memo = undefined;
|
|
69
|
+
this.keychain?.forget();
|
|
70
|
+
}
|
|
71
|
+
list() {
|
|
72
|
+
const memo = this.remembered();
|
|
73
|
+
memo.list ??= this.read();
|
|
74
|
+
return [...memo.list];
|
|
75
|
+
}
|
|
76
|
+
read() {
|
|
77
|
+
const claude = this.folders(CLAUDE);
|
|
78
|
+
const codex = this.folders(CODEX).filter((name) => !claude.includes(name));
|
|
79
|
+
const openCode = this.folders(OPENCODE).filter((name) => !claude.includes(name) && !codex.includes(name));
|
|
80
|
+
const copilot = this.folders(COPILOT).filter((name) => !claude.includes(name) && !codex.includes(name) && !openCode.includes(name));
|
|
81
|
+
return [
|
|
82
|
+
...(this.registered(this.global) ? [this.global] : []),
|
|
83
|
+
...claude.map((name) => this.home(CLAUDE, name)),
|
|
84
|
+
...(this.registered(this.codex) ? [this.codex] : []),
|
|
85
|
+
...codex.map((name) => this.home(CODEX, name)),
|
|
86
|
+
...openCode.map((name) => this.home(OPENCODE, name)),
|
|
87
|
+
...copilot.map((name) => this.home(COPILOT, name)),
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
find(key) {
|
|
91
|
+
return this.list().find((home) => home.key === key);
|
|
92
|
+
}
|
|
93
|
+
/** Existing history folders, including global folders not registered for running sessions. */
|
|
94
|
+
all() {
|
|
95
|
+
return [...this.list(), ...this.availableGlobals()];
|
|
96
|
+
}
|
|
97
|
+
availableGlobals() {
|
|
98
|
+
return [this.global, this.codex].filter((home) => existsSync(home.dir) && !this.registration.keys.has(home.key));
|
|
99
|
+
}
|
|
100
|
+
async register(home) {
|
|
101
|
+
if (!this.availableGlobals().some((each) => each.key === home.key))
|
|
102
|
+
throw fail('account.unknown', { key: home.key });
|
|
103
|
+
await this.registration.save(home.key, true);
|
|
104
|
+
this.registration.keys.add(home.key);
|
|
105
|
+
this.forget();
|
|
106
|
+
}
|
|
107
|
+
registered(home) {
|
|
108
|
+
return this.registration.keys.has(home.key) && existsSync(home.dir);
|
|
109
|
+
}
|
|
110
|
+
ownerOf(configDir) {
|
|
111
|
+
if (configDir === null)
|
|
112
|
+
return this.find(GLOBAL_ACCOUNT)?.key;
|
|
113
|
+
return this.list().find((home) => samePath(home.dir, configDir))?.key;
|
|
114
|
+
}
|
|
115
|
+
times(home) {
|
|
116
|
+
const { times } = this.remembered();
|
|
117
|
+
const known = times.get(home.dir);
|
|
118
|
+
if (known !== undefined)
|
|
119
|
+
return known;
|
|
120
|
+
let text;
|
|
121
|
+
try {
|
|
122
|
+
text = readFileSync(join(home.dir, '.credentials.json'), 'utf8');
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
text = this.keychainText(home);
|
|
126
|
+
}
|
|
127
|
+
const parsed = parseCredentialTimes(text);
|
|
128
|
+
times.set(home.dir, parsed);
|
|
129
|
+
return parsed;
|
|
130
|
+
}
|
|
131
|
+
/** The CLI names the item after the `CLAUDE_CONFIG_DIR` it ran with, the one `claudeEnv` gives the folder. */
|
|
132
|
+
keychainText(home) {
|
|
133
|
+
if (this.keychain === null || home.agent !== CLAUDE)
|
|
134
|
+
return undefined;
|
|
135
|
+
const configDir = home.global ? this.env.CLAUDE_CONFIG_DIR : home.dir;
|
|
136
|
+
return this.keychain.read(keychainService(configDir));
|
|
137
|
+
}
|
|
138
|
+
/** A folder to sign a new account of the agent in; Codex does not create its folder, so it is made here for both. */
|
|
139
|
+
pending(now, agent) {
|
|
140
|
+
const home = this.home(agent, `${PENDING_PREFIX}${now.getTime().toString(36)}`);
|
|
141
|
+
mkdirSync(home.dir, { recursive: true });
|
|
142
|
+
this.forget();
|
|
143
|
+
return home;
|
|
144
|
+
}
|
|
145
|
+
place(pending, email) {
|
|
146
|
+
const name = folderName(email, this.taken());
|
|
147
|
+
const home = this.home(pending.agent, name);
|
|
148
|
+
renameSync(pending.dir, home.dir);
|
|
149
|
+
if (pending.agent === CLAUDE)
|
|
150
|
+
this.keychain?.move(keychainService(pending.dir), keychainService(home.dir));
|
|
151
|
+
this.forget();
|
|
152
|
+
return home;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* An API account. Claude's: a folder whose `settings.json` carries the key (`mergeSettings` keeps it there) and whose
|
|
156
|
+
* `.claude.json` has it approved, or the CLI asks whether to use it (BTN-136). Codex's: an `auth.json` the way
|
|
157
|
+
* `codex login --with-api-key` writes it (BTN-177). OpenCode's: an `auth.json` under the provider the key is of (BTN-214).
|
|
158
|
+
* Copilot's: the GitHub token in `even-keel.json` (BTN-215).
|
|
159
|
+
*/
|
|
160
|
+
addApiKey(name, key, agent = CLAUDE) {
|
|
161
|
+
const value = assertApiKey(key, agent);
|
|
162
|
+
const home = this.home(agent, folderName(name.trim() === '' ? (agent === COPILOT ? 'github' : 'api') : name, this.taken()));
|
|
163
|
+
mkdirSync(home.dir, { recursive: true });
|
|
164
|
+
this.forget();
|
|
165
|
+
if (agent === COPILOT) {
|
|
166
|
+
writeToken(home.dir, value);
|
|
167
|
+
return home;
|
|
168
|
+
}
|
|
169
|
+
if (agent === OPENCODE) {
|
|
170
|
+
writeKey(home.dir, value);
|
|
171
|
+
return home;
|
|
172
|
+
}
|
|
173
|
+
if (agent === CODEX) {
|
|
174
|
+
writeFileSync(join(home.dir, CODEX_AUTH), JSON.stringify({ auth_mode: 'apikey', [CODEX_KEY_FIELD]: value }, null, 2));
|
|
175
|
+
return home;
|
|
176
|
+
}
|
|
177
|
+
writeFileSync(join(home.dir, 'settings.json'), JSON.stringify({ env: { [API_KEY_VARIABLE]: value } }, null, 2));
|
|
178
|
+
const approved = { customApiKeyResponses: { approved: [value.slice(-APPROVED_SUFFIX)], rejected: [] } };
|
|
179
|
+
writeFileSync(join(home.dir, '.claude.json'), JSON.stringify(approved, null, 2));
|
|
180
|
+
return home;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The trimmed key that may take the place of the folder's own (BTN-418): of the right form, into a folder that holds its
|
|
184
|
+
* key itself — not the agent's own folder, not one with `apiKeyHelper` — and of the provider of the key it holds.
|
|
185
|
+
*/
|
|
186
|
+
replacement(home, key) {
|
|
187
|
+
const value = assertApiKey(key, home.agent);
|
|
188
|
+
const held = home.global ? null : this.apiKeyOf(home);
|
|
189
|
+
if (held === null)
|
|
190
|
+
throw fail('account.keyNotHeld', { agent: home.agent });
|
|
191
|
+
const provider = keyProvider(home.agent, held);
|
|
192
|
+
if (keyProvider(home.agent, value) !== provider)
|
|
193
|
+
throw fail('account.keyOtherProvider', { provider });
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* A new key in the place of the folder's own, the rest of its files as they are: Claude's `settings.json` and the
|
|
198
|
+
* approval in `.claude.json` (the old key's suffix goes), Codex's `auth.json`, OpenCode's provider entry, Copilot's token.
|
|
199
|
+
*/
|
|
200
|
+
replaceApiKey(home, key) {
|
|
201
|
+
const held = this.apiKeyOf(home);
|
|
202
|
+
const value = this.replacement(home, key);
|
|
203
|
+
if (home.agent === COPILOT)
|
|
204
|
+
return writeToken(home.dir, value);
|
|
205
|
+
if (home.agent === OPENCODE)
|
|
206
|
+
return replaceKey(home.dir, value);
|
|
207
|
+
if (home.agent === CODEX) {
|
|
208
|
+
const file = join(home.dir, CODEX_AUTH);
|
|
209
|
+
writeFileSync(file, JSON.stringify({ ...readObject(file), [CODEX_KEY_FIELD]: value }, null, 2));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const settingsFile = join(home.dir, 'settings.json');
|
|
213
|
+
const settings = readObject(settingsFile);
|
|
214
|
+
writeFileSync(settingsFile, JSON.stringify({ ...settings, env: { ...objectOf(settings.env), [API_KEY_VARIABLE]: value } }, null, 2));
|
|
215
|
+
const configFile = this.configFile(home);
|
|
216
|
+
const config = readObject(configFile);
|
|
217
|
+
const responses = objectOf(config.customApiKeyResponses);
|
|
218
|
+
const approved = (Array.isArray(responses.approved) ? responses.approved : []).filter((suffix) => suffix !== held?.slice(-APPROVED_SUFFIX));
|
|
219
|
+
const rejected = Array.isArray(responses.rejected) ? responses.rejected : [];
|
|
220
|
+
const customApiKeyResponses = { ...responses, approved: [...approved, value.slice(-APPROVED_SUFFIX)], rejected };
|
|
221
|
+
writeFileSync(configFile, JSON.stringify({ ...config, customApiKeyResponses }, null, 2));
|
|
222
|
+
}
|
|
223
|
+
/** The key in Claude's `settings.json`, Codex's or OpenCode's `auth.json`; a folder with `apiKeyHelper` has none to read. */
|
|
224
|
+
apiKeyOf(home) {
|
|
225
|
+
if (home.agent === OPENCODE)
|
|
226
|
+
return keyOf(home.dir);
|
|
227
|
+
if (home.agent === COPILOT)
|
|
228
|
+
return tokenOf(home.dir);
|
|
229
|
+
try {
|
|
230
|
+
const key = home.agent === CODEX
|
|
231
|
+
? JSON.parse(readFileSync(join(home.dir, CODEX_AUTH), 'utf8'))?.[CODEX_KEY_FIELD]
|
|
232
|
+
: JSON.parse(readFileSync(join(home.dir, 'settings.json'), 'utf8'))?.env?.[API_KEY_VARIABLE];
|
|
233
|
+
return typeof key === 'string' && key !== '' ? key : null;
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** Claude's shared configuration into an account folder; Codex reads its own `config.toml`. */
|
|
240
|
+
share(home) {
|
|
241
|
+
if (home.global || home.agent !== CLAUDE)
|
|
242
|
+
return false;
|
|
243
|
+
return shareConfig({ dir: this.global.dir, config: this.configFile(this.global) }, home.dir);
|
|
244
|
+
}
|
|
245
|
+
/** The `.claude.json` the CLI reads with this folder. */
|
|
246
|
+
configFile(home) {
|
|
247
|
+
if (!home.global)
|
|
248
|
+
return join(home.dir, '.claude.json');
|
|
249
|
+
return this.env.CLAUDE_CONFIG_DIR === undefined ? join(homedir(), '.claude.json') : join(this.global.dir, '.claude.json');
|
|
250
|
+
}
|
|
251
|
+
async remove(home) {
|
|
252
|
+
if (home.global) {
|
|
253
|
+
await this.registration.save(home.key, false);
|
|
254
|
+
this.registration.keys.delete(home.key);
|
|
255
|
+
this.forget();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
rmSync(home.dir, { recursive: true, force: true, maxRetries: 3 });
|
|
259
|
+
this.forget();
|
|
260
|
+
}
|
|
261
|
+
home(agent, name) {
|
|
262
|
+
const root = this.roots[agent];
|
|
263
|
+
if (root === undefined)
|
|
264
|
+
throw fail('request.required', { field: 'agent' });
|
|
265
|
+
return { key: name, dir: join(root, name), global: false, agent };
|
|
266
|
+
}
|
|
267
|
+
taken() {
|
|
268
|
+
return Object.keys(this.roots).flatMap((agent) => this.folders(agent));
|
|
269
|
+
}
|
|
270
|
+
folders(agent) {
|
|
271
|
+
try {
|
|
272
|
+
return readdirSync(this.roots[agent], { withFileTypes: true })
|
|
273
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.') && !entry.name.endsWith(LOCK_SUFFIX) && !RESERVED.includes(entry.name))
|
|
274
|
+
.map((entry) => entry.name)
|
|
275
|
+
.sort();
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/** A JSON object file; an empty object for one missing, broken or holding something else. */
|
|
283
|
+
function readObject(file) {
|
|
284
|
+
try {
|
|
285
|
+
return objectOf(JSON.parse(readFileSync(file, 'utf8')));
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return {};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function objectOf(value) {
|
|
292
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : {};
|
|
293
|
+
}
|
|
294
|
+
/** The trimmed key, or a refusal of one that is not a single word long enough to approve. */
|
|
295
|
+
export function assertApiKey(key, agent = CLAUDE) {
|
|
296
|
+
const value = key.trim();
|
|
297
|
+
if (agent === COPILOT) {
|
|
298
|
+
if (!API_KEY_PATTERN.test(value) || !COPILOT_TOKEN.test(value))
|
|
299
|
+
throw fail('account.copilotTokenInvalid', {});
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
if (!API_KEY_PATTERN.test(value))
|
|
303
|
+
throw fail(agent === CODEX ? 'account.codexKeyInvalid' : 'account.apiKeyInvalid', {});
|
|
304
|
+
return value;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* The tokens Copilot CLI takes: a fine-grained personal one (`github_pat_`) or an OAuth one of an app (`gho_`, `ghu_`);
|
|
308
|
+
* a classic `ghp_` is refused by the CLI.
|
|
309
|
+
*/
|
|
310
|
+
const COPILOT_TOKEN = /^(github_pat_|gho_|ghu_)/;
|
|
311
|
+
/** Whose key an API account of the agent holds: Codex's is OpenAI's, OpenCode's either, by its prefix, Copilot's GitHub's. */
|
|
312
|
+
export function keyProvider(agent, key) {
|
|
313
|
+
if (agent === COPILOT)
|
|
314
|
+
return 'github';
|
|
315
|
+
if (agent === OPENCODE)
|
|
316
|
+
return keyVendor(key);
|
|
317
|
+
return agent === CODEX ? 'openai' : 'anthropic';
|
|
318
|
+
}
|
|
319
|
+
export function accountsRoot(env) {
|
|
320
|
+
return env.EVEN_KEEL_ACCOUNTS_DIR ?? join(homedir(), '.claude-accounts');
|
|
321
|
+
}
|
|
322
|
+
export function codexAccountsRoot(env) {
|
|
323
|
+
return env.EVEN_KEEL_CODEX_ACCOUNTS_DIR ?? join(homedir(), '.codex-accounts');
|
|
324
|
+
}
|
|
325
|
+
/** OpenCode has no folder of its own to stand for an account: every one of them is under this root. */
|
|
326
|
+
export function openCodeAccountsRoot(env) {
|
|
327
|
+
return env.EVEN_KEEL_OPENCODE_ACCOUNTS_DIR ?? join(homedir(), '.opencode-accounts');
|
|
328
|
+
}
|
|
329
|
+
/** Copilot's own `~/.copilot` is not an account here: its sign-in is the OS's, shared by every folder (BTN-215). */
|
|
330
|
+
export function copilotAccountsRoot(env) {
|
|
331
|
+
return env.EVEN_KEEL_COPILOT_ACCOUNTS_DIR ?? join(homedir(), '.copilot-accounts');
|
|
332
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
const SERVICE = 'Claude Code-credentials';
|
|
4
|
+
const NOT_FOUND = 44;
|
|
5
|
+
const TTL_MS = 60_000;
|
|
6
|
+
const TIMEOUT_MS = 3_000;
|
|
7
|
+
const HEX = /^(?:[0-9a-f]{2})+$/i;
|
|
8
|
+
/**
|
|
9
|
+
* The macOS Keychain item Claude Code keeps a folder's sign-in in, named as the CLI names it: no `CLAUDE_CONFIG_DIR`
|
|
10
|
+
* gives the bare name, a folder adds `-` and the first 8 hex of the sha256 of its path in NFC (`docs/accounts.md`).
|
|
11
|
+
*/
|
|
12
|
+
export function keychainService(configDir) {
|
|
13
|
+
if (!configDir)
|
|
14
|
+
return SERVICE;
|
|
15
|
+
return `${SERVICE}-${createHash('sha256').update(configDir.normalize('NFC')).digest('hex').slice(0, 8)}`;
|
|
16
|
+
}
|
|
17
|
+
function runSecurity(args, input) {
|
|
18
|
+
const result = spawnSync('security', args, { encoding: 'utf8', input, timeout: TIMEOUT_MS, windowsHide: true });
|
|
19
|
+
const timedOut = result.error?.code === 'ETIMEDOUT' || result.signal !== null;
|
|
20
|
+
return { status: result.status, stdout: result.stdout ?? '', timedOut };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Reads the secret of a Keychain item through `security find-generic-password -w`. An answer is kept for a minute, as
|
|
24
|
+
* the state broadcast asks for it per account. A read that timed out may be held by an access dialog: that item is
|
|
25
|
+
* never asked for again until the service restarts, so the dialog does not come back every minute.
|
|
26
|
+
*/
|
|
27
|
+
export class KeychainCredentials {
|
|
28
|
+
security;
|
|
29
|
+
warn;
|
|
30
|
+
answers = new Map();
|
|
31
|
+
blocked = new Set();
|
|
32
|
+
constructor(security = runSecurity, warn = console.warn) {
|
|
33
|
+
this.security = security;
|
|
34
|
+
this.warn = warn;
|
|
35
|
+
}
|
|
36
|
+
read(service, now = Date.now()) {
|
|
37
|
+
if (this.blocked.has(service))
|
|
38
|
+
return undefined;
|
|
39
|
+
const known = this.answers.get(service);
|
|
40
|
+
if (known !== undefined && now - known.at < TTL_MS)
|
|
41
|
+
return known.text;
|
|
42
|
+
const answer = this.security(['find-generic-password', '-s', service, '-w']);
|
|
43
|
+
if (answer.timedOut) {
|
|
44
|
+
this.blocked.add(service);
|
|
45
|
+
this.warn(`Keychain: reading "${service}" took over ${TIMEOUT_MS} ms, likely an access dialog; not asked again until a restart`);
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const text = answer.status === 0 ? decode(answer.stdout.trim()) : undefined;
|
|
49
|
+
this.answers.set(service, { text, at: now });
|
|
50
|
+
return text;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Carries a folder's sign-in item to the name of the folder's new path (BTN-745): the `.pending-*` folder of "Add
|
|
54
|
+
* account" is renamed after the sign-in, and its item would stay behind matching no folder. The old item is found by
|
|
55
|
+
* its name alone and keeps its own account (`acct`): the CLI writes the macOS user name there (BTN-748). An item
|
|
56
|
+
* already under the new name, whatever its account, wins and the old one is only deleted; a failed write keeps the old
|
|
57
|
+
* one. The secret goes to `security -i` on stdin as hex, as the CLI writes it, never in the arguments.
|
|
58
|
+
*/
|
|
59
|
+
move(from, to) {
|
|
60
|
+
this.forget();
|
|
61
|
+
const found = this.ask(['find-generic-password', '-s', from]);
|
|
62
|
+
if (found === undefined)
|
|
63
|
+
return;
|
|
64
|
+
if (found.status !== 0) {
|
|
65
|
+
this.warn(found.status === NOT_FOUND
|
|
66
|
+
? `Keychain: no item "${from}" to carry to "${to}"; the sign-in may be in .credentials.json`
|
|
67
|
+
: `Keychain: could not look up "${from}" (security exit ${found.status}); nothing carried to "${to}"`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const account = accountOf(found.stdout);
|
|
71
|
+
if (account !== null && !QUOTABLE.test(account)) {
|
|
72
|
+
this.warn(`Keychain: the account of "${from}" cannot be passed to security -i; the sign-in stays there`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const by = account === null ? [] : ['-a', account];
|
|
76
|
+
const old = this.ask(['find-generic-password', ...by, '-s', from, '-w']);
|
|
77
|
+
if (old === undefined)
|
|
78
|
+
return;
|
|
79
|
+
const text = old.status === 0 ? decode(old.stdout.trim()) : undefined;
|
|
80
|
+
if (text === undefined) {
|
|
81
|
+
this.warn(`Keychain: could not read "${from}"; nothing carried to "${to}"`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const present = this.ask(['find-generic-password', '-s', to]);
|
|
85
|
+
if (present === undefined)
|
|
86
|
+
return;
|
|
87
|
+
if (present.status === NOT_FOUND) {
|
|
88
|
+
const hex = Buffer.from(text, 'utf8').toString('hex');
|
|
89
|
+
const as = account === null ? '' : ` -a "${account}"`;
|
|
90
|
+
const written = this.ask(['-i'], `add-generic-password -U${as} -s "${to}" -X "${hex}"\n`);
|
|
91
|
+
if (written?.status !== 0) {
|
|
92
|
+
this.warn(`Keychain: could not write "${to}"; the sign-in stays under "${from}"`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else if (present.status !== 0) {
|
|
97
|
+
this.warn(`Keychain: could not look up "${to}" (security exit ${present.status}); the sign-in stays under "${from}"`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (this.ask(['delete-generic-password', ...by, '-s', from])?.status !== 0)
|
|
101
|
+
this.warn(`Keychain: could not delete "${from}"`);
|
|
102
|
+
}
|
|
103
|
+
/** One `security` call; `undefined` when it timed out, likely on an access dialog. */
|
|
104
|
+
ask(args, input) {
|
|
105
|
+
const answer = this.security(args, input);
|
|
106
|
+
if (!answer.timedOut)
|
|
107
|
+
return answer;
|
|
108
|
+
this.warn(`Keychain: "security ${args[0]}" took over ${TIMEOUT_MS} ms, likely an access dialog`);
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
forget() {
|
|
112
|
+
this.answers.clear();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The `acct` attribute from the dump of `find-generic-password` without `-w`: `"acct"<blob>="m1"`, or `0x<hex>` and an
|
|
117
|
+
* escaped copy when a byte is outside printable ASCII; `null` for `<NULL>` or no such line.
|
|
118
|
+
*/
|
|
119
|
+
function accountOf(dump) {
|
|
120
|
+
const line = /^\s*"acct"<blob>=(?:"(.*)"|0x([0-9a-f]*))/im.exec(dump);
|
|
121
|
+
if (line === null)
|
|
122
|
+
return null;
|
|
123
|
+
return line[1] ?? Buffer.from(line[2], 'hex').toString('utf8');
|
|
124
|
+
}
|
|
125
|
+
/** `security -i` splits its line on quotes: an account with a quote, a backslash or a control character is not passed. */
|
|
126
|
+
const QUOTABLE = /^[^"\\\x00-\x1f\x7f]*$/;
|
|
127
|
+
/** `security -w` prints a secret that is not plain text as hex. */
|
|
128
|
+
function decode(text) {
|
|
129
|
+
if (!text)
|
|
130
|
+
return undefined;
|
|
131
|
+
if (text.startsWith('{') || !HEX.test(text))
|
|
132
|
+
return text;
|
|
133
|
+
return Buffer.from(text, 'hex').toString('utf8');
|
|
134
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const MODELS_URL = 'https://api.anthropic.com/v1/models?limit=1';
|
|
2
|
+
const OPENAI_MODELS_URL = 'https://api.openai.com/v1/models';
|
|
3
|
+
const OPENAI_ME_URL = 'https://api.openai.com/v1/me';
|
|
4
|
+
const GITHUB_USER_URL = 'https://api.github.com/user';
|
|
5
|
+
const API_VERSION = '2023-06-01';
|
|
6
|
+
/** Asks Anthropic whether it takes the key: the models list costs no tokens. 401 and 403 are a refused key. */
|
|
7
|
+
export function checkApiKey(key, timeoutMs, fetchImpl = fetch) {
|
|
8
|
+
return askModels(MODELS_URL, { 'x-api-key': key, 'anthropic-version': API_VERSION }, timeoutMs, fetchImpl, 'anthropic-organization-id');
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The same question to OpenAI for a Codex key (BTN-177). OpenAI's models list carries no `openai-organization` (BTN-649),
|
|
12
|
+
* so a working key's organization is asked of `/v1/me`; a failure there leaves the key working, its organization unknown.
|
|
13
|
+
*/
|
|
14
|
+
export async function checkOpenAiKey(key, timeoutMs, fetchImpl = fetch) {
|
|
15
|
+
const headers = { authorization: `Bearer ${key}` };
|
|
16
|
+
const verdict = await askModels(OPENAI_MODELS_URL, headers, timeoutMs, fetchImpl, 'openai-organization');
|
|
17
|
+
if (verdict.state !== 'ok' || verdict.organization !== undefined)
|
|
18
|
+
return verdict;
|
|
19
|
+
const me = await askModels(OPENAI_ME_URL, headers, timeoutMs, fetchImpl, 'openai-organization');
|
|
20
|
+
return me.state === 'ok' && me.organization !== undefined ? me : verdict;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Whether GitHub takes a Copilot account's token (BTN-215): `/user` answers any valid token for free. Whether it may make
|
|
24
|
+
* Copilot requests is not asked — only a session shows that.
|
|
25
|
+
*/
|
|
26
|
+
export function checkGitHubToken(key, timeoutMs, fetchImpl = fetch) {
|
|
27
|
+
return askModels(GITHUB_USER_URL, { authorization: `Bearer ${key}`, accept: 'application/vnd.github+json', 'user-agent': 'even-keel' }, timeoutMs, fetchImpl);
|
|
28
|
+
}
|
|
29
|
+
async function askModels(url, headers, timeoutMs, fetchImpl, organizationHeader) {
|
|
30
|
+
let response;
|
|
31
|
+
try {
|
|
32
|
+
response = await fetchImpl(url, { headers, signal: AbortSignal.timeout(timeoutMs) });
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
return { state: 'unknown', reason: error instanceof Error ? error.message : String(error) };
|
|
36
|
+
}
|
|
37
|
+
await response.body?.cancel();
|
|
38
|
+
if (response.ok) {
|
|
39
|
+
const organization = organizationHeader === undefined ? '' : (response.headers.get(organizationHeader)?.trim() ?? '');
|
|
40
|
+
return organization === '' ? { state: 'ok' } : { state: 'ok', organization };
|
|
41
|
+
}
|
|
42
|
+
if (response.status === 401 || response.status === 403)
|
|
43
|
+
return { state: 'rejected' };
|
|
44
|
+
return { state: 'unknown', reason: `HTTP ${response.status}` };
|
|
45
|
+
}
|
|
46
|
+
/** The last check of each API account's key: one per interval, and a check that learns nothing keeps the verdict. */
|
|
47
|
+
export class KeyChecks {
|
|
48
|
+
intervalMs;
|
|
49
|
+
checked = new Map();
|
|
50
|
+
constructor(intervalMs) {
|
|
51
|
+
this.intervalMs = intervalMs;
|
|
52
|
+
}
|
|
53
|
+
due(key, now) {
|
|
54
|
+
const last = this.checked.get(key);
|
|
55
|
+
return last === undefined || now.getTime() - last.at >= this.intervalMs;
|
|
56
|
+
}
|
|
57
|
+
note(key, verdict, now) {
|
|
58
|
+
const rejected = verdict.state === 'unknown' ? (this.checked.get(key)?.rejected ?? false) : verdict.state === 'rejected';
|
|
59
|
+
this.checked.set(key, { at: now.getTime(), rejected });
|
|
60
|
+
}
|
|
61
|
+
rejected(key) {
|
|
62
|
+
return this.checked.get(key)?.rejected ?? false;
|
|
63
|
+
}
|
|
64
|
+
forget(key) {
|
|
65
|
+
this.checked.delete(key);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { writeAtomic } from '../storage/atomic.js';
|
|
3
|
+
import { refuseNewer, SCHEMA_VERSION, versionOf } from '../storage/schema.js';
|
|
4
|
+
import { GLOBAL_ACCOUNT, namedAfter } from './model.js';
|
|
5
|
+
import { isTransport } from './transport.js';
|
|
6
|
+
export function emptyLedger() {
|
|
7
|
+
return { accounts: {}, removing: [] };
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* `email` is `undefined` while the folder is signed out (nothing to note) and `null` when it is signed in with no
|
|
11
|
+
* identity, as an OpenCode folder: a key folder reused that way no longer holds the key it was noted with, and a stale
|
|
12
|
+
* record would make every limit of its sessions look like an account switch to the chain.
|
|
13
|
+
*/
|
|
14
|
+
export function noteAccount(ledger, key, email, now) {
|
|
15
|
+
const record = ledger.accounts[key];
|
|
16
|
+
if (email === undefined || email === (record?.email ?? null))
|
|
17
|
+
return undefined;
|
|
18
|
+
const { name, ...kept } = record ?? { email: null, since: null, previous: null };
|
|
19
|
+
// The name the user gave belonged to the account that signed out of the folder, not to the one in it now (BTN-528).
|
|
20
|
+
const another = kept.email !== null && email !== null && kept.email.toLowerCase() !== email.toLowerCase();
|
|
21
|
+
const noted = { ...kept, ...(!another && name !== undefined && { name }), email, since: now.toISOString(), previous: kept.email };
|
|
22
|
+
return { ...ledger, accounts: { ...ledger.accounts, [key]: noted } };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The account the folder `key` is named after (`folderName`) when another one signed in to it since: the page shows the
|
|
26
|
+
* change instead of a silent rename. Null while it holds that account, or when its name came from no sign-in.
|
|
27
|
+
*/
|
|
28
|
+
export function formerOwner(ledger, key) {
|
|
29
|
+
const record = ledger.accounts[key];
|
|
30
|
+
if (record?.previous == null || record.email === null || key === GLOBAL_ACCOUNT)
|
|
31
|
+
return null;
|
|
32
|
+
return namedAfter(key, record.previous) && !namedAfter(key, record.email) ? record.previous : null;
|
|
33
|
+
}
|
|
34
|
+
export function nameAccount(ledger, key, name) {
|
|
35
|
+
const { name: _old, ...record } = ledger.accounts[key] ?? { email: null, since: null, previous: null };
|
|
36
|
+
const named = name.trim() === '' ? record : { ...record, name: name.trim() };
|
|
37
|
+
return { ...ledger, accounts: { ...ledger.accounts, [key]: named } };
|
|
38
|
+
}
|
|
39
|
+
export function accountAt(ledger, key, at) {
|
|
40
|
+
const record = ledger.accounts[key];
|
|
41
|
+
if (record === undefined || record.since === null)
|
|
42
|
+
return null;
|
|
43
|
+
return Date.parse(at) >= Date.parse(record.since) ? record.email : record.previous;
|
|
44
|
+
}
|
|
45
|
+
export function forgetAccount(ledger, key) {
|
|
46
|
+
const { [key]: _forgotten, ...accounts } = ledger.accounts;
|
|
47
|
+
return { ...ledger, accounts, removing: ledger.removing.filter((each) => each !== key) };
|
|
48
|
+
}
|
|
49
|
+
export function markRemoving(ledger, key, on) {
|
|
50
|
+
const removing = ledger.removing.filter((each) => each !== key);
|
|
51
|
+
return { ...ledger, removing: on ? [...removing, key] : removing };
|
|
52
|
+
}
|
|
53
|
+
export function parseLedger(raw) {
|
|
54
|
+
if (!isRecord(raw))
|
|
55
|
+
return emptyLedger();
|
|
56
|
+
if (isRecord(raw.accounts)) {
|
|
57
|
+
const accounts = {};
|
|
58
|
+
for (const [key, value] of Object.entries(raw.accounts))
|
|
59
|
+
if (isRecord(value))
|
|
60
|
+
accounts[key] = recordOf(value);
|
|
61
|
+
const removing = Array.isArray(raw.removing) ? raw.removing.filter((key) => typeof key === 'string') : [];
|
|
62
|
+
if (!isRecord(raw.transports))
|
|
63
|
+
return { accounts, removing };
|
|
64
|
+
const transports = Object.fromEntries(Object.entries(raw.transports).filter((entry) => isTransport(entry[1])));
|
|
65
|
+
return { accounts, removing, transports };
|
|
66
|
+
}
|
|
67
|
+
if ('email' in raw || 'since' in raw)
|
|
68
|
+
return { accounts: { [GLOBAL_ACCOUNT]: recordOf(raw) }, removing: [] };
|
|
69
|
+
return emptyLedger();
|
|
70
|
+
}
|
|
71
|
+
export function loadLedger(file) {
|
|
72
|
+
let raw;
|
|
73
|
+
try {
|
|
74
|
+
raw = JSON.parse(readFileSync(file, 'utf8'));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return emptyLedger();
|
|
78
|
+
}
|
|
79
|
+
refuseNewer(file, versionOf(raw));
|
|
80
|
+
return parseLedger(raw);
|
|
81
|
+
}
|
|
82
|
+
export function saveLedger(file, ledger) {
|
|
83
|
+
writeAtomic(file, JSON.stringify({ version: SCHEMA_VERSION, ...ledger }, null, 2));
|
|
84
|
+
}
|
|
85
|
+
function recordOf(raw) {
|
|
86
|
+
const text = (value) => (typeof value === 'string' ? value : null);
|
|
87
|
+
const record = { email: text(raw.email), since: text(raw.since), previous: text(raw.previous) };
|
|
88
|
+
return { ...record, ...(typeof raw.name === 'string' && { name: raw.name }) };
|
|
89
|
+
}
|
|
90
|
+
function isRecord(value) {
|
|
91
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
92
|
+
}
|