codehere 0.7.1 → 1.0.1
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 +22 -13
- package/dist/application/services/verdict-service.js +154 -0
- package/dist/application/use-cases/assess-cross-vendor.js +126 -0
- package/dist/application/use-cases/build-cross-vendor-verdict.js +118 -0
- package/dist/application/use-cases/export-aat.js +107 -0
- package/dist/application/use-cases/generate-acceptance-criteria.js +223 -0
- package/dist/application/use-cases/generate-final-report.js +140 -15
- package/dist/application/use-cases/generate-narration.js +5 -3
- package/dist/application/use-cases/generate-thread-title.js +5 -2
- package/dist/application/use-cases/narrator-prompt-template.js +1 -1
- package/dist/application/use-cases/plan-auto-review.js +57 -0
- package/dist/application/use-cases/reconcile-audit.js +100 -0
- package/dist/application/use-cases/rerun-tests.js +226 -0
- package/dist/application/use-cases/search-sessions.js +70 -0
- package/dist/application/use-cases/should-auto-review.js +71 -0
- package/dist/application/use-cases/thread-export.js +90 -0
- package/dist/application/use-cases/verify-claim.js +187 -5
- package/dist/domain/audit.js +11 -0
- package/dist/domain/services/codebase-detector.js +0 -6
- package/dist/domain/task.js +91 -0
- package/dist/error-handler.js +3 -115
- package/dist/index.js +279 -31
- package/dist/infrastructure/completion/bash-completion.js +7 -3
- package/dist/infrastructure/completion/fish-completion.js +7 -12
- package/dist/infrastructure/completion/zsh-completion.js +7 -12
- package/dist/infrastructure/config/config-manager.js +4 -3
- package/dist/infrastructure/config/product-context.js +1 -19
- package/dist/infrastructure/context/adapters/figma-source.js +37 -2
- package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
- package/dist/infrastructure/context/github-api.js +110 -0
- package/dist/infrastructure/context/github-device-flow.js +65 -0
- package/dist/infrastructure/context/oauth-store.js +5 -2
- package/dist/infrastructure/context/registry-bootstrap.js +0 -4
- package/dist/infrastructure/cost/budget-config.js +4 -3
- package/dist/infrastructure/cost/cost-router.js +16 -23
- package/dist/infrastructure/cost/spend-aggregator.js +91 -16
- package/dist/infrastructure/cost/team-ledger.js +355 -0
- package/dist/infrastructure/discovery/discovery-log.js +85 -0
- package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
- package/dist/infrastructure/hooks/claude-sdk-hook.js +139 -3
- package/dist/infrastructure/hooks/hook-mode.js +20 -4
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
- package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
- package/dist/infrastructure/observability/execution-tracer.js +1 -1
- package/dist/infrastructure/orchestration/agents-config-seed.js +23 -11
- package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
- package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
- package/dist/infrastructure/orchestration/cassette-recorder.js +1 -1
- package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
- package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
- package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
- package/dist/infrastructure/orchestration/context-handoff.js +32 -14
- package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
- package/dist/infrastructure/orchestration/failover-config.js +1 -11
- package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +134 -3
- package/dist/infrastructure/orchestration/graphify-context.js +14 -59
- package/dist/infrastructure/orchestration/memory-injector.js +4 -1
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +167 -9
- package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
- package/dist/infrastructure/orchestration/progress-aggregator.js +275 -23
- package/dist/infrastructure/orchestration/session-manager.js +593 -94
- package/dist/infrastructure/orchestration/session-persistence.js +177 -41
- package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
- package/dist/infrastructure/protocols/cloud-sync.js +2 -2
- package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
- package/dist/infrastructure/protocols/mcp-server-impl.js +1498 -354
- package/dist/infrastructure/protocols/mcp-tools.js +401 -0
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
- package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
- package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
- package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
- package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
- package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
- package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
- package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
- package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
- package/dist/infrastructure/protocols/ui/styles.js +2147 -0
- package/dist/infrastructure/protocols/ui/tokens.js +96 -0
- package/dist/infrastructure/protocols/ui-page-v2.js +116 -4126
- package/dist/infrastructure/protocols/verify-page.js +884 -148
- package/dist/infrastructure/security/fix-code.js +144 -0
- package/dist/infrastructure/security/fix-secret.js +89 -0
- package/dist/infrastructure/security/permissions-config.js +3 -2
- package/dist/infrastructure/security/scan-types.js +10 -0
- package/dist/infrastructure/security/ship-scan.js +195 -0
- package/dist/infrastructure/security/trust-scorer.js +70 -3
- package/dist/infrastructure/security/vibe-checks.js +99 -0
- package/dist/infrastructure/storage/atomic-write.js +86 -0
- package/dist/infrastructure/storage/project-thread-mirror.js +151 -4
- package/dist/infrastructure/storage/project-thread-reader.js +262 -25
- package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
- package/dist/monitoring.js +2 -2
- package/dist/presentation/cli/commands/audit-command.js +79 -4
- package/dist/presentation/cli/commands/budget-command.js +2 -2
- package/dist/presentation/cli/commands/check-command.js +96 -0
- package/dist/presentation/cli/commands/context-source-command.js +20 -1
- package/dist/presentation/cli/commands/help-command.js +5 -5
- package/dist/presentation/cli/commands/hook-command.js +5 -5
- package/dist/presentation/cli/commands/init-command.js +89 -66
- package/dist/presentation/cli/commands/memory-command.js +28 -28
- package/dist/presentation/cli/commands/permission-command.js +1 -12
- package/dist/presentation/cli/commands/protocols-builder.js +2 -2
- package/dist/presentation/cli/commands/protocols-command.js +4 -4
- package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
- package/dist/presentation/cli/commands/report-command.js +48 -32
- package/dist/presentation/cli/commands/scan-command.js +52 -0
- package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
- package/dist/presentation/cli/commands/search-command.js +51 -0
- package/dist/presentation/cli/commands/setup-command.js +31 -8
- package/dist/presentation/cli/commands/status-command.js +39 -86
- package/dist/presentation/cli/commands/team-command.js +256 -0
- package/dist/presentation/cli/commands/thread-command.js +87 -5
- package/dist/presentation/cli/commands/trust-command.js +53 -31
- package/dist/presentation/cli/commands/verify-command.js +74 -9
- package/dist/presentation/cli/error-display.js +0 -8
- package/dist/presentation/cli/repl.js +15 -7
- package/dist/presentation/cli/task-deliverable.js +36 -10
- package/dist/testing/replay-adapter.js +0 -19
- package/dist/ui.js +51 -239
- package/dist/utils/version.js +0 -6
- package/package.json +30 -10
- package/dist/domain/entities/task.js +0 -61
- package/dist/infrastructure/protocols/ui-page.js +0 -2377
- package/dist/infrastructure/security/input-validator.js +0 -152
- package/dist/infrastructure/storage/embeddings-path.js +0 -37
- package/dist/infrastructure/storage/task-repository.js +0 -206
|
@@ -95,7 +95,7 @@ export const figmaSource = {
|
|
|
95
95
|
if (!node)
|
|
96
96
|
throw new ContextSourceError('figma', 'not_found', `node ${nodeId} not in response`);
|
|
97
97
|
text = renderNodeAsTree(node, fileKey, nodeId);
|
|
98
|
-
metadata = { fileKey, nodeId, type: node.type, name: node.name };
|
|
98
|
+
metadata = { fileKey, nodeId, type: node.type, name: node.name, frames: collectFrames(node) };
|
|
99
99
|
}
|
|
100
100
|
else {
|
|
101
101
|
const parsed = JSON.parse(res.body);
|
|
@@ -104,7 +104,12 @@ export const figmaSource = {
|
|
|
104
104
|
if (!parsed.document)
|
|
105
105
|
throw new ContextSourceError('figma', 'parse', 'Figma response missing document');
|
|
106
106
|
text = renderFile(parsed, fileKey);
|
|
107
|
-
metadata = {
|
|
107
|
+
metadata = {
|
|
108
|
+
fileKey,
|
|
109
|
+
name: parsed.name ?? null,
|
|
110
|
+
lastModified: parsed.lastModified ?? null,
|
|
111
|
+
frames: collectFrames(parsed.document),
|
|
112
|
+
};
|
|
108
113
|
}
|
|
109
114
|
return {
|
|
110
115
|
url: url.toString(),
|
|
@@ -116,6 +121,36 @@ export const figmaSource = {
|
|
|
116
121
|
};
|
|
117
122
|
},
|
|
118
123
|
};
|
|
124
|
+
function collectFrames(root, limit = 12) {
|
|
125
|
+
const frames = [];
|
|
126
|
+
const visit = (node) => {
|
|
127
|
+
if (frames.length >= limit)
|
|
128
|
+
return;
|
|
129
|
+
if (node.type === 'FRAME') {
|
|
130
|
+
const bb = node.absoluteBoundingBox;
|
|
131
|
+
frames.push({
|
|
132
|
+
name: node.name,
|
|
133
|
+
width: bb ? Math.round(bb.width) : null,
|
|
134
|
+
height: bb ? Math.round(bb.height) : null,
|
|
135
|
+
text: firstText(node),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
for (const child of node.children ?? [])
|
|
139
|
+
visit(child);
|
|
140
|
+
};
|
|
141
|
+
visit(root);
|
|
142
|
+
return frames;
|
|
143
|
+
}
|
|
144
|
+
function firstText(node) {
|
|
145
|
+
if (node.characters)
|
|
146
|
+
return node.characters.replace(/\s+/g, ' ').slice(0, MAX_TEXT_PER_NODE);
|
|
147
|
+
for (const child of node.children ?? []) {
|
|
148
|
+
const t = firstText(child);
|
|
149
|
+
if (t)
|
|
150
|
+
return t;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
119
154
|
function renderFile(file, fileKey) {
|
|
120
155
|
const lines = [];
|
|
121
156
|
lines.push(`Figma file · ${file.name ?? '(unnamed)'}`);
|
|
@@ -61,8 +61,9 @@ export const githubIssueSource = {
|
|
|
61
61
|
'X-GitHub-Api-Version': '2022-11-28',
|
|
62
62
|
'User-Agent': 'codehere/0.8',
|
|
63
63
|
};
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
const bearer = cfg.token ?? cfg.oauth?.accessToken;
|
|
65
|
+
if (bearer)
|
|
66
|
+
headers['Authorization'] = `Bearer ${bearer}`;
|
|
66
67
|
const issueRes = await ctx.httpGet(`${endpoint}/repos/${ref.owner}/${ref.repo}/issues/${ref.number}`, headers);
|
|
67
68
|
if (issueRes.status === 404) {
|
|
68
69
|
throw new ContextSourceError('github', 'not_found', `${ref.owner}/${ref.repo}#${ref.number} not found`);
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
* File permissions · 0600 on write so a multi-user machine can't read each
|
|
28
28
|
* other's tokens.
|
|
29
29
|
*/
|
|
30
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync
|
|
30
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync } from 'fs';
|
|
31
|
+
import { writeFileAtomic } from '../storage/atomic-write.js';
|
|
31
32
|
import { homedir } from 'os';
|
|
32
33
|
import { dirname, join } from 'path';
|
|
33
34
|
function configPath() {
|
|
@@ -52,7 +53,9 @@ export function readAllConfig(path) {
|
|
|
52
53
|
function writeAllConfig(cfg, path) {
|
|
53
54
|
const target = path ?? configPath();
|
|
54
55
|
mkdirSync(dirname(target), { recursive: true });
|
|
55
|
-
|
|
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 });
|
|
56
59
|
try {
|
|
57
60
|
chmodSync(target, 0o600);
|
|
58
61
|
}
|
|
@@ -32,10 +32,6 @@ export function getDefaultRegistry() {
|
|
|
32
32
|
cached = reg;
|
|
33
33
|
return reg;
|
|
34
34
|
}
|
|
35
|
-
/** Forget the cached registry · used by tests + future `codehere context-source reload`. */
|
|
36
|
-
export function resetDefaultRegistry() {
|
|
37
|
-
cached = null;
|
|
38
|
-
}
|
|
39
35
|
/**
|
|
40
36
|
* Resolve config path · respects CODEHERE_HOME env so tests can isolate.
|
|
41
37
|
* File format: a JSON object keyed by adapter id, e.g.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { writeFileAtomic } from '../storage/atomic-write.js';
|
|
1
2
|
/**
|
|
2
3
|
* v0.6 S3 slice 2 — daily spend budget config.
|
|
3
4
|
*
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
* the budget is a USER ceiling, not a codehere guarantee — we can't
|
|
22
23
|
* see the actual provider invoice in real time.
|
|
23
24
|
*/
|
|
24
|
-
import { existsSync, mkdirSync, readFileSync
|
|
25
|
+
import { existsSync, mkdirSync, readFileSync } from 'fs';
|
|
25
26
|
import { dirname, join } from 'path';
|
|
26
27
|
import { homedir } from 'os';
|
|
27
28
|
const DEFAULT_CONFIG = {
|
|
@@ -29,7 +30,7 @@ const DEFAULT_CONFIG = {
|
|
|
29
30
|
warnAtPct: 70,
|
|
30
31
|
};
|
|
31
32
|
function configFile() {
|
|
32
|
-
return join(homedir(), '.codehere', 'budget.yaml');
|
|
33
|
+
return join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'budget.yaml');
|
|
33
34
|
}
|
|
34
35
|
/** Parse a tiny subset of YAML — `key: value` pairs, # comments, blank lines.
|
|
35
36
|
* No nesting, no lists, no multi-line. Same approach as permissions-config:
|
|
@@ -95,7 +96,7 @@ export function saveBudget(cfg) {
|
|
|
95
96
|
}
|
|
96
97
|
lines.push(`warn_at_pct: ${cfg.warnAtPct}`);
|
|
97
98
|
lines.push('');
|
|
98
|
-
|
|
99
|
+
writeFileAtomic(path, lines.join('\n'));
|
|
99
100
|
}
|
|
100
101
|
/** True when today's estimated spend has hit or exceeded the daily ceiling.
|
|
101
102
|
* Returns false when no budget is set (no enforcement). */
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* routing decisions have real financial impact at scale.
|
|
7
7
|
*
|
|
8
8
|
* Model tiers (approximate $/M output tokens, 2026):
|
|
9
|
-
* fast tier (
|
|
10
|
-
* balanced tier (
|
|
11
|
-
* max tier (
|
|
9
|
+
* fast tier (claude-haiku-4-5) $1.25 — cheap, good for simple tasks
|
|
10
|
+
* balanced tier (claude-sonnet-4-6) $15.00 — good for most tasks
|
|
11
|
+
* max tier (claude-fable-5) $50.00 — strongest (Anthropic's current flagship,
|
|
12
|
+
* Mythos-class; 1M ctx); use sparingly
|
|
12
13
|
*
|
|
13
14
|
* Routing logic:
|
|
14
15
|
* trust >= 90, simple task → fast tier (60x cheaper than max tier)
|
|
@@ -16,15 +17,20 @@
|
|
|
16
17
|
* trust < 75, or critical → balanced tier with human review flag
|
|
17
18
|
* critical findings present → block before reaching model
|
|
18
19
|
*/
|
|
19
|
-
const MODEL_COSTS = {
|
|
20
|
+
export const MODEL_COSTS = {
|
|
20
21
|
haiku: 1.25,
|
|
21
22
|
sonnet: 15.0,
|
|
22
|
-
opus:
|
|
23
|
+
opus: 50.0,
|
|
23
24
|
};
|
|
25
|
+
// NB: the `opus` key is the internal name for the MAX / strongest tier. Its model
|
|
26
|
+
// is now Claude Fable 5 (Anthropic's current flagship, released 2026-06-09 —
|
|
27
|
+
// stronger than Opus 4.8, which it falls back to on high-risk queries). User-facing
|
|
28
|
+
// surfaces show the tier label ("max tier") + this modelId, so the key name is
|
|
29
|
+
// internal-only.
|
|
24
30
|
const MODEL_IDS = {
|
|
25
31
|
haiku: 'claude-haiku-4-5-20251001',
|
|
26
32
|
sonnet: 'claude-sonnet-4-6',
|
|
27
|
-
opus: 'claude-
|
|
33
|
+
opus: 'claude-fable-5',
|
|
28
34
|
};
|
|
29
35
|
const TIER_LABELS = {
|
|
30
36
|
haiku: 'fast tier',
|
|
@@ -84,21 +90,8 @@ export function recommendModel(trustScore, complexity = 'standard', criticalFind
|
|
|
84
90
|
humanReview: true,
|
|
85
91
|
};
|
|
86
92
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
* @param distribution Fraction of tasks at each tier
|
|
92
|
-
*/
|
|
93
|
-
export function estimateMonthlySavings(avgTokensPerTask = 2000, tasksPerMonth = 500, distribution = { haiku: 0.4, sonnet: 0.55, opus: 0.05 }) {
|
|
94
|
-
const tokensMillion = avgTokensPerTask / 1_000_000;
|
|
95
|
-
let routedCost = 0;
|
|
96
|
-
for (const [tier, fraction] of Object.entries(distribution)) {
|
|
97
|
-
routedCost += fraction * tasksPerMonth * tokensMillion * MODEL_COSTS[tier];
|
|
98
|
-
}
|
|
99
|
-
const opusCost = tasksPerMonth * tokensMillion * MODEL_COSTS.opus;
|
|
100
|
-
const saving = opusCost - routedCost;
|
|
101
|
-
const savingPct = (saving / opusCost) * 100;
|
|
102
|
-
return { routedCost, opusCost, saving, savingPct };
|
|
103
|
-
}
|
|
93
|
+
// MED #29 (atomic audit) · estimateMonthlySavings deleted: a pure projection
|
|
94
|
+
// with zero production callers whose output ("~83% savings") had been quoted
|
|
95
|
+
// as a shipped outcome. The honest claim now lives in PRODUCT_STATE (🟡,
|
|
96
|
+
// projection); the budget gate carries the cost-bounded pillar.
|
|
104
97
|
//# sourceMappingURL=cost-router.js.map
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { existsSync, readFileSync } from 'fs';
|
|
23
23
|
import { homedir } from 'os';
|
|
24
|
-
import { join } from 'path';
|
|
24
|
+
import { basename, join } from 'path';
|
|
25
25
|
/** Resolved at call time, not module load, so test setups that swap $HOME
|
|
26
26
|
* after import still get a fresh path on each invocation. */
|
|
27
27
|
function sessionsFile() {
|
|
28
|
-
return join(homedir(), '.codehere', 'sessions.jsonl');
|
|
28
|
+
return join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'sessions.jsonl');
|
|
29
29
|
}
|
|
30
30
|
/** USD per million OUTPUT tokens at the agent's typical 2026 tier. Input is
|
|
31
31
|
* estimated at 1/5 of output rate (Anthropic + OpenAI both publish ~5x
|
|
@@ -34,7 +34,7 @@ function sessionsFile() {
|
|
|
34
34
|
*
|
|
35
35
|
* Conservative-ceiling philosophy: better to over-estimate spend than
|
|
36
36
|
* under-promise and miss a real bill. */
|
|
37
|
-
const COST_PER_M_OUTPUT_TOKENS_USD = {
|
|
37
|
+
export const COST_PER_M_OUTPUT_TOKENS_USD = {
|
|
38
38
|
'claude-code': 15.0, // Sonnet 4.6 baseline
|
|
39
39
|
codex: 10.0, // GPT-5 / codex baseline
|
|
40
40
|
aider: 15.0, // typically Sonnet
|
|
@@ -44,28 +44,69 @@ const COST_PER_M_OUTPUT_TOKENS_USD = {
|
|
|
44
44
|
};
|
|
45
45
|
const INPUT_RATE_RATIO = 0.2; // input ≈ 1/5 of output
|
|
46
46
|
const CACHE_READ_RATIO = 0.1; // cache reads ≈ 1/10 of input (so 1/50 output)
|
|
47
|
+
/** Per-MODEL published rates (USD per million tokens), 2026. Matched by substring
|
|
48
|
+
* so version suffixes resolve (claude-opus-4-x, gpt-5-codex, gemini-2.5-pro).
|
|
49
|
+
* This is what makes an Opus run cost more than a Haiku run instead of one flat
|
|
50
|
+
* per-agent tier, and prices a paid model even when the agent's default is free.
|
|
51
|
+
* Still an estimate, not an invoice — see the file header. */
|
|
52
|
+
const MODEL_RATES = [
|
|
53
|
+
// Anthropic — verified against platform.claude.com rate card (2026-07). Standard
|
|
54
|
+
// (post-introductory) rates; Sonnet 5's Aug-2026 intro price is lower, so the
|
|
55
|
+
// standard rate keeps the estimate conservative rather than under-billing.
|
|
56
|
+
{ match: /fable|mythos/i, inputUsd: 10, outputUsd: 50 }, // Claude 5 flagship tier
|
|
57
|
+
{ match: /opus/i, inputUsd: 5, outputUsd: 25 },
|
|
58
|
+
{ match: /sonnet/i, inputUsd: 3, outputUsd: 15 },
|
|
59
|
+
{ match: /haiku/i, inputUsd: 1, outputUsd: 5 },
|
|
60
|
+
{ match: /gpt-5|codex/i, inputUsd: 1.25, outputUsd: 10 },
|
|
61
|
+
{ match: /gemini.*(2\.5.*pro|1\.5.*pro|-pro)/i, inputUsd: 1.25, outputUsd: 10 }, // paid Gemini Pro tiers
|
|
62
|
+
];
|
|
63
|
+
/** Self-hosted / local backends cost nothing per token — never bill them at a
|
|
64
|
+
* cloud tier just because the agent (e.g. aider) usually runs on a paid model. */
|
|
65
|
+
const LOCAL_MODEL = /(?:^|[/:])(?:ollama(?:_chat)?|lmstudio|lm-studio|llama[._-]?cpp|local)\b/i;
|
|
66
|
+
export function modelRate(model) {
|
|
67
|
+
if (!model)
|
|
68
|
+
return undefined;
|
|
69
|
+
for (const r of MODEL_RATES)
|
|
70
|
+
if (r.match.test(model))
|
|
71
|
+
return { inputUsd: r.inputUsd, outputUsd: r.outputUsd };
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
47
74
|
/**
|
|
48
|
-
* Estimate dollar cost for one session's token usage
|
|
49
|
-
*
|
|
75
|
+
* Estimate dollar cost for one session's token usage. Per-model rates when the
|
|
76
|
+
* adapter surfaced the real model (accurate input AND output); otherwise the
|
|
77
|
+
* agent's typical output tier (input ≈ 1/5). Returns 0 when neither is priced.
|
|
50
78
|
*/
|
|
51
79
|
export function estimateCostUsd(agent, usage) {
|
|
52
80
|
if (!usage)
|
|
53
81
|
return 0;
|
|
54
|
-
const outputRate = COST_PER_M_OUTPUT_TOKENS_USD[agent];
|
|
55
|
-
if (outputRate === undefined || outputRate === 0)
|
|
56
|
-
return 0;
|
|
57
|
-
const inputRate = outputRate * INPUT_RATE_RATIO;
|
|
58
|
-
const cacheReadRate = inputRate * CACHE_READ_RATIO;
|
|
59
|
-
// Cache CREATION is billed at input rate (Anthropic). Use same default.
|
|
60
82
|
const tokens = {
|
|
61
83
|
input: Math.max(0, usage.inputTokens || 0),
|
|
62
84
|
output: Math.max(0, usage.outputTokens || 0),
|
|
63
85
|
cacheRead: Math.max(0, usage.cacheReadTokens || 0),
|
|
64
86
|
cacheCreate: Math.max(0, usage.cacheCreationTokens || 0),
|
|
65
87
|
};
|
|
88
|
+
// Local/self-hosted models are free to call — don't bill them at cloud rates.
|
|
89
|
+
// EXCEPT ollama's `:cloud`-tagged models: no local weights, proxied to Ollama's
|
|
90
|
+
// billed cloud — "ollama/glm-5.2:cloud" must NOT be recorded as $0.
|
|
91
|
+
if (usage.model && LOCAL_MODEL.test(usage.model) && !/:cloud$/i.test(usage.model))
|
|
92
|
+
return 0;
|
|
93
|
+
// Per-model when the real model is known — also prices a paid model whose agent
|
|
94
|
+
// default tier is 0 (closes the gemini/opencode $0-on-a-paid-backend hole).
|
|
95
|
+
const mr = modelRate(usage.model);
|
|
96
|
+
if (mr) {
|
|
97
|
+
return ((tokens.input / 1_000_000) * mr.inputUsd +
|
|
98
|
+
(tokens.output / 1_000_000) * mr.outputUsd +
|
|
99
|
+
(tokens.cacheRead / 1_000_000) * (mr.inputUsd * CACHE_READ_RATIO) +
|
|
100
|
+
(tokens.cacheCreate / 1_000_000) * mr.inputUsd);
|
|
101
|
+
}
|
|
102
|
+
// Fallback: the agent's typical tier. No regression for sessions without a model.
|
|
103
|
+
const outputRate = COST_PER_M_OUTPUT_TOKENS_USD[agent];
|
|
104
|
+
if (outputRate === undefined || outputRate === 0)
|
|
105
|
+
return 0;
|
|
106
|
+
const inputRate = outputRate * INPUT_RATE_RATIO;
|
|
66
107
|
return ((tokens.input / 1_000_000) * inputRate +
|
|
67
108
|
(tokens.output / 1_000_000) * outputRate +
|
|
68
|
-
(tokens.cacheRead / 1_000_000) *
|
|
109
|
+
(tokens.cacheRead / 1_000_000) * (inputRate * CACHE_READ_RATIO) +
|
|
69
110
|
(tokens.cacheCreate / 1_000_000) * inputRate);
|
|
70
111
|
}
|
|
71
112
|
/** Return the millisecond timestamp for the start of "today" in local time. */
|
|
@@ -87,6 +128,8 @@ export function computeSpendToday(now = new Date()) {
|
|
|
87
128
|
tokens: { input: 0, output: 0, cache: 0 },
|
|
88
129
|
sessionCount: 0,
|
|
89
130
|
byAgent: {},
|
|
131
|
+
byProject: {},
|
|
132
|
+
practice: { usd: 0, sessions: 0 },
|
|
90
133
|
windowStartedAt: cutoff,
|
|
91
134
|
};
|
|
92
135
|
if (!existsSync(sessionsFile()))
|
|
@@ -98,9 +141,12 @@ export function computeSpendToday(now = new Date()) {
|
|
|
98
141
|
catch {
|
|
99
142
|
return empty;
|
|
100
143
|
}
|
|
101
|
-
// Pair spawned (carries agent) with completed (carries usage + completedAt).
|
|
144
|
+
// Pair spawned (carries agent + cwd + mockRun) with completed (carries usage + completedAt).
|
|
102
145
|
const spawnedAgent = new Map();
|
|
103
|
-
const
|
|
146
|
+
const spawnedProject = new Map();
|
|
147
|
+
const spawnedMock = new Set();
|
|
148
|
+
const completedById = new Map();
|
|
149
|
+
const report = { ...empty, byAgent: {}, byProject: {}, practice: { usd: 0, sessions: 0 } };
|
|
104
150
|
for (const line of content.split('\n')) {
|
|
105
151
|
const trimmed = line.trim();
|
|
106
152
|
if (!trimmed)
|
|
@@ -114,17 +160,37 @@ export function computeSpendToday(now = new Date()) {
|
|
|
114
160
|
}
|
|
115
161
|
if (rec.t === 'spawned' && rec.id && rec.agent) {
|
|
116
162
|
spawnedAgent.set(rec.id, rec.agent);
|
|
163
|
+
if (rec.cwd)
|
|
164
|
+
spawnedProject.set(rec.id, basename(rec.cwd));
|
|
165
|
+
if (rec.mockRun === true)
|
|
166
|
+
spawnedMock.add(rec.id);
|
|
117
167
|
continue;
|
|
118
168
|
}
|
|
119
169
|
if (rec.t !== 'completed' || !rec.id)
|
|
120
170
|
continue;
|
|
121
171
|
if (typeof rec.completedAt !== 'number' || rec.completedAt < cutoff)
|
|
122
172
|
continue;
|
|
123
|
-
|
|
124
|
-
if (!agent)
|
|
173
|
+
if (!spawnedAgent.has(rec.id))
|
|
125
174
|
continue;
|
|
175
|
+
// ONE session = ONE count: a session legitimately carries MULTIPLE
|
|
176
|
+
// completed rows (the exit handler writes one, the async test-observation
|
|
177
|
+
// re-persists a corrected one — loadRecent dedups by id, later wins).
|
|
178
|
+
// Summing every row DOUBLED cost + session counts on the team record
|
|
179
|
+
// (found live 2026-07-07: 2 real runs read as 4 sessions / 2x spend).
|
|
180
|
+
// Same semantics as the reader: keep the LAST row per id.
|
|
181
|
+
completedById.set(rec.id, rec);
|
|
182
|
+
}
|
|
183
|
+
for (const [id, rec] of completedById) {
|
|
184
|
+
const agent = spawnedAgent.get(id);
|
|
126
185
|
const usage = rec.artifacts?.usage;
|
|
127
186
|
const usd = estimateCostUsd(agent, usage);
|
|
187
|
+
// Practice runs divert to their own bucket — a mock's fabricated "cost"
|
|
188
|
+
// never joins the real totals, the by-* breakdowns, or the budget's view.
|
|
189
|
+
if (spawnedMock.has(id)) {
|
|
190
|
+
report.practice.usd += usd;
|
|
191
|
+
report.practice.sessions += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
128
194
|
report.sessionCount += 1;
|
|
129
195
|
if (usage) {
|
|
130
196
|
report.tokens.input += usage.inputTokens || 0;
|
|
@@ -136,12 +202,21 @@ export function computeSpendToday(now = new Date()) {
|
|
|
136
202
|
cur.usd += usd;
|
|
137
203
|
cur.sessions += 1;
|
|
138
204
|
report.byAgent[agent] = cur;
|
|
205
|
+
const project = spawnedProject.get(id) || 'unknown';
|
|
206
|
+
const pcur = report.byProject[project] || { usd: 0, sessions: 0 };
|
|
207
|
+
pcur.usd += usd;
|
|
208
|
+
pcur.sessions += 1;
|
|
209
|
+
report.byProject[project] = pcur;
|
|
139
210
|
}
|
|
140
211
|
// Round totals to 4 decimals (1/100 of a cent) — final display rounds to 2.
|
|
141
212
|
report.totalUsd = Math.round(report.totalUsd * 10_000) / 10_000;
|
|
142
213
|
for (const k of Object.keys(report.byAgent)) {
|
|
143
214
|
report.byAgent[k].usd = Math.round(report.byAgent[k].usd * 10_000) / 10_000;
|
|
144
215
|
}
|
|
216
|
+
for (const k of Object.keys(report.byProject)) {
|
|
217
|
+
report.byProject[k].usd = Math.round(report.byProject[k].usd * 10_000) / 10_000;
|
|
218
|
+
}
|
|
219
|
+
report.practice.usd = Math.round(report.practice.usd * 10_000) / 10_000;
|
|
145
220
|
return report;
|
|
146
221
|
}
|
|
147
222
|
//# sourceMappingURL=spend-aggregator.js.map
|