codehere 1.0.1 → 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.
- package/README.md +36 -192
- package/dist/application/use-cases/check-claimed-tests.js +81 -0
- package/dist/application/use-cases/detect-test-weakening.js +237 -0
- package/dist/application/use-cases/rerun-tests.js +110 -7
- package/dist/index.js +38 -1212
- package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
- package/dist/presentation/cli/commands/check-command.js +178 -23
- package/dist/presentation/cli/commands/help-command.js +21 -285
- package/dist/ui.js +5 -236
- package/dist/utils/version.js +0 -6
- package/package.json +9 -38
- package/dist/application/services/verdict-service.js +0 -154
- package/dist/application/use-cases/assess-cross-vendor.js +0 -126
- package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
- package/dist/application/use-cases/domain-trust.js +0 -110
- package/dist/application/use-cases/export-aat.js +0 -107
- package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
- package/dist/application/use-cases/generate-final-report.js +0 -259
- package/dist/application/use-cases/generate-narration.js +0 -130
- package/dist/application/use-cases/generate-thread-title.js +0 -181
- package/dist/application/use-cases/narrator-prompt-template.js +0 -158
- package/dist/application/use-cases/plan-auto-review.js +0 -57
- package/dist/application/use-cases/reconcile-audit.js +0 -100
- package/dist/application/use-cases/search-sessions.js +0 -70
- package/dist/application/use-cases/should-auto-review.js +0 -71
- package/dist/application/use-cases/thread-export.js +0 -245
- package/dist/application/use-cases/verify-claim.js +0 -456
- package/dist/application/use-cases/write-pass.js +0 -277
- package/dist/domain/audit.js +0 -11
- package/dist/domain/services/codebase-detector.js +0 -85
- package/dist/domain/task.js +0 -91
- package/dist/error-handler.js +0 -139
- package/dist/infrastructure/completion/bash-completion.js +0 -29
- package/dist/infrastructure/completion/fish-completion.js +0 -17
- package/dist/infrastructure/completion/zsh-completion.js +0 -25
- package/dist/infrastructure/config/config-manager.js +0 -174
- package/dist/infrastructure/config/product-context.js +0 -49
- package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
- package/dist/infrastructure/context/adapters/figma-source.js +0 -198
- package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
- package/dist/infrastructure/context/adapters/linear-source.js +0 -190
- package/dist/infrastructure/context/context-resolver.js +0 -144
- package/dist/infrastructure/context/context-source.js +0 -114
- package/dist/infrastructure/context/github-api.js +0 -110
- package/dist/infrastructure/context/github-device-flow.js +0 -65
- package/dist/infrastructure/context/oauth-flow.js +0 -207
- package/dist/infrastructure/context/oauth-store.js +0 -122
- package/dist/infrastructure/context/registry-bootstrap.js +0 -95
- package/dist/infrastructure/context/write-pass-resolver.js +0 -106
- package/dist/infrastructure/cost/budget-config.js +0 -114
- package/dist/infrastructure/cost/cost-router.js +0 -97
- package/dist/infrastructure/cost/spend-aggregator.js +0 -222
- package/dist/infrastructure/cost/team-ledger.js +0 -355
- package/dist/infrastructure/discovery/discovery-log.js +0 -85
- package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
- package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
- package/dist/infrastructure/hooks/hook-mode.js +0 -87
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
- package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
- package/dist/infrastructure/hooks/session-analyzer.js +0 -177
- package/dist/infrastructure/observability/execution-tracer.js +0 -227
- package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
- package/dist/infrastructure/orchestration/agent-installer.js +0 -101
- package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
- package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
- package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
- package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
- package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
- package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
- package/dist/infrastructure/orchestration/context-handoff.js +0 -162
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
- package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
- package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
- package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
- package/dist/infrastructure/orchestration/failover-config.js +0 -67
- package/dist/infrastructure/orchestration/failover-policy.js +0 -106
- package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
- package/dist/infrastructure/orchestration/graphify-context.js +0 -209
- package/dist/infrastructure/orchestration/memory-injector.js +0 -380
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
- package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
- package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
- package/dist/infrastructure/orchestration/readiness.js +0 -103
- package/dist/infrastructure/orchestration/session-manager.js +0 -1590
- package/dist/infrastructure/orchestration/session-persistence.js +0 -394
- package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
- package/dist/infrastructure/protocols/cloud-sync.js +0 -207
- package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
- package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
- package/dist/infrastructure/protocols/mcp-tools.js +0 -401
- package/dist/infrastructure/protocols/probe-port.js +0 -36
- package/dist/infrastructure/protocols/thread-phase.js +0 -105
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
- package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
- package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
- package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
- package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
- package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
- package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
- package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
- package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
- package/dist/infrastructure/protocols/ui/styles.js +0 -2147
- package/dist/infrastructure/protocols/ui/tokens.js +0 -96
- package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
- package/dist/infrastructure/protocols/verify-page.js +0 -1677
- package/dist/infrastructure/security/file-collection.js +0 -118
- package/dist/infrastructure/security/fix-code.js +0 -144
- package/dist/infrastructure/security/fix-secret.js +0 -89
- package/dist/infrastructure/security/permissions-config.js +0 -235
- package/dist/infrastructure/security/scan-types.js +0 -10
- package/dist/infrastructure/security/ship-scan.js +0 -195
- package/dist/infrastructure/security/trust-scorer.js +0 -724
- package/dist/infrastructure/security/vibe-checks.js +0 -99
- package/dist/infrastructure/storage/atomic-write.js +0 -86
- package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
- package/dist/infrastructure/storage/project-thread-migration.js +0 -237
- package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
- package/dist/infrastructure/storage/project-thread-reader.js +0 -409
- package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
- package/dist/monitoring.js +0 -212
- package/dist/presentation/cli/commands/audit-command.js +0 -201
- package/dist/presentation/cli/commands/budget-command.js +0 -79
- package/dist/presentation/cli/commands/config-command.js +0 -125
- package/dist/presentation/cli/commands/context-source-command.js +0 -217
- package/dist/presentation/cli/commands/hook-command.js +0 -265
- package/dist/presentation/cli/commands/init-command.js +0 -590
- package/dist/presentation/cli/commands/memory-command.js +0 -1031
- package/dist/presentation/cli/commands/narrator-command.js +0 -101
- package/dist/presentation/cli/commands/permission-command.js +0 -105
- package/dist/presentation/cli/commands/protocols-builder.js +0 -254
- package/dist/presentation/cli/commands/protocols-command.js +0 -84
- package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
- package/dist/presentation/cli/commands/report-command.js +0 -403
- package/dist/presentation/cli/commands/scan-command.js +0 -52
- package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
- package/dist/presentation/cli/commands/search-command.js +0 -51
- package/dist/presentation/cli/commands/setup-command.js +0 -330
- package/dist/presentation/cli/commands/status-command.js +0 -614
- package/dist/presentation/cli/commands/team-command.js +0 -256
- package/dist/presentation/cli/commands/thread-command.js +0 -370
- package/dist/presentation/cli/commands/trust-command.js +0 -328
- package/dist/presentation/cli/commands/verify-command.js +0 -298
- package/dist/presentation/cli/error-display.js +0 -328
- package/dist/presentation/cli/repl.js +0 -805
- package/dist/presentation/cli/task-deliverable.js +0 -153
- package/dist/testing/cassette.js +0 -125
- package/dist/testing/replay-adapter.js +0 -147
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal GitHub REST layer for the integration — reads an issue and turns it
|
|
3
|
-
* into a task prompt. Uses the OAuth token persisted by github-device-flow via
|
|
4
|
-
* oauth-store ('github'). HTTP is injectable so tests run without network.
|
|
5
|
-
*
|
|
6
|
-
* This is the "issues -> tasks" piece of Track 2: fetch the issue, format a
|
|
7
|
-
* prompt; the existing POST /api/task does the actual spawn (one proven path).
|
|
8
|
-
*/
|
|
9
|
-
const defaultGet = async (url, headers) => {
|
|
10
|
-
const res = await fetch(url, { headers });
|
|
11
|
-
return { status: res.status, json: () => res.json() };
|
|
12
|
-
};
|
|
13
|
-
/** Accepts an issue URL, "owner/repo#123", or "owner/repo" (number then optional). */
|
|
14
|
-
export function parseRepoRef(input) {
|
|
15
|
-
const s = (input || '').trim();
|
|
16
|
-
const url = s.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/i);
|
|
17
|
-
if (url)
|
|
18
|
-
return { owner: url[1], repo: url[2], number: Number(url[3]) };
|
|
19
|
-
const hash = s.match(/^([^/\s]+)\/([^/\s#]+)#(\d+)$/);
|
|
20
|
-
if (hash)
|
|
21
|
-
return { owner: hash[1], repo: hash[2], number: Number(hash[3]) };
|
|
22
|
-
const plain = s.match(/^([^/\s]+)\/([^/\s#]+)$/);
|
|
23
|
-
if (plain)
|
|
24
|
-
return { owner: plain[1], repo: plain[2] };
|
|
25
|
-
return undefined;
|
|
26
|
-
}
|
|
27
|
-
export async function fetchIssue(token, owner, repo, number, http = defaultGet) {
|
|
28
|
-
const res = await http(`https://api.github.com/repos/${owner}/${repo}/issues/${number}`, {
|
|
29
|
-
Authorization: `Bearer ${token}`,
|
|
30
|
-
Accept: 'application/vnd.github+json',
|
|
31
|
-
'User-Agent': 'codehere',
|
|
32
|
-
});
|
|
33
|
-
if (res.status === 404)
|
|
34
|
-
throw new Error(`github issue not found: ${owner}/${repo}#${number}`);
|
|
35
|
-
if (res.status === 401 || res.status === 403)
|
|
36
|
-
throw new Error('github auth failed or token lacks repo scope');
|
|
37
|
-
if (res.status >= 400)
|
|
38
|
-
throw new Error(`github api error ${res.status}`);
|
|
39
|
-
const d = (await res.json());
|
|
40
|
-
return {
|
|
41
|
-
number: Number(d.number ?? number),
|
|
42
|
-
title: String(d.title ?? ''),
|
|
43
|
-
body: typeof d.body === 'string' ? d.body : '',
|
|
44
|
-
htmlUrl: String(d.html_url ?? ''),
|
|
45
|
-
state: String(d.state ?? 'open'),
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
/** List open issues for a repo so the user can PICK one instead of typing
|
|
49
|
-
* "owner/repo#123" from memory. Filters out pull requests (the issues endpoint
|
|
50
|
-
* returns PRs too — they carry a `pull_request` field). */
|
|
51
|
-
export async function listIssues(token, owner, repo, http = defaultGet) {
|
|
52
|
-
const res = await http(`https://api.github.com/repos/${owner}/${repo}/issues?state=open&per_page=50`, {
|
|
53
|
-
Authorization: `Bearer ${token}`,
|
|
54
|
-
Accept: 'application/vnd.github+json',
|
|
55
|
-
'User-Agent': 'codehere',
|
|
56
|
-
});
|
|
57
|
-
if (res.status === 404)
|
|
58
|
-
throw new Error(`github repo not found: ${owner}/${repo}`);
|
|
59
|
-
if (res.status === 401 || res.status === 403)
|
|
60
|
-
throw new Error('github auth failed or token lacks repo scope');
|
|
61
|
-
if (res.status >= 400)
|
|
62
|
-
throw new Error(`github api error ${res.status}`);
|
|
63
|
-
const arr = (await res.json());
|
|
64
|
-
return (Array.isArray(arr) ? arr : [])
|
|
65
|
-
.filter((d) => !d.pull_request)
|
|
66
|
-
.map((d) => ({
|
|
67
|
-
number: Number(d.number ?? 0),
|
|
68
|
-
title: String(d.title ?? ''),
|
|
69
|
-
body: typeof d.body === 'string' ? d.body : '',
|
|
70
|
-
htmlUrl: String(d.html_url ?? ''),
|
|
71
|
-
state: String(d.state ?? 'open'),
|
|
72
|
-
}));
|
|
73
|
-
}
|
|
74
|
-
/** Format an issue as a task prompt an agent can act on. */
|
|
75
|
-
export function issueToTaskPrompt(issue) {
|
|
76
|
-
const body = issue.body && issue.body.trim() ? `\n\n${issue.body.trim()}` : '';
|
|
77
|
-
return `Work on GitHub issue #${issue.number}: ${issue.title}${body}\n\nIssue: ${issue.htmlUrl}`;
|
|
78
|
-
}
|
|
79
|
-
/** Pull the issue ref out of a task prompt built by issueToTaskPrompt (the
|
|
80
|
-
* "Issue: <url>" line), so a finished task knows which issue to report back to. */
|
|
81
|
-
export function issueRefFromText(text) {
|
|
82
|
-
const m = (text || '').match(/github\.com\/[^\s)]+\/issues\/\d+/i);
|
|
83
|
-
if (!m)
|
|
84
|
-
return undefined;
|
|
85
|
-
const ref = parseRepoRef(m[0]);
|
|
86
|
-
return ref && ref.number !== undefined ? { owner: ref.owner, repo: ref.repo, number: ref.number } : undefined;
|
|
87
|
-
}
|
|
88
|
-
const defaultSend = async (url, method, headers, body) => {
|
|
89
|
-
const res = await fetch(url, { method, headers, body });
|
|
90
|
-
return { status: res.status, json: () => res.json() };
|
|
91
|
-
};
|
|
92
|
-
/** Post the agent's result back onto the issue as a comment. Non-destructive —
|
|
93
|
-
* closes the issue -> task -> result loop without touching any branches. */
|
|
94
|
-
export async function commentOnIssue(token, owner, repo, number, body, http = defaultSend) {
|
|
95
|
-
const res = await http(`https://api.github.com/repos/${owner}/${repo}/issues/${number}/comments`, 'POST', {
|
|
96
|
-
Authorization: `Bearer ${token}`,
|
|
97
|
-
Accept: 'application/vnd.github+json',
|
|
98
|
-
'Content-Type': 'application/json',
|
|
99
|
-
'User-Agent': 'codehere',
|
|
100
|
-
}, JSON.stringify({ body }));
|
|
101
|
-
if (res.status === 401 || res.status === 403)
|
|
102
|
-
throw new Error('github auth failed or token lacks repo scope');
|
|
103
|
-
if (res.status === 404)
|
|
104
|
-
throw new Error('github issue not found (or no access)');
|
|
105
|
-
if (res.status >= 400)
|
|
106
|
-
throw new Error(`github comment failed: ${res.status}`);
|
|
107
|
-
const d = (await res.json());
|
|
108
|
-
return { htmlUrl: String(d.html_url ?? '') };
|
|
109
|
-
}
|
|
110
|
-
//# sourceMappingURL=github-api.js.map
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
const DEVICE_CODE_URL = 'https://github.com/login/device/code';
|
|
2
|
-
const TOKEN_URL = 'https://github.com/login/oauth/access_token';
|
|
3
|
-
const DEFAULT_SCOPE = 'repo read:org';
|
|
4
|
-
const GRANT = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
5
|
-
/** The public client_id of codehere's registered GitHub OAuth App. This is a
|
|
6
|
-
* PUBLIC value (device flow needs no secret), so it ships as the default — the
|
|
7
|
-
* same way the `gh` CLI bundles its own. Set this once the OAuth App with Device
|
|
8
|
-
* Flow enabled is registered; until then GitHub connect stays inert and the UI
|
|
9
|
-
* shows the "configure your own" fallback. CODEHERE_GITHUB_CLIENT_ID overrides
|
|
10
|
-
* it (for users who prefer their own app). */
|
|
11
|
-
const BUNDLED_GITHUB_CLIENT_ID = '';
|
|
12
|
-
export function githubClientId() {
|
|
13
|
-
const env = (process.env.CODEHERE_GITHUB_CLIENT_ID || '').trim();
|
|
14
|
-
if (env)
|
|
15
|
-
return env;
|
|
16
|
-
const bundled = BUNDLED_GITHUB_CLIENT_ID.trim();
|
|
17
|
-
return bundled || undefined;
|
|
18
|
-
}
|
|
19
|
-
const defaultHttp = async (url, body) => {
|
|
20
|
-
const res = await fetch(url, {
|
|
21
|
-
method: 'POST',
|
|
22
|
-
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
|
23
|
-
body: JSON.stringify(body),
|
|
24
|
-
});
|
|
25
|
-
return (await res.json());
|
|
26
|
-
};
|
|
27
|
-
/** Step 1: ask GitHub for a device + user code. The user enters userCode at
|
|
28
|
-
* verificationUri to approve; we then poll for the token. */
|
|
29
|
-
export async function requestDeviceCode(clientId, scope = DEFAULT_SCOPE, http = defaultHttp) {
|
|
30
|
-
const d = await http(DEVICE_CODE_URL, { client_id: clientId, scope });
|
|
31
|
-
if (!d.device_code) {
|
|
32
|
-
throw new Error('github device-code request failed' + (d.error ? `: ${String(d.error)}` : ''));
|
|
33
|
-
}
|
|
34
|
-
return {
|
|
35
|
-
deviceCode: String(d.device_code),
|
|
36
|
-
userCode: String(d.user_code ?? ''),
|
|
37
|
-
verificationUri: String(d.verification_uri ?? 'https://github.com/login/device'),
|
|
38
|
-
intervalSec: Number(d.interval ?? 5),
|
|
39
|
-
expiresInSec: Number(d.expires_in ?? 900),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
/** Step 2: poll once for the token. Caller repeats at `intervalSec` until it
|
|
43
|
-
* resolves to connected or error (RFC 8628: authorization_pending = keep going,
|
|
44
|
-
* slow_down = back off). */
|
|
45
|
-
export async function pollForToken(clientId, deviceCode, http = defaultHttp) {
|
|
46
|
-
const d = await http(TOKEN_URL, { client_id: clientId, device_code: deviceCode, grant_type: GRANT });
|
|
47
|
-
if (d.access_token) {
|
|
48
|
-
return {
|
|
49
|
-
status: 'connected',
|
|
50
|
-
token: {
|
|
51
|
-
provider: 'github',
|
|
52
|
-
accessToken: String(d.access_token),
|
|
53
|
-
expiresAt: null, // GitHub OAuth App tokens don't expire unless revoked
|
|
54
|
-
scope: d.scope ? String(d.scope) : null,
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
const err = String(d.error ?? 'unknown');
|
|
59
|
-
if (err === 'authorization_pending')
|
|
60
|
-
return { status: 'pending' };
|
|
61
|
-
if (err === 'slow_down')
|
|
62
|
-
return { status: 'slow_down', intervalSec: Number(d.interval ?? 10) };
|
|
63
|
-
return { status: 'error', error: err };
|
|
64
|
-
}
|
|
65
|
-
//# sourceMappingURL=github-device-flow.js.map
|
|
@@ -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,122 +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 } from 'fs';
|
|
31
|
-
import { writeFileAtomic } from '../storage/atomic-write.js';
|
|
32
|
-
import { homedir } from 'os';
|
|
33
|
-
import { dirname, join } from 'path';
|
|
34
|
-
function configPath() {
|
|
35
|
-
const home = process.env.CODEHERE_HOME || homedir();
|
|
36
|
-
return join(home, '.codehere', 'context-sources.json');
|
|
37
|
-
}
|
|
38
|
-
export function readAllConfig(path) {
|
|
39
|
-
const target = path ?? configPath();
|
|
40
|
-
if (!existsSync(target))
|
|
41
|
-
return {};
|
|
42
|
-
try {
|
|
43
|
-
const raw = readFileSync(target, 'utf-8');
|
|
44
|
-
const parsed = JSON.parse(raw);
|
|
45
|
-
if (typeof parsed !== 'object' || parsed === null)
|
|
46
|
-
return {};
|
|
47
|
-
return parsed;
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
return {};
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
function writeAllConfig(cfg, path) {
|
|
54
|
-
const target = path ?? configPath();
|
|
55
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
56
|
-
// mode on the TEMP file, so the credentials are never briefly world-readable
|
|
57
|
-
// the way write-then-chmod leaves them. The chmod below stays as belt-and-braces.
|
|
58
|
-
writeFileAtomic(target, JSON.stringify(cfg, null, 2) + '\n', { mode: 0o600 });
|
|
59
|
-
try {
|
|
60
|
-
chmodSync(target, 0o600);
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
// chmod can fail on FATs · the local-first promise still holds; we just
|
|
64
|
-
// can't enforce 0600. Real-world users on macOS/Linux always succeed.
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
/** Persist a freshly-minted OAuth token for the given provider. */
|
|
68
|
-
export function saveToken(token, path) {
|
|
69
|
-
const cfg = readAllConfig(path);
|
|
70
|
-
const existing = cfg[token.provider] ?? {};
|
|
71
|
-
cfg[token.provider] = {
|
|
72
|
-
...existing,
|
|
73
|
-
oauth: {
|
|
74
|
-
accessToken: token.accessToken,
|
|
75
|
-
refreshToken: token.refreshToken,
|
|
76
|
-
expiresAt: token.expiresAt,
|
|
77
|
-
scope: token.scope,
|
|
78
|
-
userLabel: token.userLabel,
|
|
79
|
-
connectedAt: Date.now(),
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
writeAllConfig(cfg, path);
|
|
83
|
-
}
|
|
84
|
-
/** Disconnect · drops only the oauth subtree, preserves any manual apiKey. */
|
|
85
|
-
export function clearToken(providerId, path) {
|
|
86
|
-
const cfg = readAllConfig(path);
|
|
87
|
-
const entry = cfg[providerId];
|
|
88
|
-
if (!entry || !entry.oauth)
|
|
89
|
-
return false;
|
|
90
|
-
delete entry.oauth;
|
|
91
|
-
// If the entry is now empty (no manual apiKey either), drop the whole row.
|
|
92
|
-
const isEmpty = Object.keys(entry).filter((k) => k !== 'oauth').length === 0;
|
|
93
|
-
if (isEmpty)
|
|
94
|
-
delete cfg[providerId];
|
|
95
|
-
writeAllConfig(cfg, path);
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
/** Read the persisted OAuth token for a provider (undefined if not connected). */
|
|
99
|
-
export function loadToken(providerId, path) {
|
|
100
|
-
const cfg = readAllConfig(path);
|
|
101
|
-
return cfg[providerId]?.oauth;
|
|
102
|
-
}
|
|
103
|
-
export function connectionStatus(providerId, path) {
|
|
104
|
-
const cfg = readAllConfig(path);
|
|
105
|
-
const entry = cfg[providerId];
|
|
106
|
-
if (!entry)
|
|
107
|
-
return { status: 'disconnected' };
|
|
108
|
-
if (entry.oauth?.accessToken) {
|
|
109
|
-
const exp = entry.oauth.expiresAt;
|
|
110
|
-
const expired = typeof exp === 'number' && exp < Date.now();
|
|
111
|
-
return {
|
|
112
|
-
status: expired ? 'expired' : 'connected',
|
|
113
|
-
userLabel: entry.oauth.userLabel,
|
|
114
|
-
connectedAt: entry.oauth.connectedAt,
|
|
115
|
-
expiresAt: exp,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
if (entry.apiKey || entry.token)
|
|
119
|
-
return { status: 'manual' };
|
|
120
|
-
return { status: 'disconnected' };
|
|
121
|
-
}
|
|
122
|
-
//# sourceMappingURL=oauth-store.js.map
|
|
@@ -1,95 +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
|
-
/**
|
|
36
|
-
* Resolve config path · respects CODEHERE_HOME env so tests can isolate.
|
|
37
|
-
* File format: a JSON object keyed by adapter id, e.g.
|
|
38
|
-
* { "linear": { "apiKey": "..." }, "figma": { "token": "..." } }
|
|
39
|
-
*/
|
|
40
|
-
function configPath() {
|
|
41
|
-
const home = process.env.CODEHERE_HOME || homedir();
|
|
42
|
-
return join(home, '.codehere', 'context-sources.json');
|
|
43
|
-
}
|
|
44
|
-
function loadConfig() {
|
|
45
|
-
const path = configPath();
|
|
46
|
-
if (!existsSync(path))
|
|
47
|
-
return {};
|
|
48
|
-
try {
|
|
49
|
-
const raw = readFileSync(path, 'utf-8');
|
|
50
|
-
const parsed = JSON.parse(raw);
|
|
51
|
-
if (typeof parsed !== 'object' || parsed === null)
|
|
52
|
-
return {};
|
|
53
|
-
return parsed;
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
return {};
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Real-network FetchContext · used by the daemon at runtime. Tests inject
|
|
61
|
-
* a fake context so this code path never runs in CI.
|
|
62
|
-
*/
|
|
63
|
-
export function createDefaultFetchContext(opts) {
|
|
64
|
-
const config = opts.configOverride ?? loadConfig();
|
|
65
|
-
return {
|
|
66
|
-
config,
|
|
67
|
-
async httpGet(target, headers) {
|
|
68
|
-
const r = await fetch(target, { method: 'GET', headers: headers ?? {} });
|
|
69
|
-
const body = await r.text();
|
|
70
|
-
const hOut = {};
|
|
71
|
-
r.headers.forEach((v, k) => {
|
|
72
|
-
hOut[k.toLowerCase()] = v;
|
|
73
|
-
});
|
|
74
|
-
return { status: r.status, body, headers: hOut };
|
|
75
|
-
},
|
|
76
|
-
emitAuditLine: opts.emitAuditLine,
|
|
77
|
-
now: () => Date.now(),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
export function createDefaultFetchContextWithPost(opts) {
|
|
81
|
-
const base = createDefaultFetchContext(opts);
|
|
82
|
-
return {
|
|
83
|
-
...base,
|
|
84
|
-
async httpPost(target, body, headers) {
|
|
85
|
-
const r = await fetch(target, { method: 'POST', headers: headers ?? {}, body });
|
|
86
|
-
const text = await r.text();
|
|
87
|
-
const hOut = {};
|
|
88
|
-
r.headers.forEach((v, k) => {
|
|
89
|
-
hOut[k.toLowerCase()] = v;
|
|
90
|
-
});
|
|
91
|
-
return { status: r.status, body: text, headers: hOut };
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
//# sourceMappingURL=registry-bootstrap.js.map
|