codehere 0.8.0 → 1.0.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.
Files changed (119) hide show
  1. package/README.md +37 -184
  2. package/dist/application/use-cases/check-claimed-tests.js +81 -0
  3. package/dist/application/use-cases/detect-test-weakening.js +237 -0
  4. package/dist/application/use-cases/rerun-tests.js +329 -0
  5. package/dist/index.js +74 -1004
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
  7. package/dist/presentation/cli/commands/check-command.js +251 -0
  8. package/dist/presentation/cli/commands/help-command.js +22 -286
  9. package/dist/ui.js +19 -438
  10. package/dist/utils/version.js +0 -12
  11. package/package.json +18 -27
  12. package/dist/application/use-cases/domain-trust.js +0 -110
  13. package/dist/application/use-cases/generate-final-report.js +0 -134
  14. package/dist/application/use-cases/generate-narration.js +0 -128
  15. package/dist/application/use-cases/generate-thread-title.js +0 -178
  16. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  17. package/dist/application/use-cases/thread-export.js +0 -155
  18. package/dist/application/use-cases/verify-claim.js +0 -274
  19. package/dist/application/use-cases/write-pass.js +0 -277
  20. package/dist/domain/entities/task.js +0 -61
  21. package/dist/domain/services/codebase-detector.js +0 -91
  22. package/dist/error-handler.js +0 -251
  23. package/dist/infrastructure/completion/bash-completion.js +0 -25
  24. package/dist/infrastructure/completion/fish-completion.js +0 -22
  25. package/dist/infrastructure/completion/zsh-completion.js +0 -30
  26. package/dist/infrastructure/config/config-manager.js +0 -173
  27. package/dist/infrastructure/config/product-context.js +0 -67
  28. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  29. package/dist/infrastructure/context/adapters/figma-source.js +0 -163
  30. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
  31. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  32. package/dist/infrastructure/context/context-resolver.js +0 -144
  33. package/dist/infrastructure/context/context-source.js +0 -114
  34. package/dist/infrastructure/context/oauth-flow.js +0 -207
  35. package/dist/infrastructure/context/oauth-store.js +0 -119
  36. package/dist/infrastructure/context/registry-bootstrap.js +0 -99
  37. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  38. package/dist/infrastructure/cost/budget-config.js +0 -113
  39. package/dist/infrastructure/cost/cost-router.js +0 -104
  40. package/dist/infrastructure/cost/spend-aggregator.js +0 -147
  41. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
  42. package/dist/infrastructure/hooks/hook-mode.js +0 -71
  43. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
  44. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  45. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  46. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  47. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  48. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  49. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -56
  50. package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
  51. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  52. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
  53. package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
  54. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
  55. package/dist/infrastructure/orchestration/context-handoff.js +0 -144
  56. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  57. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  58. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  59. package/dist/infrastructure/orchestration/failover-config.js +0 -77
  60. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  61. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
  62. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
  63. package/dist/infrastructure/orchestration/graphify-context.js +0 -254
  64. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  65. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
  66. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  67. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  68. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  69. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -510
  70. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
  71. package/dist/infrastructure/orchestration/readiness.js +0 -103
  72. package/dist/infrastructure/orchestration/session-manager.js +0 -1091
  73. package/dist/infrastructure/orchestration/session-persistence.js +0 -258
  74. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  75. package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
  76. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  77. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  78. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -2853
  79. package/dist/infrastructure/protocols/probe-port.js +0 -36
  80. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  81. package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
  82. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  83. package/dist/infrastructure/protocols/verify-page.js +0 -983
  84. package/dist/infrastructure/security/file-collection.js +0 -118
  85. package/dist/infrastructure/security/input-validator.js +0 -152
  86. package/dist/infrastructure/security/permissions-config.js +0 -234
  87. package/dist/infrastructure/security/trust-scorer.js +0 -657
  88. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  89. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  90. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  91. package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
  92. package/dist/infrastructure/storage/project-thread-reader.js +0 -172
  93. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  94. package/dist/infrastructure/storage/task-repository.js +0 -206
  95. package/dist/monitoring.js +0 -212
  96. package/dist/presentation/cli/commands/audit-command.js +0 -126
  97. package/dist/presentation/cli/commands/budget-command.js +0 -79
  98. package/dist/presentation/cli/commands/config-command.js +0 -125
  99. package/dist/presentation/cli/commands/context-source-command.js +0 -198
  100. package/dist/presentation/cli/commands/hook-command.js +0 -265
  101. package/dist/presentation/cli/commands/init-command.js +0 -578
  102. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  103. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  104. package/dist/presentation/cli/commands/permission-command.js +0 -116
  105. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  106. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  107. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  108. package/dist/presentation/cli/commands/report-command.js +0 -387
  109. package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
  110. package/dist/presentation/cli/commands/setup-command.js +0 -307
  111. package/dist/presentation/cli/commands/status-command.js +0 -661
  112. package/dist/presentation/cli/commands/thread-command.js +0 -288
  113. package/dist/presentation/cli/commands/trust-command.js +0 -306
  114. package/dist/presentation/cli/commands/verify-command.js +0 -233
  115. package/dist/presentation/cli/error-display.js +0 -336
  116. package/dist/presentation/cli/repl.js +0 -797
  117. package/dist/presentation/cli/task-deliverable.js +0 -127
  118. package/dist/testing/cassette.js +0 -125
  119. package/dist/testing/replay-adapter.js +0 -166
@@ -1,207 +0,0 @@
1
- /**
2
- * OAuth 2.0 + PKCE flow for context-source providers (v0.8 S2.5).
3
- *
4
- * Provider-agnostic state machine that handles the four steps of the
5
- * code-grant flow with PKCE (RFC 7636) so we don't need a client secret on
6
- * the user's machine:
7
- *
8
- * 1. begin(provider) · build authorize URL · stash state + verifier
9
- * 2. (browser) user approves · provider redirects to our callback
10
- * 3. complete(code, state) · exchange code for {access_token, refresh_token, user}
11
- * 4. refresh(refresh_token) · swap for a fresh access_token
12
- *
13
- * Tokens persist in ~/.codehere/context-sources.json (the same file manual
14
- * configs use). User can revoke from the provider's web UI any time; our
15
- * disconnect command just deletes the local config.
16
- *
17
- * Provider client IDs come from env vars (CODEHERE_LINEAR_CLIENT_ID,
18
- * CODEHERE_FIGMA_CLIENT_ID) so binaries can ship without secrets baked in.
19
- * The codehere app registrations are public client IDs · PKCE removes the
20
- * need for a client secret on the user's machine. Anyone running codehere
21
- * locally uses the same `codehere-local` app at each provider, no per-user
22
- * registration needed.
23
- *
24
- * IMPORTANT · this module is pure logic. All I/O (httpPost, filesystem,
25
- * clock) is injected. The daemon's HTTP route at /oauth/callback supplies
26
- * the production wiring (registry-bootstrap.ts) and tests use mocks.
27
- */
28
- import { createHash, randomBytes } from 'crypto';
29
- const DEFAULT_REDIRECT = 'http://127.0.0.1:3773/oauth/callback';
30
- export const PROVIDERS = {
31
- linear: {
32
- id: 'linear',
33
- label: 'Linear',
34
- authorizeUrl: 'https://linear.app/oauth/authorize',
35
- tokenUrl: 'https://api.linear.app/oauth/token',
36
- scopes: ['read'],
37
- clientIdEnv: 'CODEHERE_LINEAR_CLIENT_ID',
38
- redirectUri: DEFAULT_REDIRECT,
39
- tokenPostMode: 'form',
40
- userInfoUrl: 'https://api.linear.app/graphql', // POST { query: '{ viewer { name email } }' }
41
- },
42
- figma: {
43
- id: 'figma',
44
- label: 'Figma',
45
- authorizeUrl: 'https://www.figma.com/oauth',
46
- tokenUrl: 'https://api.figma.com/v1/oauth/token',
47
- scopes: ['file_read'],
48
- clientIdEnv: 'CODEHERE_FIGMA_CLIENT_ID',
49
- redirectUri: DEFAULT_REDIRECT,
50
- tokenPostMode: 'form',
51
- userInfoUrl: 'https://api.figma.com/v1/me',
52
- },
53
- };
54
- export function listProviders() {
55
- return Object.values(PROVIDERS);
56
- }
57
- export function getProvider(id) {
58
- return PROVIDERS[id];
59
- }
60
- /* ──────────────────────────────────────────────────────────────────────── */
61
- /* PKCE helpers */
62
- /* ──────────────────────────────────────────────────────────────────────── */
63
- function base64UrlEncode(buf) {
64
- return buf.toString('base64').replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
65
- }
66
- /** Random 43-128 char string · we pick 64 bytes (86 chars after b64url). */
67
- export function generateCodeVerifier() {
68
- return base64UrlEncode(randomBytes(64));
69
- }
70
- export function deriveCodeChallenge(verifier) {
71
- return base64UrlEncode(createHash('sha256').update(verifier).digest());
72
- }
73
- /** Opaque CSRF defense · returned by provider unchanged · we verify on callback. */
74
- export function generateState() {
75
- return base64UrlEncode(randomBytes(24));
76
- }
77
- export function begin(provider, ctx) {
78
- if (!ctx.clientId) {
79
- return { kind: 'not_registered', provider: provider.id, envVar: provider.clientIdEnv };
80
- }
81
- const codeVerifier = ctx.rng?.verifier() ?? generateCodeVerifier();
82
- const codeChallenge = deriveCodeChallenge(codeVerifier);
83
- const state = ctx.rng?.state() ?? generateState();
84
- const startedAt = ctx.now ? ctx.now() : Date.now();
85
- const params = new URLSearchParams({
86
- response_type: 'code',
87
- client_id: ctx.clientId,
88
- redirect_uri: provider.redirectUri,
89
- scope: provider.scopes.join(' '),
90
- state,
91
- code_challenge: codeChallenge,
92
- code_challenge_method: 'S256',
93
- });
94
- return {
95
- authorizeUrl: `${provider.authorizeUrl}?${params.toString()}`,
96
- pending: { provider: provider.id, state, codeVerifier, startedAt },
97
- };
98
- }
99
- export async function complete(code, state, ctx) {
100
- const pending = ctx.pending.get(state);
101
- if (!pending) {
102
- return { kind: 'state_unknown', detail: `no pending flow for state '${state.slice(0, 8)}...'` };
103
- }
104
- ctx.pending.delete(state); // single-use
105
- const provider = getProvider(pending.provider);
106
- if (!provider) {
107
- return { kind: 'state_unknown', detail: `unknown provider '${pending.provider}'` };
108
- }
109
- const maxAge = ctx.maxAgeMs ?? 10 * 60 * 1000;
110
- const now = ctx.now ? ctx.now() : Date.now();
111
- if (now - pending.startedAt > maxAge) {
112
- return { kind: 'state_expired', detail: `pending flow older than ${Math.round(maxAge / 1000)}s` };
113
- }
114
- if (!ctx.clientId) {
115
- return { kind: 'not_registered', detail: provider.clientIdEnv };
116
- }
117
- const params = new URLSearchParams({
118
- grant_type: 'authorization_code',
119
- code,
120
- redirect_uri: provider.redirectUri,
121
- client_id: ctx.clientId,
122
- code_verifier: pending.codeVerifier,
123
- });
124
- const headers = {
125
- 'Content-Type': 'application/x-www-form-urlencoded',
126
- Accept: 'application/json',
127
- };
128
- const res = await ctx.httpPost(provider.tokenUrl, params.toString(), headers);
129
- if (res.status !== 200) {
130
- return {
131
- kind: 'provider_error',
132
- detail: `${provider.label} token endpoint returned ${res.status}: ${res.body.slice(0, 200)}`,
133
- };
134
- }
135
- let payload;
136
- try {
137
- payload = JSON.parse(res.body);
138
- }
139
- catch (err) {
140
- return {
141
- kind: 'parse_error',
142
- detail: `token response not JSON: ${err instanceof Error ? err.message : String(err)}`,
143
- };
144
- }
145
- if (!payload.access_token) {
146
- return { kind: 'parse_error', detail: 'token response missing access_token' };
147
- }
148
- const token = {
149
- provider: provider.id,
150
- accessToken: payload.access_token,
151
- refreshToken: payload.refresh_token,
152
- expiresAt: typeof payload.expires_in === 'number' ? now + payload.expires_in * 1000 : null,
153
- scope: payload.scope ?? provider.scopes.join(' '),
154
- };
155
- // Best-effort user-info fetch · failure here doesn't undo the connect
156
- if (ctx.fetchUserInfo && provider.userInfoUrl) {
157
- try {
158
- const u = await ctx.fetchUserInfo(token.accessToken, provider.userInfoUrl);
159
- if (u) {
160
- const label = u.email || u.name;
161
- if (label)
162
- token.userLabel = label;
163
- }
164
- }
165
- catch {
166
- // ignore · token is valid, user label is decoration
167
- }
168
- }
169
- return token;
170
- }
171
- export async function refresh(provider, refreshToken, ctx) {
172
- if (!ctx.clientId)
173
- return { kind: 'not_registered', detail: provider.clientIdEnv };
174
- const params = new URLSearchParams({
175
- grant_type: 'refresh_token',
176
- refresh_token: refreshToken,
177
- client_id: ctx.clientId,
178
- });
179
- const res = await ctx.httpPost(provider.tokenUrl, params.toString(), {
180
- 'Content-Type': 'application/x-www-form-urlencoded',
181
- Accept: 'application/json',
182
- });
183
- if (res.status !== 200) {
184
- return { kind: 'provider_error', detail: `${provider.label} refresh returned ${res.status}` };
185
- }
186
- let payload;
187
- try {
188
- payload = JSON.parse(res.body);
189
- }
190
- catch (err) {
191
- return {
192
- kind: 'parse_error',
193
- detail: `refresh response not JSON: ${err instanceof Error ? err.message : String(err)}`,
194
- };
195
- }
196
- if (!payload.access_token)
197
- return { kind: 'parse_error', detail: 'refresh missing access_token' };
198
- const now = ctx.now ? ctx.now() : Date.now();
199
- return {
200
- provider: provider.id,
201
- accessToken: payload.access_token,
202
- refreshToken: payload.refresh_token ?? refreshToken,
203
- expiresAt: typeof payload.expires_in === 'number' ? now + payload.expires_in * 1000 : null,
204
- scope: payload.scope ?? null,
205
- };
206
- }
207
- //# sourceMappingURL=oauth-flow.js.map
@@ -1,119 +0,0 @@
1
- /**
2
- * OAuth token storage · v0.8 S2.5.
3
- *
4
- * Persists OAuth tokens to ~/.codehere/context-sources.json next to manual
5
- * configs. File shape evolved · backward-compatible:
6
- *
7
- * {
8
- * "linear": {
9
- * "apiKey": "...", // manual mode · still supported
10
- * "oauth": { // OAuth mode · NEW
11
- * "accessToken": "...",
12
- * "refreshToken": "...",
13
- * "expiresAt": 1700000000000,
14
- * "scope": "read",
15
- * "userLabel": "ega@example.com",
16
- * "connectedAt": 1700000000000
17
- * }
18
- * },
19
- * "figma": { ... }
20
- * }
21
- *
22
- * The linear-source / figma-source adapters read EITHER apiKey OR
23
- * oauth.accessToken (in S3 we'll merge them under one access path; for now
24
- * S2.5 just lands the storage shape so the OAuth flow has somewhere to put
25
- * the token).
26
- *
27
- * File permissions · 0600 on write so a multi-user machine can't read each
28
- * other's tokens.
29
- */
30
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
31
- import { homedir } from 'os';
32
- import { dirname, join } from 'path';
33
- function configPath() {
34
- const home = process.env.CODEHERE_HOME || homedir();
35
- return join(home, '.codehere', 'context-sources.json');
36
- }
37
- export function readAllConfig(path) {
38
- const target = path ?? configPath();
39
- if (!existsSync(target))
40
- return {};
41
- try {
42
- const raw = readFileSync(target, 'utf-8');
43
- const parsed = JSON.parse(raw);
44
- if (typeof parsed !== 'object' || parsed === null)
45
- return {};
46
- return parsed;
47
- }
48
- catch {
49
- return {};
50
- }
51
- }
52
- function writeAllConfig(cfg, path) {
53
- const target = path ?? configPath();
54
- mkdirSync(dirname(target), { recursive: true });
55
- writeFileSync(target, JSON.stringify(cfg, null, 2) + '\n', 'utf-8');
56
- try {
57
- chmodSync(target, 0o600);
58
- }
59
- catch {
60
- // chmod can fail on FATs · the local-first promise still holds; we just
61
- // can't enforce 0600. Real-world users on macOS/Linux always succeed.
62
- }
63
- }
64
- /** Persist a freshly-minted OAuth token for the given provider. */
65
- export function saveToken(token, path) {
66
- const cfg = readAllConfig(path);
67
- const existing = cfg[token.provider] ?? {};
68
- cfg[token.provider] = {
69
- ...existing,
70
- oauth: {
71
- accessToken: token.accessToken,
72
- refreshToken: token.refreshToken,
73
- expiresAt: token.expiresAt,
74
- scope: token.scope,
75
- userLabel: token.userLabel,
76
- connectedAt: Date.now(),
77
- },
78
- };
79
- writeAllConfig(cfg, path);
80
- }
81
- /** Disconnect · drops only the oauth subtree, preserves any manual apiKey. */
82
- export function clearToken(providerId, path) {
83
- const cfg = readAllConfig(path);
84
- const entry = cfg[providerId];
85
- if (!entry || !entry.oauth)
86
- return false;
87
- delete entry.oauth;
88
- // If the entry is now empty (no manual apiKey either), drop the whole row.
89
- const isEmpty = Object.keys(entry).filter((k) => k !== 'oauth').length === 0;
90
- if (isEmpty)
91
- delete cfg[providerId];
92
- writeAllConfig(cfg, path);
93
- return true;
94
- }
95
- /** Read the persisted OAuth token for a provider (undefined if not connected). */
96
- export function loadToken(providerId, path) {
97
- const cfg = readAllConfig(path);
98
- return cfg[providerId]?.oauth;
99
- }
100
- export function connectionStatus(providerId, path) {
101
- const cfg = readAllConfig(path);
102
- const entry = cfg[providerId];
103
- if (!entry)
104
- return { status: 'disconnected' };
105
- if (entry.oauth?.accessToken) {
106
- const exp = entry.oauth.expiresAt;
107
- const expired = typeof exp === 'number' && exp < Date.now();
108
- return {
109
- status: expired ? 'expired' : 'connected',
110
- userLabel: entry.oauth.userLabel,
111
- connectedAt: entry.oauth.connectedAt,
112
- expiresAt: exp,
113
- };
114
- }
115
- if (entry.apiKey || entry.token)
116
- return { status: 'manual' };
117
- return { status: 'disconnected' };
118
- }
119
- //# sourceMappingURL=oauth-store.js.map
@@ -1,99 +0,0 @@
1
- /**
2
- * Registry bootstrap · v0.8 S2.
3
- *
4
- * Builds the default ContextSourceRegistry at daemon startup with our 1st-
5
- * party adapters (Linear, Figma), loads adapter config from
6
- * `~/.codehere/context-sources.json`, and provides a default
7
- * ContextSourceFetchContext that wraps node's fetch() for the network path
8
- * and the audit chain for telemetry.
9
- *
10
- * This file is the ONLY place where context-sources touch process.env, the
11
- * filesystem, and global fetch · everything else is pure code + injected
12
- * primitives. Makes tests trivial and the security boundary explicit.
13
- */
14
- import { existsSync, readFileSync } from 'fs';
15
- import { homedir } from 'os';
16
- import { join } from 'path';
17
- import { createRegistry, register, } from './context-source.js';
18
- import { linearSource } from './adapters/linear-source.js';
19
- import { figmaSource } from './adapters/figma-source.js';
20
- import { drawioSource } from './adapters/drawio-source.js';
21
- import { githubIssueSource } from './adapters/github-issue-source.js';
22
- /** Built once per daemon · in-memory · re-read from disk on demand via `reload`. */
23
- let cached = null;
24
- export function getDefaultRegistry() {
25
- if (cached)
26
- return cached;
27
- const reg = createRegistry();
28
- register(reg, linearSource);
29
- register(reg, figmaSource);
30
- register(reg, drawioSource);
31
- register(reg, githubIssueSource);
32
- cached = reg;
33
- return reg;
34
- }
35
- /** Forget the cached registry · used by tests + future `codehere context-source reload`. */
36
- export function resetDefaultRegistry() {
37
- cached = null;
38
- }
39
- /**
40
- * Resolve config path · respects CODEHERE_HOME env so tests can isolate.
41
- * File format: a JSON object keyed by adapter id, e.g.
42
- * { "linear": { "apiKey": "..." }, "figma": { "token": "..." } }
43
- */
44
- function configPath() {
45
- const home = process.env.CODEHERE_HOME || homedir();
46
- return join(home, '.codehere', 'context-sources.json');
47
- }
48
- function loadConfig() {
49
- const path = configPath();
50
- if (!existsSync(path))
51
- return {};
52
- try {
53
- const raw = readFileSync(path, 'utf-8');
54
- const parsed = JSON.parse(raw);
55
- if (typeof parsed !== 'object' || parsed === null)
56
- return {};
57
- return parsed;
58
- }
59
- catch {
60
- return {};
61
- }
62
- }
63
- /**
64
- * Real-network FetchContext · used by the daemon at runtime. Tests inject
65
- * a fake context so this code path never runs in CI.
66
- */
67
- export function createDefaultFetchContext(opts) {
68
- const config = opts.configOverride ?? loadConfig();
69
- return {
70
- config,
71
- async httpGet(target, headers) {
72
- const r = await fetch(target, { method: 'GET', headers: headers ?? {} });
73
- const body = await r.text();
74
- const hOut = {};
75
- r.headers.forEach((v, k) => {
76
- hOut[k.toLowerCase()] = v;
77
- });
78
- return { status: r.status, body, headers: hOut };
79
- },
80
- emitAuditLine: opts.emitAuditLine,
81
- now: () => Date.now(),
82
- };
83
- }
84
- export function createDefaultFetchContextWithPost(opts) {
85
- const base = createDefaultFetchContext(opts);
86
- return {
87
- ...base,
88
- async httpPost(target, body, headers) {
89
- const r = await fetch(target, { method: 'POST', headers: headers ?? {}, body });
90
- const text = await r.text();
91
- const hOut = {};
92
- r.headers.forEach((v, k) => {
93
- hOut[k.toLowerCase()] = v;
94
- });
95
- return { status: r.status, body: text, headers: hOut };
96
- },
97
- };
98
- }
99
- //# sourceMappingURL=registry-bootstrap.js.map
@@ -1,106 +0,0 @@
1
- /**
2
- * Write-pass target resolver · v0.9 S6 part B.
3
- *
4
- * Turns a W-PASS target like `linear://INFRA-247` into a concrete
5
- * HTTPS request the daemon can fire on the user's behalf, AFTER the
6
- * user has approved the payload. The resolver layer mirrors the
7
- * context-source registry · same scheme namespace, opposite direction
8
- * (read vs write).
9
- *
10
- * Resolved request shape:
11
- * - url · absolute HTTPS
12
- * - method · same as the intent (POST/PUT/PATCH/DELETE)
13
- * - headers · auth + content-type · loaded from ctx.config[scheme]
14
- * - body · JSON-stringified payload
15
- *
16
- * Tests pass a fake httpRequest · production uses node's fetch. Same
17
- * seam as context-source so adapters look symmetric.
18
- *
19
- * S6 ships ONE resolver (Linear) to prove the shape. github + figma +
20
- * drawio + notion follow in subsequent slices · each is a self-contained
21
- * function in this file, so adding one is a copy-paste-modify, not an
22
- * architectural change.
23
- */
24
- import { gradeDomain, hostOf } from '../../application/use-cases/domain-trust.js';
25
- export class WritePassResolveError extends Error {
26
- code;
27
- constructor(code, message) {
28
- super(`[w-pass resolve] ${code}: ${message}`);
29
- this.code = code;
30
- }
31
- }
32
- /**
33
- * Resolve an intent + effective payload into a concrete HTTP request.
34
- * Throws WritePassResolveError if the scheme isn't supported OR auth
35
- * is missing · the daemon turns those into a 4xx response BEFORE
36
- * touching the network.
37
- */
38
- export function resolveWriteRequest(intent, effectivePayload, config) {
39
- const m = intent.target.match(/^([a-z][a-z0-9+.-]*):\/\/(.+)$/);
40
- if (!m) {
41
- throw new WritePassResolveError('invalid_target', `bad URL: ${intent.target}`);
42
- }
43
- const scheme = m[1];
44
- const rest = m[2];
45
- const built = scheme === 'linear'
46
- ? resolveLinear(rest, intent.method, effectivePayload, config.linear)
47
- : scheme === 'github'
48
- ? resolveGitHub(rest, intent.method, effectivePayload, config.github)
49
- : null;
50
- if (built) {
51
- return { ...built, trust: gradeDomain(hostOf(built.url)) };
52
- }
53
- throw new WritePassResolveError('unsupported_scheme', `no W-PASS resolver for "${scheme}" yet · supported in S6: linear, github`);
54
- }
55
- /* ──────────────────────────────────────────────────────────────────────── */
56
- /* Linear */
57
- /* ──────────────────────────────────────────────────────────────────────── */
58
- function resolveLinear(rest, method, payload, cfg) {
59
- const auth = cfg?.oauth?.accessToken ? `Bearer ${cfg.oauth.accessToken}` : cfg?.apiKey;
60
- if (!auth) {
61
- throw new WritePassResolveError('auth_missing', 'no Linear apiKey or OAuth token in config["linear"] · agent cannot write without explicit credentials');
62
- }
63
- // Linear's API is GraphQL · we send mutations via POST. The agent's
64
- // intent.payload is expected to be { query, variables } or { mutation, variables }.
65
- // We pass it through verbatim · the human saw + approved it.
66
- const endpoint = cfg?.endpoint ?? 'https://api.linear.app/graphql';
67
- return {
68
- url: endpoint,
69
- // Linear writes always POST (it's GraphQL) · intent.method is recorded
70
- // for audit but doesn't change the transport. We log a one-line audit
71
- // hint when method != POST so the user sees the mediation.
72
- method: 'POST',
73
- headers: {
74
- Authorization: auth,
75
- 'Content-Type': 'application/json',
76
- 'X-Codehere-Mediated': 'w-pass',
77
- 'X-Codehere-Original-Method': method,
78
- 'X-Codehere-Identifier': rest,
79
- },
80
- body: JSON.stringify(payload),
81
- };
82
- }
83
- /* ──────────────────────────────────────────────────────────────────────── */
84
- /* GitHub */
85
- /* ──────────────────────────────────────────────────────────────────────── */
86
- function resolveGitHub(rest, method, payload, cfg) {
87
- if (!cfg?.token) {
88
- throw new WritePassResolveError('auth_missing', 'no GitHub token in config["github"] · public-by-URL is READ only · writes need an explicit PAT');
89
- }
90
- // rest is e.g. owner/repo/issues/42/comments or owner/repo/issues/42
91
- const endpoint = cfg.endpoint ?? 'https://api.github.com';
92
- return {
93
- url: `${endpoint}/repos/${rest}`,
94
- method,
95
- headers: {
96
- Authorization: `Bearer ${cfg.token}`,
97
- Accept: 'application/vnd.github+json',
98
- 'X-GitHub-Api-Version': '2022-11-28',
99
- 'Content-Type': 'application/json',
100
- 'User-Agent': 'codehere/0.9 w-pass',
101
- 'X-Codehere-Mediated': 'w-pass',
102
- },
103
- body: JSON.stringify(payload),
104
- };
105
- }
106
- //# sourceMappingURL=write-pass-resolver.js.map
@@ -1,113 +0,0 @@
1
- /**
2
- * v0.6 S3 slice 2 — daily spend budget config.
3
- *
4
- * Reads ~/.codehere/budget.yaml. The pre-spawn check in /api/task compares
5
- * computeSpendToday().totalUsd against `daily_usd`; over-budget refuses with
6
- * a friendly message instead of silently spawning another paid call.
7
- *
8
- * Why this is the v1 promise · claim_market_position v3 lists "cost-bounded
9
- * execution" as one of the few capture-strategy edges still possibly open.
10
- * Slice 1 surfaced spend; slice 2 enforces it. Together they make the edge
11
- * demonstrable in 60 seconds: set a $1 budget, spawn until you hit it,
12
- * watch codehere refuse the next spawn.
13
- *
14
- * Schema (~/.codehere/budget.yaml):
15
- *
16
- * daily_usd: 5.00 # null / omitted = no budget enforcement
17
- * warn_at_pct: 70 # default 70 — chip turns yellow at 70%
18
- *
19
- * Single-file, hand-editable, plain YAML. Per claim_user_persona HIGH:
20
- * non-technical users can edit it directly. Per overconfidence-guard:
21
- * the budget is a USER ceiling, not a codehere guarantee — we can't
22
- * see the actual provider invoice in real time.
23
- */
24
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
25
- import { dirname, join } from 'path';
26
- import { homedir } from 'os';
27
- const DEFAULT_CONFIG = {
28
- dailyUsd: null,
29
- warnAtPct: 70,
30
- };
31
- function configFile() {
32
- return join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'budget.yaml');
33
- }
34
- /** Parse a tiny subset of YAML — `key: value` pairs, # comments, blank lines.
35
- * No nesting, no lists, no multi-line. Same approach as permissions-config:
36
- * keep the dependency surface zero, schema simple. */
37
- function parseYaml(content) {
38
- const out = {};
39
- for (const rawLine of content.split('\n')) {
40
- const line = rawLine.replace(/#.*$/, '').trim();
41
- if (!line)
42
- continue;
43
- const colonIdx = line.indexOf(':');
44
- if (colonIdx === -1)
45
- continue;
46
- const key = line.slice(0, colonIdx).trim();
47
- const val = line.slice(colonIdx + 1).trim();
48
- if (key)
49
- out[key] = val;
50
- }
51
- return out;
52
- }
53
- /** Read the budget config from disk. Missing file or parse failure → defaults
54
- * (no enforcement). Best-effort: never throws — a corrupt file should not
55
- * break the spawn path. */
56
- export function loadBudget() {
57
- const path = configFile();
58
- if (!existsSync(path))
59
- return { ...DEFAULT_CONFIG };
60
- try {
61
- const raw = readFileSync(path, 'utf-8');
62
- const parsed = parseYaml(raw);
63
- const cfg = { ...DEFAULT_CONFIG };
64
- if ('daily_usd' in parsed) {
65
- const n = Number(parsed.daily_usd);
66
- cfg.dailyUsd = Number.isFinite(n) && n > 0 ? n : null;
67
- }
68
- if ('warn_at_pct' in parsed) {
69
- const n = Number(parsed.warn_at_pct);
70
- cfg.warnAtPct = Number.isFinite(n) && n > 0 && n <= 100 ? n : DEFAULT_CONFIG.warnAtPct;
71
- }
72
- return cfg;
73
- }
74
- catch {
75
- return { ...DEFAULT_CONFIG };
76
- }
77
- }
78
- /** Persist the budget config. Creates ~/.codehere/ if missing. Comments
79
- * in the emitted YAML help a non-technical user edit it later. */
80
- export function saveBudget(cfg) {
81
- const path = configFile();
82
- const dir = dirname(path);
83
- if (!existsSync(dir))
84
- mkdirSync(dir, { recursive: true });
85
- const lines = [
86
- '# codehere daily-spend budget. Edit by hand or via `codehere budget set <amount>`.',
87
- "# daily_usd: hard ceiling for today's estimated spend. Set to a number to enable.",
88
- '# Leave commented out / omit to disable budget enforcement entirely.',
89
- ];
90
- if (cfg.dailyUsd !== null) {
91
- lines.push(`daily_usd: ${cfg.dailyUsd}`);
92
- }
93
- else {
94
- lines.push('# daily_usd: 5.00');
95
- }
96
- lines.push(`warn_at_pct: ${cfg.warnAtPct}`);
97
- lines.push('');
98
- writeFileSync(path, lines.join('\n'), 'utf-8');
99
- }
100
- /** True when today's estimated spend has hit or exceeded the daily ceiling.
101
- * Returns false when no budget is set (no enforcement). */
102
- export function isOverBudget(spentTodayUsd, cfg = loadBudget()) {
103
- if (cfg.dailyUsd === null)
104
- return false;
105
- return spentTodayUsd >= cfg.dailyUsd;
106
- }
107
- /** Percentage of today's budget consumed (0-100, or null when no budget set). */
108
- export function budgetUsedPct(spentTodayUsd, cfg = loadBudget()) {
109
- if (cfg.dailyUsd === null || cfg.dailyUsd === 0)
110
- return null;
111
- return Math.min(100, Math.round((spentTodayUsd / cfg.dailyUsd) * 100));
112
- }
113
- //# sourceMappingURL=budget-config.js.map