codehere 0.6.2 → 0.7.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.
Files changed (70) hide show
  1. package/README.md +5 -3
  2. package/dist/application/use-cases/domain-trust.js +110 -0
  3. package/dist/application/use-cases/generate-final-report.js +28 -4
  4. package/dist/application/use-cases/generate-narration.js +11 -0
  5. package/dist/application/use-cases/generate-thread-title.js +4 -0
  6. package/dist/application/use-cases/thread-export.js +155 -0
  7. package/dist/application/use-cases/verify-claim.js +274 -0
  8. package/dist/application/use-cases/write-pass.js +277 -0
  9. package/dist/index.js +211 -16
  10. package/dist/infrastructure/completion/bash-completion.js +2 -2
  11. package/dist/infrastructure/config/config-manager.js +1 -1
  12. package/dist/infrastructure/context/adapters/drawio-source.js +157 -0
  13. package/dist/infrastructure/context/adapters/figma-source.js +163 -0
  14. package/dist/infrastructure/context/adapters/github-issue-source.js +159 -0
  15. package/dist/infrastructure/context/adapters/linear-source.js +190 -0
  16. package/dist/infrastructure/context/context-resolver.js +144 -0
  17. package/dist/infrastructure/context/context-source.js +114 -0
  18. package/dist/infrastructure/context/oauth-flow.js +207 -0
  19. package/dist/infrastructure/context/oauth-store.js +119 -0
  20. package/dist/infrastructure/context/registry-bootstrap.js +99 -0
  21. package/dist/infrastructure/context/write-pass-resolver.js +106 -0
  22. package/dist/infrastructure/hooks/claude-sdk-hook.js +73 -57
  23. package/dist/infrastructure/hooks/session-analyzer.js +65 -8
  24. package/dist/infrastructure/observability/execution-tracer.js +0 -5
  25. package/dist/infrastructure/orchestration/aider-adapter.js +45 -17
  26. package/dist/infrastructure/orchestration/artifacts-extractor.js +39 -5
  27. package/dist/infrastructure/orchestration/cassette-recorder.js +120 -0
  28. package/dist/infrastructure/orchestration/claude-code-adapter.js +33 -4
  29. package/dist/infrastructure/orchestration/codex-adapter.js +146 -13
  30. package/dist/infrastructure/orchestration/context-handoff.js +3 -2
  31. package/dist/infrastructure/orchestration/control/codex-control-surface.js +99 -0
  32. package/dist/infrastructure/orchestration/control/control-surface.js +15 -0
  33. package/dist/infrastructure/orchestration/gemini-adapter.js +12 -2
  34. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +233 -0
  35. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +73 -0
  36. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +83 -0
  37. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +37 -0
  38. package/dist/infrastructure/orchestration/opencode-adapter.js +22 -2
  39. package/dist/infrastructure/orchestration/progress-aggregator.js +177 -3
  40. package/dist/infrastructure/orchestration/session-manager.js +94 -6
  41. package/dist/infrastructure/orchestration/session-persistence.js +139 -30
  42. package/dist/infrastructure/orchestration/thread-key-store.js +127 -0
  43. package/dist/infrastructure/orchestration/write-pass-store.js +46 -0
  44. package/dist/infrastructure/protocols/cloud-sync.js +1 -6
  45. package/dist/infrastructure/protocols/mcp-server-impl.js +1280 -12
  46. package/dist/infrastructure/protocols/thread-phase.js +105 -0
  47. package/dist/infrastructure/protocols/ui-page-v2.js +3768 -75
  48. package/dist/infrastructure/protocols/ui-page.js +12 -3299
  49. package/dist/infrastructure/protocols/verify-page.js +941 -0
  50. package/dist/infrastructure/security/trust-scorer.js +76 -1
  51. package/dist/infrastructure/storage/cross-tool-agents.js +1 -1
  52. package/dist/infrastructure/storage/project-thread-migration.js +2 -2
  53. package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
  54. package/dist/monitoring.js +1 -1
  55. package/dist/presentation/cli/commands/audit-command.js +126 -0
  56. package/dist/presentation/cli/commands/config-command.js +2 -2
  57. package/dist/presentation/cli/commands/context-source-command.js +198 -0
  58. package/dist/presentation/cli/commands/help-command.js +51 -1
  59. package/dist/presentation/cli/commands/report-command.js +6 -4
  60. package/dist/presentation/cli/commands/setup-command.js +71 -18
  61. package/dist/presentation/cli/commands/status-command.js +0 -4
  62. package/dist/presentation/cli/commands/thread-command.js +288 -0
  63. package/dist/presentation/cli/commands/verify-command.js +233 -0
  64. package/dist/presentation/cli/error-display.js +1 -1
  65. package/dist/presentation/cli/repl.js +32 -1
  66. package/dist/presentation/cli/task-deliverable.js +127 -0
  67. package/dist/testing/cassette.js +125 -0
  68. package/dist/testing/replay-adapter.js +166 -0
  69. package/dist/ui.js +18 -1
  70. package/package.json +4 -2
@@ -0,0 +1,207 @@
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
@@ -0,0 +1,119 @@
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
@@ -0,0 +1,99 @@
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
@@ -0,0 +1,106 @@
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