@bjoernrochel/sevro 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +34 -4
- package/README.md +7 -6
- package/dist/examples/basic/graded.json +13 -0
- package/dist/examples/basic/host.js +9 -0
- package/dist/examples/basic/host.js.map +1 -0
- package/dist/examples/basic/prompt-only.json +7 -0
- package/dist/examples/recipes/candidate.js +23 -0
- package/dist/examples/recipes/candidate.js.map +1 -0
- package/dist/examples/recipes/cases/coding-change.json +21 -0
- package/dist/examples/recipes/cases/host-tools.json +21 -0
- package/dist/examples/recipes/cases/mise.json +21 -0
- package/dist/examples/recipes/cases/node.json +21 -0
- package/dist/examples/recipes/cases/python-uv.json +21 -0
- package/dist/examples/recipes/cases/semantic.json +17 -0
- package/dist/examples/recipes/coding-change/check.js +16 -0
- package/dist/examples/recipes/coding-change/check.js.map +1 -0
- package/dist/examples/recipes/executable.js +20 -0
- package/dist/examples/recipes/executable.js.map +1 -0
- package/dist/examples/recipes/extension-artifacts.js +56 -0
- package/dist/examples/recipes/extension-artifacts.js.map +1 -0
- package/dist/examples/recipes/extension-methods.js +167 -0
- package/dist/examples/recipes/extension-methods.js.map +1 -0
- package/dist/examples/recipes/extension.js +28 -0
- package/dist/examples/recipes/extension.js.map +1 -0
- package/dist/examples/recipes/modules.js +9 -0
- package/dist/examples/recipes/modules.js.map +1 -0
- package/dist/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
- package/dist/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
- package/dist/examples/recipes/repository-skill/README.md +3 -0
- package/dist/examples/recipes/run-inputs.js +144 -0
- package/dist/examples/recipes/run-inputs.js.map +1 -0
- package/dist/examples/recipes/run-options.js +103 -0
- package/dist/examples/recipes/run-options.js.map +1 -0
- package/dist/examples/recipes/run.js +62 -0
- package/dist/examples/recipes/run.js.map +1 -0
- package/dist/examples/recipes/runtime/empty.json +1 -0
- package/dist/examples/recipes/runtime/goals.json +1 -0
- package/dist/examples/recipes/runtime/mise.json +6 -0
- package/dist/examples/recipes/runtime/path.json +1 -0
- package/dist/examples/recipes/runtime/plugins.json +4 -0
- package/dist/examples/recipes/runtime/python-uv.json +10 -0
- package/dist/examples/recipes/semantic-judge.js +27 -0
- package/dist/examples/recipes/semantic-judge.js.map +1 -0
- package/dist/examples/templates/coding/adapter.js +14 -0
- package/dist/examples/templates/coding/adapter.js.map +1 -0
- package/dist/examples/templates/coding/case.json +15 -0
- package/dist/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
- package/dist/examples/templates/repository-skill/extension-data.js +61 -0
- package/dist/examples/templates/repository-skill/extension-data.js.map +1 -0
- package/dist/examples/templates/repository-skill/extension.js +29 -0
- package/dist/examples/templates/repository-skill/extension.js.map +1 -0
- package/dist/examples/templates/response/adapter.js +13 -0
- package/dist/examples/templates/response/adapter.js.map +1 -0
- package/dist/examples/templates/response/case.json +13 -0
- package/dist/src/advisory.js +86 -0
- package/dist/src/advisory.js.map +1 -0
- package/dist/src/candidate-artifacts.js +139 -0
- package/dist/src/candidate-artifacts.js.map +1 -0
- package/dist/src/candidate-codex-state.js +110 -0
- package/dist/src/candidate-codex-state.js.map +1 -0
- package/dist/src/case-validation.js +92 -0
- package/dist/src/case-validation.js.map +1 -0
- package/dist/src/cli/cancellation.js +21 -0
- package/dist/src/cli/cancellation.js.map +1 -0
- package/dist/src/cli/command.js +60 -0
- package/dist/src/cli/command.js.map +1 -0
- package/dist/src/cli/completion.js +14 -0
- package/dist/src/cli/completion.js.map +1 -0
- package/dist/src/cli/demo.js +45 -0
- package/dist/src/cli/demo.js.map +1 -0
- package/dist/src/cli/evaluation.js +40 -0
- package/dist/src/cli/evaluation.js.map +1 -0
- package/dist/src/cli/extension.js +54 -0
- package/dist/src/cli/extension.js.map +1 -0
- package/dist/src/cli/help.js +190 -0
- package/dist/src/cli/help.js.map +1 -0
- package/dist/src/cli/hosts.js +33 -0
- package/dist/src/cli/hosts.js.map +1 -0
- package/dist/src/cli/init-settings.js +29 -0
- package/dist/src/cli/init-settings.js.map +1 -0
- package/dist/src/cli/init.js +82 -0
- package/dist/src/cli/init.js.map +1 -0
- package/dist/src/cli/invocation.js +431 -0
- package/dist/src/cli/invocation.js.map +1 -0
- package/dist/src/cli/loading.js +110 -0
- package/dist/src/cli/loading.js.map +1 -0
- package/dist/src/cli/preparation.js +88 -0
- package/dist/src/cli/preparation.js.map +1 -0
- package/dist/src/cli/presentation.js +61 -0
- package/dist/src/cli/presentation.js.map +1 -0
- package/dist/src/cli/progress.js +5 -0
- package/dist/src/cli/progress.js.map +1 -0
- package/dist/src/cli/run-manifest.js +108 -0
- package/dist/src/cli/run-manifest.js.map +1 -0
- package/dist/src/cli/run-options.js +367 -0
- package/dist/src/cli/run-options.js.map +1 -0
- package/dist/src/cli/run-overrides.js +51 -0
- package/dist/src/cli/run-overrides.js.map +1 -0
- package/dist/src/cli/settings.js +127 -0
- package/dist/src/cli/settings.js.map +1 -0
- package/dist/src/cli/summary.js +28 -0
- package/dist/src/cli/summary.js.map +1 -0
- package/dist/src/cli.js +20 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/contract-types.js +2 -0
- package/dist/src/contract-types.js.map +1 -0
- package/dist/src/engine.js +4 -0
- package/dist/src/engine.js.map +1 -0
- package/dist/src/evaluation/advisory.js +85 -0
- package/dist/src/evaluation/advisory.js.map +1 -0
- package/dist/src/evaluation/check-plan.js +292 -0
- package/dist/src/evaluation/check-plan.js.map +1 -0
- package/dist/src/evaluation/collection.js +61 -0
- package/dist/src/evaluation/collection.js.map +1 -0
- package/dist/src/evaluation/context.js +2 -0
- package/dist/src/evaluation/context.js.map +1 -0
- package/dist/src/evaluation/grading-host.js +28 -0
- package/dist/src/evaluation/grading-host.js.map +1 -0
- package/dist/src/evaluation/grading.js +154 -0
- package/dist/src/evaluation/grading.js.map +1 -0
- package/dist/src/evaluation/invocation-plan.js +312 -0
- package/dist/src/evaluation/invocation-plan.js.map +1 -0
- package/dist/src/evaluation/plan.js +376 -0
- package/dist/src/evaluation/plan.js.map +1 -0
- package/dist/src/evaluation/progress.js +14 -0
- package/dist/src/evaluation/progress.js.map +1 -0
- package/dist/src/evaluation/resources.js +159 -0
- package/dist/src/evaluation/resources.js.map +1 -0
- package/dist/src/evaluation/run.js +148 -0
- package/dist/src/evaluation/run.js.map +1 -0
- package/dist/src/evaluation/scheduler.js +27 -0
- package/dist/src/evaluation/scheduler.js.map +1 -0
- package/dist/src/evaluation/semantic.js +183 -0
- package/dist/src/evaluation/semantic.js.map +1 -0
- package/dist/src/evaluation/trial.js +593 -0
- package/dist/src/evaluation/trial.js.map +1 -0
- package/dist/src/evaluation/types.js +20 -0
- package/dist/src/evaluation/types.js.map +1 -0
- package/dist/src/evidence/host-results.js +67 -0
- package/dist/src/evidence/host-results.js.map +1 -0
- package/dist/src/evidence/retained-artifacts.js +17 -0
- package/dist/src/evidence/retained-artifacts.js.map +1 -0
- package/dist/src/evidence/trial-writer.js +131 -0
- package/dist/src/evidence/trial-writer.js.map +1 -0
- package/dist/src/evidence/usage.js +14 -0
- package/dist/src/evidence/usage.js.map +1 -0
- package/dist/src/executable.js +20 -0
- package/dist/src/executable.js.map +1 -0
- package/dist/src/extension-client.js +212 -0
- package/dist/src/extension-client.js.map +1 -0
- package/dist/src/extension-session.js +236 -0
- package/dist/src/extension-session.js.map +1 -0
- package/dist/src/fixtures/advisory.js +206 -0
- package/dist/src/fixtures/advisory.js.map +1 -0
- package/dist/src/fixtures/bin.js +41 -0
- package/dist/src/fixtures/bin.js.map +1 -0
- package/dist/src/fixtures/cleanup.js +43 -0
- package/dist/src/fixtures/cleanup.js.map +1 -0
- package/dist/src/fixtures/create.js +94 -0
- package/dist/src/fixtures/create.js.map +1 -0
- package/dist/src/fixtures/generated.js +181 -0
- package/dist/src/fixtures/generated.js.map +1 -0
- package/dist/src/fixtures/git-hooks.js +16 -0
- package/dist/src/fixtures/git-hooks.js.map +1 -0
- package/dist/src/fixtures/preparation.js +201 -0
- package/dist/src/fixtures/preparation.js.map +1 -0
- package/dist/src/fixtures/repository.js +255 -0
- package/dist/src/fixtures/repository.js.map +1 -0
- package/dist/src/fixtures/setup.js +175 -0
- package/dist/src/fixtures/setup.js.map +1 -0
- package/dist/src/fixtures/tools.js +57 -0
- package/dist/src/fixtures/tools.js.map +1 -0
- package/dist/src/fixtures/workspace-state.js +155 -0
- package/dist/src/fixtures/workspace-state.js.map +1 -0
- package/dist/src/generated/cli-result.cjs +2 -0
- package/dist/src/generated/extension.cjs +2 -0
- package/dist/src/generated/report.cjs +2 -0
- package/dist/src/generated/run-evidence.cjs +2 -0
- package/dist/src/generated/runtime.cjs +2 -0
- package/dist/src/git.js +72 -0
- package/dist/src/git.js.map +1 -0
- package/dist/src/graders/artifact.js +68 -0
- package/dist/src/graders/artifact.js.map +1 -0
- package/dist/src/graders/git-head.js +112 -0
- package/dist/src/graders/git-head.js.map +1 -0
- package/dist/src/graders/output.js +345 -0
- package/dist/src/graders/output.js.map +1 -0
- package/dist/src/graders/semantic.js +132 -0
- package/dist/src/graders/semantic.js.map +1 -0
- package/dist/src/graders/shell.js +321 -0
- package/dist/src/graders/shell.js.map +1 -0
- package/dist/src/host-diagnostic-redaction.js +52 -0
- package/dist/src/host-diagnostic-redaction.js.map +1 -0
- package/dist/src/host-diagnostics.js +248 -0
- package/dist/src/host-diagnostics.js.map +1 -0
- package/dist/src/hosts/claude-continuation.js +89 -0
- package/dist/src/hosts/claude-continuation.js.map +1 -0
- package/dist/src/hosts/claude-credential.js +121 -0
- package/dist/src/hosts/claude-credential.js.map +1 -0
- package/dist/src/hosts/claude-events.js +136 -0
- package/dist/src/hosts/claude-events.js.map +1 -0
- package/dist/src/hosts/claude-keychain.js +100 -0
- package/dist/src/hosts/claude-keychain.js.map +1 -0
- package/dist/src/hosts/claude-login-recovery.js +67 -0
- package/dist/src/hosts/claude-login-recovery.js.map +1 -0
- package/dist/src/hosts/claude-login-refresh.js +146 -0
- package/dist/src/hosts/claude-login-refresh.js.map +1 -0
- package/dist/src/hosts/claude-login-store.js +124 -0
- package/dist/src/hosts/claude-login-store.js.map +1 -0
- package/dist/src/hosts/claude-login.js +74 -0
- package/dist/src/hosts/claude-login.js.map +1 -0
- package/dist/src/hosts/claude-native-goal.js +138 -0
- package/dist/src/hosts/claude-native-goal.js.map +1 -0
- package/dist/src/hosts/claude-process.js +10 -0
- package/dist/src/hosts/claude-process.js.map +1 -0
- package/dist/src/hosts/claude-refresh-lock.js +76 -0
- package/dist/src/hosts/claude-refresh-lock.js.map +1 -0
- package/dist/src/hosts/claude-repository-invocation.js +229 -0
- package/dist/src/hosts/claude-repository-invocation.js.map +1 -0
- package/dist/src/hosts/claude-settings.js +156 -0
- package/dist/src/hosts/claude-settings.js.map +1 -0
- package/dist/src/hosts/claude-skill-activations.js +131 -0
- package/dist/src/hosts/claude-skill-activations.js.map +1 -0
- package/dist/src/hosts/claude-state.js +237 -0
- package/dist/src/hosts/claude-state.js.map +1 -0
- package/dist/src/hosts/claude-tool-calls.js +150 -0
- package/dist/src/hosts/claude-tool-calls.js.map +1 -0
- package/dist/src/hosts/claude.js +373 -0
- package/dist/src/hosts/claude.js.map +1 -0
- package/dist/src/hosts/codex-app-server.js +764 -0
- package/dist/src/hosts/codex-app-server.js.map +1 -0
- package/dist/src/hosts/codex-configuration.js +91 -0
- package/dist/src/hosts/codex-configuration.js.map +1 -0
- package/dist/src/hosts/codex-events.js +104 -0
- package/dist/src/hosts/codex-events.js.map +1 -0
- package/dist/src/hosts/codex-exec-controls.js +434 -0
- package/dist/src/hosts/codex-exec-controls.js.map +1 -0
- package/dist/src/hosts/codex-invocation.js +54 -0
- package/dist/src/hosts/codex-invocation.js.map +1 -0
- package/dist/src/hosts/codex-marketplace.js +185 -0
- package/dist/src/hosts/codex-marketplace.js.map +1 -0
- package/dist/src/hosts/codex-native-calls.js +516 -0
- package/dist/src/hosts/codex-native-calls.js.map +1 -0
- package/dist/src/hosts/codex-process.js +10 -0
- package/dist/src/hosts/codex-process.js.map +1 -0
- package/dist/src/hosts/codex-profile.js +206 -0
- package/dist/src/hosts/codex-profile.js.map +1 -0
- package/dist/src/hosts/codex-session-files.js +89 -0
- package/dist/src/hosts/codex-session-files.js.map +1 -0
- package/dist/src/hosts/codex-state.js +310 -0
- package/dist/src/hosts/codex-state.js.map +1 -0
- package/dist/src/hosts/codex.js +527 -0
- package/dist/src/hosts/codex.js.map +1 -0
- package/dist/src/hosts/isolation-common.js +43 -0
- package/dist/src/hosts/isolation-common.js.map +1 -0
- package/dist/src/hosts/isolation-roots.js +64 -0
- package/dist/src/hosts/isolation-roots.js.map +1 -0
- package/dist/src/hosts/isolation.js +12 -0
- package/dist/src/hosts/isolation.js.map +1 -0
- package/dist/src/hosts/linux-sandbox.js +127 -0
- package/dist/src/hosts/linux-sandbox.js.map +1 -0
- package/dist/src/hosts/mac-sandbox.js +137 -0
- package/dist/src/hosts/mac-sandbox.js.map +1 -0
- package/dist/src/hosts/native-controls.js +151 -0
- package/dist/src/hosts/native-controls.js.map +1 -0
- package/dist/src/hosts/native-request.js +33 -0
- package/dist/src/hosts/native-request.js.map +1 -0
- package/dist/src/hosts/process-lifecycle.js +167 -0
- package/dist/src/hosts/process-lifecycle.js.map +1 -0
- package/dist/src/hosts/protected-worktrees.js +81 -0
- package/dist/src/hosts/protected-worktrees.js.map +1 -0
- package/dist/src/hosts/skill-hook-catalog.js +100 -0
- package/dist/src/hosts/skill-hook-catalog.js.map +1 -0
- package/dist/src/hosts/skill-hook-handler.js +228 -0
- package/dist/src/hosts/skill-hook-handler.js.map +1 -0
- package/dist/src/hosts/skill-hooks.js +423 -0
- package/dist/src/hosts/skill-hooks.js.map +1 -0
- package/dist/src/hosts/workspace-fingerprint.js +72 -0
- package/dist/src/hosts/workspace-fingerprint.js.map +1 -0
- package/dist/src/identity.js +167 -0
- package/dist/src/identity.js.map +1 -0
- package/dist/src/instrumentation.js +83 -0
- package/dist/src/instrumentation.js.map +1 -0
- package/dist/src/native-transcript-state.js +85 -0
- package/dist/src/native-transcript-state.js.map +1 -0
- package/dist/src/native-transcript-view.js +65 -0
- package/dist/src/native-transcript-view.js.map +1 -0
- package/dist/src/native-transcripts.js +208 -0
- package/dist/src/native-transcripts.js.map +1 -0
- package/dist/src/paths.js +13 -0
- package/dist/src/paths.js.map +1 -0
- package/dist/src/process-lifecycle.js +101 -0
- package/dist/src/process-lifecycle.js.map +1 -0
- package/dist/src/provenance.js +263 -0
- package/dist/src/provenance.js.map +1 -0
- package/dist/src/prune.js +228 -0
- package/dist/src/prune.js.map +1 -0
- package/dist/src/report.js +276 -0
- package/dist/src/report.js.map +1 -0
- package/dist/src/resolved-case.js +29 -0
- package/dist/src/resolved-case.js.map +1 -0
- package/dist/src/results-lock.js +40 -0
- package/dist/src/results-lock.js.map +1 -0
- package/dist/src/results.js +127 -0
- package/dist/src/results.js.map +1 -0
- package/dist/src/run-owner.js +283 -0
- package/dist/src/run-owner.js.map +1 -0
- package/dist/src/runner-paths.js +11 -0
- package/dist/src/runner-paths.js.map +1 -0
- package/dist/src/runtime/config.js +323 -0
- package/dist/src/runtime/config.js.map +1 -0
- package/dist/src/runtime/discovery-apple.js +65 -0
- package/dist/src/runtime/discovery-apple.js.map +1 -0
- package/dist/src/runtime/discovery-providers.js +37 -0
- package/dist/src/runtime/discovery-providers.js.map +1 -0
- package/dist/src/runtime/discovery.js +40 -0
- package/dist/src/runtime/discovery.js.map +1 -0
- package/dist/src/runtime/hook-processes.js +81 -0
- package/dist/src/runtime/hook-processes.js.map +1 -0
- package/dist/src/runtime/hooks.js +304 -0
- package/dist/src/runtime/hooks.js.map +1 -0
- package/dist/src/runtime/paths.js +244 -0
- package/dist/src/runtime/paths.js.map +1 -0
- package/dist/src/runtime/query.js +76 -0
- package/dist/src/runtime/query.js.map +1 -0
- package/dist/src/runtime/seeds.js +98 -0
- package/dist/src/runtime/seeds.js.map +1 -0
- package/dist/src/runtime/shared-cache.js +81 -0
- package/dist/src/runtime/shared-cache.js.map +1 -0
- package/dist/src/runtime/state.js +106 -0
- package/dist/src/runtime/state.js.map +1 -0
- package/dist/src/runtime-process.js +148 -0
- package/dist/src/runtime-process.js.map +1 -0
- package/dist/src/schema-types.js +2 -0
- package/dist/src/schema-types.js.map +1 -0
- package/dist/src/schema.js +16 -0
- package/dist/src/schema.js.map +1 -0
- package/dist/src/shell.js +5 -0
- package/dist/src/shell.js.map +1 -0
- package/dist/src/sqlite-lock.js +18 -0
- package/dist/src/sqlite-lock.js.map +1 -0
- package/dist/src/storage.js +21 -0
- package/dist/src/storage.js.map +1 -0
- package/dist/src/transcript-retention.js +109 -0
- package/dist/src/transcript-retention.js.map +1 -0
- package/dist/src/value-guards.js +26 -0
- package/dist/src/value-guards.js.map +1 -0
- package/docs/architecture.md +5 -2
- package/docs/creating-cases.md +2 -3
- package/docs/development-cli.md +12 -7
- package/docs/documentation-quality.md +4 -3
- package/docs/evaluation-workflow.md +8 -8
- package/docs/getting-started.md +7 -6
- package/docs/guide-evaluation.md +1 -1
- package/docs/identity-v1.md +4 -1
- package/docs/installing.md +31 -13
- package/docs/licensing.md +2 -2
- package/docs/native-hosts.md +1 -1
- package/docs/recipes.md +41 -40
- package/docs/releases.md +20 -4
- package/docs/run-settings-v1.md +3 -3
- package/docs/specs/repository-guide.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/docs/typescript-quality.md +18 -1
- package/examples/recipes/executable.ts +19 -0
- package/examples/recipes/extension-artifacts.ts +1 -1
- package/examples/recipes/extension.ts +2 -1
- package/examples/recipes/modules.ts +9 -0
- package/examples/recipes/run-inputs.ts +14 -19
- package/examples/recipes/run-options.ts +9 -8
- package/examples/recipes/run.ts +21 -7
- package/examples/templates/repository-skill/extension-data.ts +1 -1
- package/examples/templates/repository-skill/extension.ts +2 -1
- package/package.json +14 -6
- package/src/cli/demo.ts +3 -2
- package/src/cli/help.ts +4 -3
- package/src/cli/init-settings.ts +5 -4
- package/src/cli/init.ts +9 -8
- package/src/cli/invocation.ts +2 -1
- package/src/cli/preparation.ts +2 -2
- package/src/cli.ts +1 -1
- package/src/executable.ts +19 -0
- package/src/extension-client.ts +6 -12
- package/src/fixtures/setup.ts +3 -2
- package/src/git.ts +2 -1
- package/src/graders/git-head.ts +3 -2
- package/src/graders/shell.ts +4 -3
- package/src/hosts/claude-keychain.ts +3 -2
- package/src/hosts/claude-login-refresh.ts +3 -2
- package/src/hosts/claude-refresh-lock.ts +2 -1
- package/src/hosts/isolation-roots.ts +3 -2
- package/src/hosts/process-lifecycle.ts +4 -6
- package/src/hosts/protected-worktrees.ts +2 -1
- package/src/hosts/skill-hook-handler.ts +1 -1
- package/src/hosts/skill-hooks.ts +10 -2
- package/src/provenance.ts +7 -6
- package/src/results-lock.ts +4 -4
- package/src/run-owner.ts +15 -9
- package/src/runner-paths.ts +21 -0
- package/src/runtime/hook-processes.ts +2 -1
- package/src/runtime/hooks.ts +9 -7
- package/src/runtime-process.ts +194 -0
- package/src/sqlite-lock.ts +25 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { isRecord } from "../value-guards.js";
|
|
2
|
+
const MAX_EVENT_STREAM_BYTES = 8 * 1024 * 1024;
|
|
3
|
+
export class ClaudeEventError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "ClaudeEventError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** Decode JSON once; consumers retain their own event validity rules. */
|
|
10
|
+
export function parseClaudeEvents(stream) {
|
|
11
|
+
if (typeof stream !== "string")
|
|
12
|
+
return stream;
|
|
13
|
+
if (Buffer.byteLength(stream, "utf8") > MAX_EVENT_STREAM_BYTES)
|
|
14
|
+
throw new ClaudeEventError("Claude event stream exceeds 8 MiB");
|
|
15
|
+
return {
|
|
16
|
+
lines: stream
|
|
17
|
+
.split("\n")
|
|
18
|
+
.filter((line) => line.trim())
|
|
19
|
+
.map(parseLine),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function parseLine(line) {
|
|
23
|
+
try {
|
|
24
|
+
return { parsed: true, value: JSON.parse(line) };
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return { parsed: false, error };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function nonnegative(value) {
|
|
31
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
32
|
+
}
|
|
33
|
+
function validAssistant(value) {
|
|
34
|
+
if (!isRecord(value.message) || !Array.isArray(value.message.content))
|
|
35
|
+
return false;
|
|
36
|
+
return value.message.content.every(validAssistantBlock);
|
|
37
|
+
}
|
|
38
|
+
function validAssistantBlock(block) {
|
|
39
|
+
if (!isRecord(block))
|
|
40
|
+
return false;
|
|
41
|
+
if (block.type !== "tool_use" || block.name !== "Skill")
|
|
42
|
+
return true;
|
|
43
|
+
return validSkillInput(block.input);
|
|
44
|
+
}
|
|
45
|
+
function validSkillInput(input) {
|
|
46
|
+
if (!isRecord(input) || typeof input.skill !== "string")
|
|
47
|
+
return false;
|
|
48
|
+
return !!input.skill.split(":").at(-1)?.trim();
|
|
49
|
+
}
|
|
50
|
+
function validResult(value) {
|
|
51
|
+
return (typeof value.subtype === "string" &&
|
|
52
|
+
!!value.subtype.trim() &&
|
|
53
|
+
typeof value.is_error === "boolean" &&
|
|
54
|
+
typeof value.result === "string");
|
|
55
|
+
}
|
|
56
|
+
function validEvent(value) {
|
|
57
|
+
if (!isRecord(value))
|
|
58
|
+
return false;
|
|
59
|
+
if (value.type === "result")
|
|
60
|
+
return validResult(value);
|
|
61
|
+
if (value.type === "assistant")
|
|
62
|
+
return validAssistant(value);
|
|
63
|
+
return typeof value.type === "string";
|
|
64
|
+
}
|
|
65
|
+
function usage(value) {
|
|
66
|
+
if (!isRecord(value))
|
|
67
|
+
return null;
|
|
68
|
+
const inputTokens = inputUsage(value);
|
|
69
|
+
if (inputTokens === null || !nonnegative(value.output_tokens))
|
|
70
|
+
return null;
|
|
71
|
+
return { inputTokens, outputTokens: value.output_tokens };
|
|
72
|
+
}
|
|
73
|
+
function inputUsage(value) {
|
|
74
|
+
const counts = [
|
|
75
|
+
value.input_tokens,
|
|
76
|
+
value.cache_creation_input_tokens ?? 0,
|
|
77
|
+
value.cache_read_input_tokens ?? 0,
|
|
78
|
+
];
|
|
79
|
+
if (!counts.every(nonnegative))
|
|
80
|
+
return null;
|
|
81
|
+
return counts.reduce((total, count) => total + count);
|
|
82
|
+
}
|
|
83
|
+
/** Summarize decoded events without exposing their private bodies. */
|
|
84
|
+
export function summarizeClaudeEvents(stream, exitCode) {
|
|
85
|
+
const { results, malformed } = parseResultEvents(parseClaudeEvents(stream));
|
|
86
|
+
return resultSummary(results, malformed, exitCode);
|
|
87
|
+
}
|
|
88
|
+
function parseResultEvents(stream) {
|
|
89
|
+
const results = [];
|
|
90
|
+
let malformed = false;
|
|
91
|
+
for (const line of stream.lines) {
|
|
92
|
+
if (!line.parsed || !validEvent(line.value)) {
|
|
93
|
+
malformed = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (line.value.type === "result")
|
|
97
|
+
results.push(line.value);
|
|
98
|
+
}
|
|
99
|
+
return { results, malformed };
|
|
100
|
+
}
|
|
101
|
+
function summarizeUsage(results, malformed) {
|
|
102
|
+
const usages = results.map((result) => usage(result.usage));
|
|
103
|
+
const validUsages = usages.filter((value) => value !== null);
|
|
104
|
+
const usageComplete = !malformed && results.length > 0 && validUsages.length === usages.length;
|
|
105
|
+
return {
|
|
106
|
+
inputTokens: usageComplete
|
|
107
|
+
? validUsages.reduce((total, value) => total + value.inputTokens, 0)
|
|
108
|
+
: null,
|
|
109
|
+
outputTokens: usageComplete
|
|
110
|
+
? validUsages.reduce((total, value) => total + value.outputTokens, 0)
|
|
111
|
+
: null,
|
|
112
|
+
usageComplete,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function measuredCost(results, malformed) {
|
|
116
|
+
const costs = results.map((result) => result.total_cost_usd);
|
|
117
|
+
if (malformed || results.length === 0 || !costs.every(nonnegative))
|
|
118
|
+
return null;
|
|
119
|
+
return costs.reduce((total, value) => total + value, 0);
|
|
120
|
+
}
|
|
121
|
+
function resultSummary(results, malformed, exitCode) {
|
|
122
|
+
const final = results.at(-1);
|
|
123
|
+
return {
|
|
124
|
+
complete: completedResults(results, malformed, exitCode),
|
|
125
|
+
finalMessage: typeof final?.result === "string" ? final.result : null,
|
|
126
|
+
costUsd: measuredCost(results, malformed),
|
|
127
|
+
...summarizeUsage(results, malformed),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function completedResults(results, malformed, exitCode) {
|
|
131
|
+
return (!malformed &&
|
|
132
|
+
exitCode === 0 &&
|
|
133
|
+
results.length > 0 &&
|
|
134
|
+
results.every((result) => result.subtype === "success" && result.is_error === false));
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=claude-events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-events.js","sourceRoot":"","sources":["claude-events.ts"],"names":[],"mappings":"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","sourcesContent":["import { isRecord } from \"../value-guards\";\nconst MAX_EVENT_STREAM_BYTES = 8 * 1024 * 1024;\n\nexport class ClaudeEventError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ClaudeEventError\";\n }\n}\n\ntype Entry = Record<string, unknown>;\nexport type ClaudeEventLine =\n { parsed: true; value: unknown } | { parsed: false; error: unknown };\nexport interface ClaudeEvents {\n lines: readonly ClaudeEventLine[];\n}\n\n/** Decode JSON once; consumers retain their own event validity rules. */\nexport function parseClaudeEvents(stream: string | ClaudeEvents): ClaudeEvents {\n if (typeof stream !== \"string\") return stream;\n if (Buffer.byteLength(stream, \"utf8\") > MAX_EVENT_STREAM_BYTES)\n throw new ClaudeEventError(\"Claude event stream exceeds 8 MiB\");\n return {\n lines: stream\n .split(\"\\n\")\n .filter((line) => line.trim())\n .map(parseLine),\n };\n}\n\nfunction parseLine(line: string): ClaudeEventLine {\n try {\n return { parsed: true, value: JSON.parse(line) as unknown };\n } catch (error) {\n return { parsed: false, error };\n }\n}\n\nfunction nonnegative(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value) && value >= 0;\n}\n\nfunction validAssistant(value: Entry): boolean {\n if (!isRecord(value.message) || !Array.isArray(value.message.content))\n return false;\n return value.message.content.every(validAssistantBlock);\n}\n\nfunction validAssistantBlock(block: unknown): boolean {\n if (!isRecord(block)) return false;\n if (block.type !== \"tool_use\" || block.name !== \"Skill\") return true;\n return validSkillInput(block.input);\n}\n\nfunction validSkillInput(input: unknown): boolean {\n if (!isRecord(input) || typeof input.skill !== \"string\") return false;\n return !!input.skill.split(\":\").at(-1)?.trim();\n}\n\nfunction validResult(value: Entry): boolean {\n return (\n typeof value.subtype === \"string\" &&\n !!value.subtype.trim() &&\n typeof value.is_error === \"boolean\" &&\n typeof value.result === \"string\"\n );\n}\n\nfunction validEvent(value: unknown): value is Entry {\n if (!isRecord(value)) return false;\n if (value.type === \"result\") return validResult(value);\n if (value.type === \"assistant\") return validAssistant(value);\n return typeof value.type === \"string\";\n}\n\ninterface Usage {\n inputTokens: number;\n outputTokens: number;\n}\n\nfunction usage(value: unknown): Usage | null {\n if (!isRecord(value)) return null;\n const inputTokens = inputUsage(value);\n if (inputTokens === null || !nonnegative(value.output_tokens)) return null;\n return { inputTokens, outputTokens: value.output_tokens };\n}\n\nfunction inputUsage(value: Entry): number | null {\n const counts = [\n value.input_tokens,\n value.cache_creation_input_tokens ?? 0,\n value.cache_read_input_tokens ?? 0,\n ];\n if (!counts.every(nonnegative)) return null;\n return counts.reduce((total, count) => total + count);\n}\n\nexport interface ClaudeEventSummary {\n complete: boolean;\n finalMessage: string | null;\n inputTokens: number | null;\n outputTokens: number | null;\n costUsd: number | null;\n usageComplete: boolean;\n}\n\n/** Summarize decoded events without exposing their private bodies. */\nexport function summarizeClaudeEvents(\n stream: string | ClaudeEvents,\n exitCode: number,\n): ClaudeEventSummary {\n const { results, malformed } = parseResultEvents(parseClaudeEvents(stream));\n return resultSummary(results, malformed, exitCode);\n}\n\nfunction parseResultEvents(stream: ClaudeEvents) {\n const results: Entry[] = [];\n let malformed = false;\n for (const line of stream.lines) {\n if (!line.parsed || !validEvent(line.value)) {\n malformed = true;\n continue;\n }\n if (line.value.type === \"result\") results.push(line.value);\n }\n return { results, malformed };\n}\n\nfunction summarizeUsage(results: Entry[], malformed: boolean) {\n const usages = results.map((result) => usage(result.usage));\n const validUsages = usages.filter((value): value is Usage => value !== null);\n const usageComplete =\n !malformed && results.length > 0 && validUsages.length === usages.length;\n return {\n inputTokens: usageComplete\n ? validUsages.reduce((total, value) => total + value.inputTokens, 0)\n : null,\n outputTokens: usageComplete\n ? validUsages.reduce((total, value) => total + value.outputTokens, 0)\n : null,\n usageComplete,\n };\n}\n\nfunction measuredCost(results: Entry[], malformed: boolean): number | null {\n const costs = results.map((result) => result.total_cost_usd);\n if (malformed || results.length === 0 || !costs.every(nonnegative))\n return null;\n return costs.reduce((total, value) => total + value, 0);\n}\n\nfunction resultSummary(\n results: Entry[],\n malformed: boolean,\n exitCode: number,\n): ClaudeEventSummary {\n const final = results.at(-1);\n return {\n complete: completedResults(results, malformed, exitCode),\n finalMessage: typeof final?.result === \"string\" ? final.result : null,\n costUsd: measuredCost(results, malformed),\n ...summarizeUsage(results, malformed),\n };\n}\n\nfunction completedResults(\n results: Entry[],\n malformed: boolean,\n exitCode: number,\n): boolean {\n return (\n !malformed &&\n exitCode === 0 &&\n results.length > 0 &&\n results.every(\n (result) => result.subtype === \"success\" && result.is_error === false,\n )\n );\n}\n"]}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { spawn } from "../runtime-process.js";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { MAX_CREDENTIAL_BYTES, requireBoundedCredential, } from "./claude-login-store.js";
|
|
4
|
+
const SECURITY = "/usr/bin/security";
|
|
5
|
+
/** `security` reports errSecItemNotFound with this exit status. */
|
|
6
|
+
const ITEM_NOT_FOUND = 44;
|
|
7
|
+
/** Claude Code's bound for one `security -i` command line. */
|
|
8
|
+
const INTERACTIVE_LINE_LIMIT = 4032;
|
|
9
|
+
const KEYCHAIN_TIMEOUT_MS = 10_000;
|
|
10
|
+
function requireSecurity() {
|
|
11
|
+
if (process.platform !== "darwin" || !existsSync(SECURITY))
|
|
12
|
+
throw new Error("Claude keychain credential is unavailable");
|
|
13
|
+
}
|
|
14
|
+
async function boundedCredentialStream(reader) {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
let size = 0;
|
|
17
|
+
for (;;) {
|
|
18
|
+
const { value, done } = await reader.read();
|
|
19
|
+
if (done)
|
|
20
|
+
break;
|
|
21
|
+
size += value.byteLength;
|
|
22
|
+
if (size > MAX_CREDENTIAL_BYTES)
|
|
23
|
+
throw new Error("Claude credential exceeds the size limit");
|
|
24
|
+
chunks.push(value);
|
|
25
|
+
}
|
|
26
|
+
return Buffer.concat(chunks);
|
|
27
|
+
}
|
|
28
|
+
async function findItem(service, account) {
|
|
29
|
+
requireSecurity();
|
|
30
|
+
const proc = spawn([SECURITY, "find-generic-password", "-a", account, "-s", service, "-w"], { stdout: "pipe", stderr: "ignore" });
|
|
31
|
+
const timer = setTimeout(() => {
|
|
32
|
+
proc.kill("SIGKILL");
|
|
33
|
+
}, KEYCHAIN_TIMEOUT_MS);
|
|
34
|
+
const reader = proc.stdout.getReader();
|
|
35
|
+
try {
|
|
36
|
+
const credential = await boundedCredentialStream(reader);
|
|
37
|
+
const code = await proc.exited;
|
|
38
|
+
if (code === ITEM_NOT_FOUND)
|
|
39
|
+
return undefined;
|
|
40
|
+
if (code !== 0)
|
|
41
|
+
throw new Error("Claude keychain credential is unavailable");
|
|
42
|
+
return requireBoundedCredential(credential);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
proc.kill("SIGKILL");
|
|
46
|
+
await proc.exited;
|
|
47
|
+
throw new Error("Claude credential is unreadable or unavailable", {
|
|
48
|
+
cause: error,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
reader.releaseLock();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Mirror Claude Code's update so the secret stays off argv when it fits. */
|
|
57
|
+
function updateCommand(service, account, bytes) {
|
|
58
|
+
const hex = bytes.toString("hex");
|
|
59
|
+
const line = `add-generic-password -U -a "${account}" -s "${service}" -X "${hex}"\n`;
|
|
60
|
+
return line.length <= INTERACTIVE_LINE_LIMIT
|
|
61
|
+
? { command: [SECURITY, "-i"], stdin: Buffer.from(line) }
|
|
62
|
+
: {
|
|
63
|
+
command: [
|
|
64
|
+
SECURITY,
|
|
65
|
+
"add-generic-password",
|
|
66
|
+
"-U",
|
|
67
|
+
"-a",
|
|
68
|
+
account,
|
|
69
|
+
"-s",
|
|
70
|
+
service,
|
|
71
|
+
"-X",
|
|
72
|
+
hex,
|
|
73
|
+
],
|
|
74
|
+
stdin: undefined,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async function updateItem(service, account, bytes) {
|
|
78
|
+
requireSecurity();
|
|
79
|
+
const { command, stdin } = updateCommand(service, account, bytes);
|
|
80
|
+
const proc = spawn(command, {
|
|
81
|
+
stdin: stdin ?? "ignore",
|
|
82
|
+
stdout: "ignore",
|
|
83
|
+
stderr: "ignore",
|
|
84
|
+
});
|
|
85
|
+
const timer = setTimeout(() => {
|
|
86
|
+
proc.kill("SIGKILL");
|
|
87
|
+
}, KEYCHAIN_TIMEOUT_MS);
|
|
88
|
+
try {
|
|
89
|
+
if ((await proc.exited) !== 0)
|
|
90
|
+
throw new Error("Claude keychain login could not be updated");
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export const claudeKeychain = {
|
|
97
|
+
read: findItem,
|
|
98
|
+
write: updateItem,
|
|
99
|
+
};
|
|
100
|
+
//# sourceMappingURL=claude-keychain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-keychain.js","sourceRoot":"","sources":["claude-keychain.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"../runtime-process\";\nimport { existsSync } from \"node:fs\";\nimport {\n MAX_CREDENTIAL_BYTES,\n requireBoundedCredential,\n type ClaudeKeychain,\n} from \"./claude-login-store\";\n\nconst SECURITY = \"/usr/bin/security\";\n/** `security` reports errSecItemNotFound with this exit status. */\nconst ITEM_NOT_FOUND = 44;\n/** Claude Code's bound for one `security -i` command line. */\nconst INTERACTIVE_LINE_LIMIT = 4032;\nconst KEYCHAIN_TIMEOUT_MS = 10_000;\n\nfunction requireSecurity(): void {\n if (process.platform !== \"darwin\" || !existsSync(SECURITY))\n throw new Error(\"Claude keychain credential is unavailable\");\n}\n\nasync function boundedCredentialStream(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n): Promise<Buffer> {\n const chunks: Uint8Array[] = [];\n let size = 0;\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n size += value.byteLength;\n if (size > MAX_CREDENTIAL_BYTES)\n throw new Error(\"Claude credential exceeds the size limit\");\n chunks.push(value);\n }\n return Buffer.concat(chunks);\n}\n\nasync function findItem(\n service: string,\n account: string,\n): Promise<Buffer | undefined> {\n requireSecurity();\n const proc = spawn(\n [SECURITY, \"find-generic-password\", \"-a\", account, \"-s\", service, \"-w\"],\n { stdout: \"pipe\", stderr: \"ignore\" },\n );\n const timer = setTimeout(() => {\n proc.kill(\"SIGKILL\");\n }, KEYCHAIN_TIMEOUT_MS);\n const reader = proc.stdout.getReader();\n try {\n const credential = await boundedCredentialStream(reader);\n const code = await proc.exited;\n if (code === ITEM_NOT_FOUND) return undefined;\n if (code !== 0)\n throw new Error(\"Claude keychain credential is unavailable\");\n return requireBoundedCredential(credential);\n } catch (error) {\n proc.kill(\"SIGKILL\");\n await proc.exited;\n throw new Error(\"Claude credential is unreadable or unavailable\", {\n cause: error,\n });\n } finally {\n clearTimeout(timer);\n reader.releaseLock();\n }\n}\n\n/** Mirror Claude Code's update so the secret stays off argv when it fits. */\nfunction updateCommand(service: string, account: string, bytes: Buffer) {\n const hex = bytes.toString(\"hex\");\n const line = `add-generic-password -U -a \"${account}\" -s \"${service}\" -X \"${hex}\"\\n`;\n return line.length <= INTERACTIVE_LINE_LIMIT\n ? { command: [SECURITY, \"-i\"], stdin: Buffer.from(line) }\n : {\n command: [\n SECURITY,\n \"add-generic-password\",\n \"-U\",\n \"-a\",\n account,\n \"-s\",\n service,\n \"-X\",\n hex,\n ],\n stdin: undefined,\n };\n}\n\nasync function updateItem(\n service: string,\n account: string,\n bytes: Buffer,\n): Promise<void> {\n requireSecurity();\n const { command, stdin } = updateCommand(service, account, bytes);\n const proc = spawn(command, {\n stdin: stdin ?? \"ignore\",\n stdout: \"ignore\",\n stderr: \"ignore\",\n });\n const timer = setTimeout(() => {\n proc.kill(\"SIGKILL\");\n }, KEYCHAIN_TIMEOUT_MS);\n try {\n if ((await proc.exited) !== 0)\n throw new Error(\"Claude keychain login could not be updated\");\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport const claudeKeychain: ClaudeKeychain = {\n read: findItem,\n write: updateItem,\n};\n"]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { isRecord } from "../value-guards.js";
|
|
5
|
+
import { rotatedOAuth, tokenDigest } from "./claude-login.js";
|
|
6
|
+
const FORMAT = "sevro.claude-login-recovery.v1";
|
|
7
|
+
/** Private, per-origin recovery slot for one refreshed login. */
|
|
8
|
+
export class ClaudeLoginRecovery {
|
|
9
|
+
path;
|
|
10
|
+
workRoot;
|
|
11
|
+
constructor(root, origin) {
|
|
12
|
+
const key = createHash("sha256").update(origin).digest("hex");
|
|
13
|
+
this.path = join(root, `${key}.json`);
|
|
14
|
+
this.workRoot = join(root, "work");
|
|
15
|
+
}
|
|
16
|
+
async pending() {
|
|
17
|
+
let text;
|
|
18
|
+
try {
|
|
19
|
+
text = await readFile(this.path, "utf8");
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return pendingRotation(text);
|
|
25
|
+
}
|
|
26
|
+
/** Durably retain the rotation before any canonical write is attempted. */
|
|
27
|
+
async retain(replaced, rotated) {
|
|
28
|
+
const record = {
|
|
29
|
+
format: FORMAT,
|
|
30
|
+
replaces: tokenDigest(replaced.refreshToken),
|
|
31
|
+
oauth: rotatedOAuth(rotated),
|
|
32
|
+
};
|
|
33
|
+
const temporary = `${this.path}.${randomUUID()}.tmp`;
|
|
34
|
+
await writeFile(temporary, JSON.stringify(record), {
|
|
35
|
+
flag: "wx",
|
|
36
|
+
mode: 0o600,
|
|
37
|
+
});
|
|
38
|
+
await rename(temporary, this.path);
|
|
39
|
+
}
|
|
40
|
+
async discard() {
|
|
41
|
+
await rm(this.path, { force: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function pendingRotation(text) {
|
|
45
|
+
let value;
|
|
46
|
+
try {
|
|
47
|
+
value = JSON.parse(text);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return isPendingRecord(value)
|
|
53
|
+
? { replaces: value.replaces, oauth: value.oauth }
|
|
54
|
+
: undefined;
|
|
55
|
+
}
|
|
56
|
+
function isPendingRecord(value) {
|
|
57
|
+
return (isRecord(value) &&
|
|
58
|
+
value.format === FORMAT &&
|
|
59
|
+
typeof value.replaces === "string" &&
|
|
60
|
+
isRecord(value.oauth));
|
|
61
|
+
}
|
|
62
|
+
export async function claudeLoginRecovery(stateRoot, origin) {
|
|
63
|
+
const root = join(stateRoot, "claude-login-recovery");
|
|
64
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
65
|
+
return new ClaudeLoginRecovery(root, origin);
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=claude-login-recovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-login-recovery.js","sourceRoot":"","sources":["claude-login-recovery.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport { mkdir, readFile, rename, rm, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { isRecord } from \"../value-guards\";\nimport { rotatedOAuth, tokenDigest, type ClaudeLogin } from \"./claude-login\";\n\nconst FORMAT = \"sevro.claude-login-recovery.v1\";\n\n/** A rotated pair retained until its canonical store verifiably holds it. */\nexport interface PendingRotation {\n /** Digest of the refresh token the rotation consumed. */\n replaces: string;\n oauth: Record<string, unknown>;\n}\n\n/** Private, per-origin recovery slot for one refreshed login. */\nexport class ClaudeLoginRecovery {\n readonly path: string;\n readonly workRoot: string;\n\n constructor(root: string, origin: string) {\n const key = createHash(\"sha256\").update(origin).digest(\"hex\");\n this.path = join(root, `${key}.json`);\n this.workRoot = join(root, \"work\");\n }\n\n async pending(): Promise<PendingRotation | undefined> {\n let text: string;\n try {\n text = await readFile(this.path, \"utf8\");\n } catch {\n return undefined;\n }\n return pendingRotation(text);\n }\n\n /** Durably retain the rotation before any canonical write is attempted. */\n async retain(replaced: ClaudeLogin, rotated: ClaudeLogin): Promise<void> {\n const record: PendingRotation & { format: string } = {\n format: FORMAT,\n replaces: tokenDigest(replaced.refreshToken),\n oauth: rotatedOAuth(rotated),\n };\n const temporary = `${this.path}.${randomUUID()}.tmp`;\n await writeFile(temporary, JSON.stringify(record), {\n flag: \"wx\",\n mode: 0o600,\n });\n await rename(temporary, this.path);\n }\n\n async discard(): Promise<void> {\n await rm(this.path, { force: true });\n }\n}\n\nfunction pendingRotation(text: string): PendingRotation | undefined {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch {\n return undefined;\n }\n return isPendingRecord(value)\n ? { replaces: value.replaces, oauth: value.oauth }\n : undefined;\n}\n\nfunction isPendingRecord(value: unknown): value is PendingRotation {\n return (\n isRecord(value) &&\n value.format === FORMAT &&\n typeof value.replaces === \"string\" &&\n isRecord(value.oauth)\n );\n}\n\nexport async function claudeLoginRecovery(\n stateRoot: string,\n origin: string,\n): Promise<ClaudeLoginRecovery> {\n const root = join(stateRoot, \"claude-login-recovery\");\n await mkdir(root, { recursive: true, mode: 0o700 });\n return new ClaudeLoginRecovery(root, origin);\n}\n"]}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { spawn } from "../runtime-process.js";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, stat, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { delimiter, join } from "node:path";
|
|
4
|
+
import { parseClaudeLogin } from "./claude-login.js";
|
|
5
|
+
import { MAX_CREDENTIAL_BYTES } from "./claude-login-store.js";
|
|
6
|
+
/** First Claude Code release verified to provide refresh-token login. */
|
|
7
|
+
export const CLAUDE_REFRESH_MINIMUM_VERSION = "2.1.284";
|
|
8
|
+
const MINIMUM_VERSION = [2, 1, 284];
|
|
9
|
+
const VERSION_TIMEOUT_MS = 10_000;
|
|
10
|
+
const HELPER_TIMEOUT_MS = 60_000;
|
|
11
|
+
/** Network settings the operator may need to reach Anthropic's token endpoint. */
|
|
12
|
+
const FORWARDED_VARIABLES = [
|
|
13
|
+
"HTTPS_PROXY",
|
|
14
|
+
"https_proxy",
|
|
15
|
+
"HTTP_PROXY",
|
|
16
|
+
"http_proxy",
|
|
17
|
+
"NO_PROXY",
|
|
18
|
+
"no_proxy",
|
|
19
|
+
"NODE_EXTRA_CA_CERTS",
|
|
20
|
+
"SSL_CERT_FILE",
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Claude resolves `security` through PATH. Refusing it makes the helper keep
|
|
24
|
+
* the rotated login in its private credential file instead of a Keychain item.
|
|
25
|
+
*/
|
|
26
|
+
const SECURITY_STUB = '#!/bin/sh\n[ "$1" = find-generic-password ] && exit 44\nexit 1\n';
|
|
27
|
+
/**
|
|
28
|
+
* Exchange the login's refresh token with Claude Code's documented
|
|
29
|
+
* `CLAUDE_CODE_OAUTH_REFRESH_TOKEN` login in a disposable configuration.
|
|
30
|
+
* The caller owns the lock, durable writeback, and recovery.
|
|
31
|
+
*/
|
|
32
|
+
export async function refreshClaudeLogin(binary, login, workRoot) {
|
|
33
|
+
await requireRefreshCapableClaude(binary);
|
|
34
|
+
await mkdir(workRoot, { recursive: true, mode: 0o700 });
|
|
35
|
+
const helper = await mkdtemp(join(workRoot, "refresh-"));
|
|
36
|
+
try {
|
|
37
|
+
const paths = await prepareHelper(helper);
|
|
38
|
+
const code = await runHelper(binary, helperEnvironment(paths, login));
|
|
39
|
+
return await storedRotation(paths.config, login, code);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
await rm(helper, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function requireRefreshCapableClaude(binary) {
|
|
46
|
+
const version = await claudeVersion(binary);
|
|
47
|
+
if (!version || !atLeastMinimum(version))
|
|
48
|
+
throw new Error(`Refreshing the saved Claude login requires Claude Code ${CLAUDE_REFRESH_MINIMUM_VERSION} or later`);
|
|
49
|
+
}
|
|
50
|
+
async function claudeVersion(binary) {
|
|
51
|
+
const proc = spawn([binary, "--version"], {
|
|
52
|
+
stdin: "ignore",
|
|
53
|
+
stdout: "pipe",
|
|
54
|
+
stderr: "ignore",
|
|
55
|
+
});
|
|
56
|
+
const timer = setTimeout(() => {
|
|
57
|
+
proc.kill("SIGKILL");
|
|
58
|
+
}, VERSION_TIMEOUT_MS);
|
|
59
|
+
try {
|
|
60
|
+
const [output, code] = await Promise.all([
|
|
61
|
+
new Response(proc.stdout).text(),
|
|
62
|
+
proc.exited,
|
|
63
|
+
]);
|
|
64
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(output.trim());
|
|
65
|
+
return code === 0 && match ? match.slice(1).map(Number) : undefined;
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function atLeastMinimum(version) {
|
|
72
|
+
for (const [index, minimum] of MINIMUM_VERSION.entries()) {
|
|
73
|
+
const part = version[index] ?? 0;
|
|
74
|
+
if (part !== minimum)
|
|
75
|
+
return part > minimum;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
async function prepareHelper(helper) {
|
|
80
|
+
const paths = {
|
|
81
|
+
config: join(helper, "config"),
|
|
82
|
+
home: join(helper, "home"),
|
|
83
|
+
bin: join(helper, "bin"),
|
|
84
|
+
};
|
|
85
|
+
await Promise.all(Object.values(paths).map((path) => mkdir(path, { mode: 0o700 })));
|
|
86
|
+
await writeFile(join(paths.bin, "security"), SECURITY_STUB, {
|
|
87
|
+
flag: "wx",
|
|
88
|
+
mode: 0o700,
|
|
89
|
+
});
|
|
90
|
+
return paths;
|
|
91
|
+
}
|
|
92
|
+
function helperEnvironment(paths, login) {
|
|
93
|
+
const forwarded = Object.fromEntries(FORWARDED_VARIABLES.flatMap((name) => {
|
|
94
|
+
const value = process.env[name];
|
|
95
|
+
return value ? [[name, value]] : [];
|
|
96
|
+
}));
|
|
97
|
+
return {
|
|
98
|
+
...forwarded,
|
|
99
|
+
PATH: [paths.bin, process.env.PATH ?? "/usr/bin:/bin"].join(delimiter),
|
|
100
|
+
HOME: paths.home,
|
|
101
|
+
TMPDIR: paths.home,
|
|
102
|
+
LANG: process.env.LANG ?? "C",
|
|
103
|
+
NO_COLOR: "1",
|
|
104
|
+
CLAUDE_CONFIG_DIR: paths.config,
|
|
105
|
+
CLAUDE_CODE_OAUTH_REFRESH_TOKEN: login.refreshToken,
|
|
106
|
+
CLAUDE_CODE_OAUTH_SCOPES: login.scopes.join(" "),
|
|
107
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** Output is discarded: it is never needed and must not reach evidence. */
|
|
111
|
+
async function runHelper(binary, env) {
|
|
112
|
+
const proc = spawn([binary, "auth", "login"], {
|
|
113
|
+
env,
|
|
114
|
+
cwd: env.HOME,
|
|
115
|
+
stdin: "ignore",
|
|
116
|
+
stdout: "ignore",
|
|
117
|
+
stderr: "ignore",
|
|
118
|
+
});
|
|
119
|
+
const timer = setTimeout(() => {
|
|
120
|
+
proc.kill("SIGKILL");
|
|
121
|
+
}, HELPER_TIMEOUT_MS);
|
|
122
|
+
try {
|
|
123
|
+
return await proc.exited;
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Accept only a persisted rotation; exit status alone proves nothing. */
|
|
130
|
+
async function storedRotation(config, login, code) {
|
|
131
|
+
const rotated = await helperLogin(join(config, ".credentials.json"));
|
|
132
|
+
if (!rotated?.refreshToken || rotated.accessToken === login.accessToken)
|
|
133
|
+
throw new Error(`Claude could not refresh the saved login (helper exit ${String(code)}); run claude and /login if it has expired`);
|
|
134
|
+
return rotated;
|
|
135
|
+
}
|
|
136
|
+
async function helperLogin(path) {
|
|
137
|
+
try {
|
|
138
|
+
if ((await stat(path)).size > MAX_CREDENTIAL_BYTES)
|
|
139
|
+
return undefined;
|
|
140
|
+
return parseClaudeLogin(await readFile(path));
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=claude-login-refresh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-login-refresh.js","sourceRoot":"","sources":["claude-login-refresh.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"../runtime-process\";\nimport {\n mkdir,\n mkdtemp,\n readFile,\n rm,\n stat,\n writeFile,\n} from \"node:fs/promises\";\nimport { delimiter, join } from \"node:path\";\nimport { parseClaudeLogin, type ClaudeLogin } from \"./claude-login\";\nimport { MAX_CREDENTIAL_BYTES } from \"./claude-login-store\";\n\n/** First Claude Code release verified to provide refresh-token login. */\nexport const CLAUDE_REFRESH_MINIMUM_VERSION = \"2.1.284\";\nconst MINIMUM_VERSION = [2, 1, 284] as const;\nconst VERSION_TIMEOUT_MS = 10_000;\nconst HELPER_TIMEOUT_MS = 60_000;\n/** Network settings the operator may need to reach Anthropic's token endpoint. */\nconst FORWARDED_VARIABLES = [\n \"HTTPS_PROXY\",\n \"https_proxy\",\n \"HTTP_PROXY\",\n \"http_proxy\",\n \"NO_PROXY\",\n \"no_proxy\",\n \"NODE_EXTRA_CA_CERTS\",\n \"SSL_CERT_FILE\",\n] as const;\n/**\n * Claude resolves `security` through PATH. Refusing it makes the helper keep\n * the rotated login in its private credential file instead of a Keychain item.\n */\nconst SECURITY_STUB =\n '#!/bin/sh\\n[ \"$1\" = find-generic-password ] && exit 44\\nexit 1\\n';\n\n/**\n * Exchange the login's refresh token with Claude Code's documented\n * `CLAUDE_CODE_OAUTH_REFRESH_TOKEN` login in a disposable configuration.\n * The caller owns the lock, durable writeback, and recovery.\n */\nexport async function refreshClaudeLogin(\n binary: string,\n login: ClaudeLogin & { refreshToken: string },\n workRoot: string,\n): Promise<ClaudeLogin> {\n await requireRefreshCapableClaude(binary);\n await mkdir(workRoot, { recursive: true, mode: 0o700 });\n const helper = await mkdtemp(join(workRoot, \"refresh-\"));\n try {\n const paths = await prepareHelper(helper);\n const code = await runHelper(binary, helperEnvironment(paths, login));\n return await storedRotation(paths.config, login, code);\n } finally {\n await rm(helper, { recursive: true, force: true });\n }\n}\n\nasync function requireRefreshCapableClaude(binary: string): Promise<void> {\n const version = await claudeVersion(binary);\n if (!version || !atLeastMinimum(version))\n throw new Error(\n `Refreshing the saved Claude login requires Claude Code ${CLAUDE_REFRESH_MINIMUM_VERSION} or later`,\n );\n}\n\nasync function claudeVersion(binary: string): Promise<number[] | undefined> {\n const proc = spawn([binary, \"--version\"], {\n stdin: \"ignore\",\n stdout: \"pipe\",\n stderr: \"ignore\",\n });\n const timer = setTimeout(() => {\n proc.kill(\"SIGKILL\");\n }, VERSION_TIMEOUT_MS);\n try {\n const [output, code] = await Promise.all([\n new Response(proc.stdout).text(),\n proc.exited,\n ]);\n const match = /^(\\d+)\\.(\\d+)\\.(\\d+)/.exec(output.trim());\n return code === 0 && match ? match.slice(1).map(Number) : undefined;\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport function atLeastMinimum(version: number[]): boolean {\n for (const [index, minimum] of MINIMUM_VERSION.entries()) {\n const part = version[index] ?? 0;\n if (part !== minimum) return part > minimum;\n }\n return true;\n}\n\nasync function prepareHelper(helper: string) {\n const paths = {\n config: join(helper, \"config\"),\n home: join(helper, \"home\"),\n bin: join(helper, \"bin\"),\n };\n await Promise.all(\n Object.values(paths).map((path) => mkdir(path, { mode: 0o700 })),\n );\n await writeFile(join(paths.bin, \"security\"), SECURITY_STUB, {\n flag: \"wx\",\n mode: 0o700,\n });\n return paths;\n}\n\nfunction helperEnvironment(\n paths: Awaited<ReturnType<typeof prepareHelper>>,\n login: ClaudeLogin & { refreshToken: string },\n): Record<string, string> {\n const forwarded = Object.fromEntries(\n FORWARDED_VARIABLES.flatMap((name) => {\n const value = process.env[name];\n return value ? [[name, value]] : [];\n }),\n );\n return {\n ...forwarded,\n PATH: [paths.bin, process.env.PATH ?? \"/usr/bin:/bin\"].join(delimiter),\n HOME: paths.home,\n TMPDIR: paths.home,\n LANG: process.env.LANG ?? \"C\",\n NO_COLOR: \"1\",\n CLAUDE_CONFIG_DIR: paths.config,\n CLAUDE_CODE_OAUTH_REFRESH_TOKEN: login.refreshToken,\n CLAUDE_CODE_OAUTH_SCOPES: login.scopes.join(\" \"),\n CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: \"1\",\n };\n}\n\n/** Output is discarded: it is never needed and must not reach evidence. */\nasync function runHelper(\n binary: string,\n env: Record<string, string>,\n): Promise<number> {\n const proc = spawn([binary, \"auth\", \"login\"], {\n env,\n cwd: env.HOME,\n stdin: \"ignore\",\n stdout: \"ignore\",\n stderr: \"ignore\",\n });\n const timer = setTimeout(() => {\n proc.kill(\"SIGKILL\");\n }, HELPER_TIMEOUT_MS);\n try {\n return await proc.exited;\n } finally {\n clearTimeout(timer);\n }\n}\n\n/** Accept only a persisted rotation; exit status alone proves nothing. */\nasync function storedRotation(\n config: string,\n login: ClaudeLogin,\n code: number,\n): Promise<ClaudeLogin> {\n const rotated = await helperLogin(join(config, \".credentials.json\"));\n if (!rotated?.refreshToken || rotated.accessToken === login.accessToken)\n throw new Error(\n `Claude could not refresh the saved login (helper exit ${String(code)}); run claude and /login if it has expired`,\n );\n return rotated;\n}\n\nasync function helperLogin(path: string): Promise<ClaudeLogin | undefined> {\n try {\n if ((await stat(path)).size > MAX_CREDENTIAL_BYTES) return undefined;\n return parseClaudeLogin(await readFile(path));\n } catch {\n return undefined;\n }\n}\n"]}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { homedir, userInfo } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
export const MAX_CREDENTIAL_BYTES = 1024 * 1024;
|
|
6
|
+
/** Resolve the store Claude Code itself uses for the selected login. */
|
|
7
|
+
export async function claudeLoginStore(sourceFile, keychain) {
|
|
8
|
+
if (sourceFile)
|
|
9
|
+
return fileLoginStore(sourceFile, dirname(sourceFile));
|
|
10
|
+
const configRoot = claudeConfigRoot();
|
|
11
|
+
const file = join(configRoot, ".credentials.json");
|
|
12
|
+
const saved = await keychainLoginStore(configRoot, keychain).catch(async (cause) => {
|
|
13
|
+
// Claude falls back to its saved file when the Keychain cannot be read.
|
|
14
|
+
if (await savedFileExists(file))
|
|
15
|
+
return undefined;
|
|
16
|
+
throw cause;
|
|
17
|
+
});
|
|
18
|
+
if (saved)
|
|
19
|
+
return saved;
|
|
20
|
+
await requireSavedFile(file);
|
|
21
|
+
return fileLoginStore(file, configRoot);
|
|
22
|
+
}
|
|
23
|
+
async function savedFileExists(path) {
|
|
24
|
+
try {
|
|
25
|
+
await stat(path);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function claudeConfigRoot() {
|
|
33
|
+
return (process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude")).normalize("NFC");
|
|
34
|
+
}
|
|
35
|
+
/** Claude keys a non-default configuration's Keychain item by its path. */
|
|
36
|
+
export function claudeKeychainService() {
|
|
37
|
+
const configured = process.env.CLAUDE_CONFIG_DIR;
|
|
38
|
+
if (!configured)
|
|
39
|
+
return "Claude Code-credentials";
|
|
40
|
+
const digest = createHash("sha256")
|
|
41
|
+
.update(configured.normalize("NFC"))
|
|
42
|
+
.digest("hex");
|
|
43
|
+
return `Claude Code-credentials-${digest.substring(0, 8)}`;
|
|
44
|
+
}
|
|
45
|
+
export function claudeKeychainAccount() {
|
|
46
|
+
let account;
|
|
47
|
+
try {
|
|
48
|
+
account = process.env.USER || userInfo().username;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return "claude-code-user";
|
|
52
|
+
}
|
|
53
|
+
return /^[a-zA-Z0-9._-]+$/.test(account) ? account : "claude-code-user";
|
|
54
|
+
}
|
|
55
|
+
async function keychainLoginStore(configRoot, keychain) {
|
|
56
|
+
if (!keychain)
|
|
57
|
+
return undefined;
|
|
58
|
+
const service = claudeKeychainService();
|
|
59
|
+
const account = claudeKeychainAccount();
|
|
60
|
+
if (!(await keychain.read(service, account)))
|
|
61
|
+
return undefined;
|
|
62
|
+
return {
|
|
63
|
+
origin: `macOS Keychain item "${service}"`,
|
|
64
|
+
lockRoot: configRoot,
|
|
65
|
+
read: async () => requireStoredLogin(await keychain.read(service, account)),
|
|
66
|
+
write: (bytes) => keychain.write(service, account, bytes),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function requireSavedFile(path) {
|
|
70
|
+
try {
|
|
71
|
+
await stat(path);
|
|
72
|
+
}
|
|
73
|
+
catch (cause) {
|
|
74
|
+
const code = cause.code;
|
|
75
|
+
throw new Error(code === "ENOENT"
|
|
76
|
+
? "Claude saved login is unavailable; run claude and /login"
|
|
77
|
+
: "Claude saved credential is unreadable or unavailable", { cause });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function requireStoredLogin(bytes) {
|
|
81
|
+
if (!bytes)
|
|
82
|
+
throw new Error("Claude credential is unreadable or unavailable");
|
|
83
|
+
return bytes;
|
|
84
|
+
}
|
|
85
|
+
export function requireBoundedCredential(credential) {
|
|
86
|
+
if (credential.byteLength === 0 ||
|
|
87
|
+
credential.byteLength > MAX_CREDENTIAL_BYTES ||
|
|
88
|
+
!credential.toString("utf8").trim())
|
|
89
|
+
throw new Error("Claude credential is empty or oversized");
|
|
90
|
+
return credential;
|
|
91
|
+
}
|
|
92
|
+
function fileLoginStore(path, lockRoot) {
|
|
93
|
+
return {
|
|
94
|
+
origin: path,
|
|
95
|
+
lockRoot,
|
|
96
|
+
read: () => readLoginFile(path),
|
|
97
|
+
write: (bytes) => replaceLoginFile(path, bytes),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
async function readLoginFile(path) {
|
|
101
|
+
try {
|
|
102
|
+
if ((await stat(path)).size > MAX_CREDENTIAL_BYTES)
|
|
103
|
+
throw new Error("oversized credential");
|
|
104
|
+
return await readFile(path);
|
|
105
|
+
}
|
|
106
|
+
catch (cause) {
|
|
107
|
+
throw new Error("Claude credential is unreadable or unavailable", {
|
|
108
|
+
cause,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Replace atomically so concurrent Claude readers never see a partial login. */
|
|
113
|
+
async function replaceLoginFile(path, bytes) {
|
|
114
|
+
const target = await realpath(path);
|
|
115
|
+
const temporary = join(dirname(target), `.sevro-login-${randomUUID()}.tmp`);
|
|
116
|
+
try {
|
|
117
|
+
await writeFile(temporary, bytes, { flag: "wx", mode: 0o600 });
|
|
118
|
+
await rename(temporary, target);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
await rm(temporary, { force: true });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=claude-login-store.js.map
|