@nanonets/graft 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/dist/app/brain-build.d.ts +66 -0
- package/dist/app/brain-build.d.ts.map +1 -0
- package/dist/app/brain-build.js +169 -0
- package/dist/app/brain-build.js.map +1 -0
- package/dist/app/checkout.d.ts +53 -3
- package/dist/app/checkout.d.ts.map +1 -1
- package/dist/app/checkout.js +195 -27
- package/dist/app/checkout.js.map +1 -1
- package/dist/app/events.d.ts +7 -0
- package/dist/app/events.d.ts.map +1 -1
- package/dist/app/events.js +7 -2
- package/dist/app/events.js.map +1 -1
- package/dist/app/history.d.ts +128 -0
- package/dist/app/history.d.ts.map +1 -0
- package/dist/app/history.js +269 -0
- package/dist/app/history.js.map +1 -0
- package/dist/app/identity.d.ts +33 -0
- package/dist/app/identity.d.ts.map +1 -1
- package/dist/app/identity.js +72 -0
- package/dist/app/identity.js.map +1 -1
- package/dist/app/main.js +12 -0
- package/dist/app/main.js.map +1 -1
- package/dist/app/pages.d.ts +17 -0
- package/dist/app/pages.d.ts.map +1 -1
- package/dist/app/pages.js +112 -5
- package/dist/app/pages.js.map +1 -1
- package/dist/app/review-process.d.ts +66 -0
- package/dist/app/review-process.d.ts.map +1 -0
- package/dist/app/review-process.js +199 -0
- package/dist/app/review-process.js.map +1 -0
- package/dist/app/review-worker.d.ts +2 -0
- package/dist/app/review-worker.d.ts.map +1 -0
- package/dist/app/review-worker.js +108 -0
- package/dist/app/review-worker.js.map +1 -0
- package/dist/app/review.d.ts.map +1 -1
- package/dist/app/review.js +4 -1
- package/dist/app/review.js.map +1 -1
- package/dist/app/server.d.ts +22 -2
- package/dist/app/server.d.ts.map +1 -1
- package/dist/app/server.js +77 -3
- package/dist/app/server.js.map +1 -1
- package/dist/app/sources.d.ts +68 -0
- package/dist/app/sources.d.ts.map +1 -0
- package/dist/app/sources.js +341 -0
- package/dist/app/sources.js.map +1 -0
- package/dist/ask/ask.d.ts +6 -0
- package/dist/ask/ask.d.ts.map +1 -1
- package/dist/ask/ask.js +30 -2
- package/dist/ask/ask.js.map +1 -1
- package/dist/brain/attach.d.ts +44 -0
- package/dist/brain/attach.d.ts.map +1 -0
- package/dist/brain/attach.js +63 -0
- package/dist/brain/attach.js.map +1 -0
- package/dist/brain/connect.d.ts +59 -0
- package/dist/brain/connect.d.ts.map +1 -0
- package/dist/brain/connect.js +80 -0
- package/dist/brain/connect.js.map +1 -0
- package/dist/brain/link.d.ts +82 -0
- package/dist/brain/link.d.ts.map +1 -0
- package/dist/brain/link.js +136 -0
- package/dist/brain/link.js.map +1 -0
- package/dist/brain/push.d.ts +72 -0
- package/dist/brain/push.d.ts.map +1 -0
- package/dist/brain/push.js +191 -0
- package/dist/brain/push.js.map +1 -0
- package/dist/brain/wire.d.ts +38 -0
- package/dist/brain/wire.d.ts.map +1 -0
- package/dist/brain/wire.js +133 -0
- package/dist/brain/wire.js.map +1 -0
- package/dist/claude/format.d.ts.map +1 -1
- package/dist/claude/format.js +10 -3
- package/dist/claude/format.js.map +1 -1
- package/dist/claude/hooks.d.ts.map +1 -1
- package/dist/claude/hooks.js +32 -1
- package/dist/claude/hooks.js.map +1 -1
- package/dist/claude/init.d.ts +5 -0
- package/dist/claude/init.d.ts.map +1 -1
- package/dist/claude/init.js +9 -1
- package/dist/claude/init.js.map +1 -1
- package/dist/claude/session-metrics.d.ts +10 -0
- package/dist/claude/session-metrics.d.ts.map +1 -1
- package/dist/claude/session-metrics.js +25 -0
- package/dist/claude/session-metrics.js.map +1 -1
- package/dist/claude/settings-merge.d.ts +18 -0
- package/dist/claude/settings-merge.d.ts.map +1 -1
- package/dist/claude/settings-merge.js +42 -8
- package/dist/claude/settings-merge.js.map +1 -1
- package/dist/claude/skill-template.d.ts.map +1 -1
- package/dist/claude/skill-template.js +6 -0
- package/dist/claude/skill-template.js.map +1 -1
- package/dist/claude/state.d.ts +14 -0
- package/dist/claude/state.d.ts.map +1 -1
- package/dist/claude/state.js.map +1 -1
- package/dist/claude/tally.d.ts +27 -0
- package/dist/claude/tally.d.ts.map +1 -1
- package/dist/claude/tally.js +74 -1
- package/dist/claude/tally.js.map +1 -1
- package/dist/cli.js +210 -3
- package/dist/cli.js.map +1 -1
- package/dist/context/price.d.ts +49 -0
- package/dist/context/price.d.ts.map +1 -0
- package/dist/context/price.js +83 -0
- package/dist/context/price.js.map +1 -0
- package/dist/context/savings.d.ts +13 -3
- package/dist/context/savings.d.ts.map +1 -1
- package/dist/context/savings.js +44 -5
- package/dist/context/savings.js.map +1 -1
- package/dist/hosts/claude-global.d.ts +33 -0
- package/dist/hosts/claude-global.d.ts.map +1 -0
- package/dist/hosts/claude-global.js +118 -0
- package/dist/hosts/claude-global.js.map +1 -0
- package/dist/hosts/plan.d.ts.map +1 -1
- package/dist/hosts/plan.js +5 -1
- package/dist/hosts/plan.js.map +1 -1
- package/dist/hosts/retract.d.ts.map +1 -1
- package/dist/hosts/retract.js +33 -13
- package/dist/hosts/retract.js.map +1 -1
- package/dist/hosts/sections.d.ts +23 -2
- package/dist/hosts/sections.d.ts.map +1 -1
- package/dist/hosts/sections.js +25 -9
- package/dist/hosts/sections.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +33 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +5 -1
- package/dist/mcp/tools.js.map +1 -1
- package/dist/telemetry/gate.d.ts +14 -0
- package/dist/telemetry/gate.d.ts.map +1 -1
- package/dist/telemetry/gate.js +18 -2
- package/dist/telemetry/gate.js.map +1 -1
- package/dist/upkeep-run.d.ts.map +1 -1
- package/dist/upkeep-run.js +12 -2
- package/dist/upkeep-run.js.map +1 -1
- package/dist/upkeep.d.ts +18 -0
- package/dist/upkeep.d.ts.map +1 -1
- package/dist/upkeep.js +38 -0
- package/dist/upkeep.js.map +1 -1
- package/dist/util/state.d.ts +9 -0
- package/dist/util/state.d.ts.map +1 -1
- package/dist/util/state.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The link between a repo's graft graph and a Trail brain.
|
|
3
|
+
*
|
|
4
|
+
* A brain holds the rules mined out of this repository's history — what the
|
|
5
|
+
* team decided and why, which the code graph cannot see. Graft holds the code
|
|
6
|
+
* and can say which symbols an answer touches. This module is the join: the
|
|
7
|
+
* token identifying the brain, and the rules cached beside it so `ask` never
|
|
8
|
+
* waits on the network.
|
|
9
|
+
*
|
|
10
|
+
* The link lives in the per-repo, git-ignored `.graft/config.json`, next to the
|
|
11
|
+
* other persisted build choices. Not in `~/.graft/`: a brain belongs to one
|
|
12
|
+
* repository, and two checkouts of different repos on one machine must not
|
|
13
|
+
* share one.
|
|
14
|
+
*/
|
|
15
|
+
import { readBuildConfig, patchBuildConfig, cacheDir, readJson, writeJsonAtomic } from '../util/state.js';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
/** Default API host. Overridden by GRAFT_BRAIN_URL, for staging and self-hosted. */
|
|
18
|
+
const DEFAULT_BRAIN_BASE_URL = 'https://agents.nanonets.com';
|
|
19
|
+
/** How long a cached rule set is served before upkeep refreshes it. */
|
|
20
|
+
export const RULES_TTL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
21
|
+
/**
|
|
22
|
+
* The TTL used while the cache holds NO rules.
|
|
23
|
+
*
|
|
24
|
+
* A brain is connected during onboarding while it is still being mined, so the
|
|
25
|
+
* first pull legitimately returns nothing. Six hours of that is the difference
|
|
26
|
+
* between the feature working and the user concluding it does not: they wired
|
|
27
|
+
* graft up, got an empty rulebook, and nothing would go back for the real one
|
|
28
|
+
* until tomorrow. Two minutes costs one cheap request per session until the
|
|
29
|
+
* rules land, and then the normal TTL takes over.
|
|
30
|
+
*/
|
|
31
|
+
export const EMPTY_RULES_TTL_MS = 2 * 60 * 1000;
|
|
32
|
+
/**
|
|
33
|
+
* The link for repo `dir`, or null when it has none.
|
|
34
|
+
*
|
|
35
|
+
* `GRAFT_BRAIN_TOKEN` / `GRAFT_BRAIN_ID` override the stored pair, so CI can
|
|
36
|
+
* attach a brain without writing to the checkout.
|
|
37
|
+
*/
|
|
38
|
+
export function readLink(dir) {
|
|
39
|
+
const envToken = process.env.GRAFT_BRAIN_TOKEN;
|
|
40
|
+
const envBrain = process.env.GRAFT_BRAIN_ID;
|
|
41
|
+
if (envToken && envBrain) {
|
|
42
|
+
return { brainId: envBrain, token: envToken, baseUrl: process.env.GRAFT_BRAIN_URL };
|
|
43
|
+
}
|
|
44
|
+
const stored = readBuildConfig(dir)?.brain;
|
|
45
|
+
if (!stored?.brainId || !stored?.token)
|
|
46
|
+
return null;
|
|
47
|
+
return stored;
|
|
48
|
+
}
|
|
49
|
+
/** Persist the link for repo `dir`, merging into any existing build config. */
|
|
50
|
+
export function writeLink(dir, link) {
|
|
51
|
+
patchBuildConfig(dir, { brain: link });
|
|
52
|
+
}
|
|
53
|
+
/** Forget the link for repo `dir`. Leaves the cached rules for `uninstall` to remove. */
|
|
54
|
+
export function clearLink(dir) {
|
|
55
|
+
patchBuildConfig(dir, { brain: undefined });
|
|
56
|
+
}
|
|
57
|
+
/** Where the rule cache lives: beside the graph, since it is derived data. */
|
|
58
|
+
export function rulesCachePath(dir) {
|
|
59
|
+
return join(cacheDir(dir), 'brain-rules.json');
|
|
60
|
+
}
|
|
61
|
+
export function readRulesCache(dir) {
|
|
62
|
+
return readJson(rulesCachePath(dir));
|
|
63
|
+
}
|
|
64
|
+
export function writeRulesCache(dir, cache) {
|
|
65
|
+
writeJsonAtomic(rulesCachePath(dir), cache, true);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Whether a cached set is old enough to refetch.
|
|
69
|
+
*
|
|
70
|
+
* Keyed off the last ATTEMPT, not the last success, and with a much shorter
|
|
71
|
+
* window while the cache is empty — see EMPTY_RULES_TTL_MS for why that case
|
|
72
|
+
* is the one that matters.
|
|
73
|
+
*/
|
|
74
|
+
export function cacheIsStale(cache, now = Date.now()) {
|
|
75
|
+
if (!cache)
|
|
76
|
+
return true;
|
|
77
|
+
const ttl = cache.rules.length === 0 ? EMPTY_RULES_TTL_MS : RULES_TTL_MS;
|
|
78
|
+
return now - (cache.checkedAt ?? cache.fetchedAt) > ttl;
|
|
79
|
+
}
|
|
80
|
+
/** Record that a refresh was attempted, without claiming rules arrived. */
|
|
81
|
+
export function markRulesChecked(dir, now = Date.now()) {
|
|
82
|
+
const cache = readRulesCache(dir);
|
|
83
|
+
if (!cache)
|
|
84
|
+
return;
|
|
85
|
+
writeRulesCache(dir, { ...cache, checkedAt: now });
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The base URL for a link, honouring the env override first so a misconfigured
|
|
89
|
+
* stored value can always be corrected without editing a file.
|
|
90
|
+
*/
|
|
91
|
+
export function baseUrlFor(link) {
|
|
92
|
+
return (process.env.GRAFT_BRAIN_URL || link.baseUrl || DEFAULT_BRAIN_BASE_URL).replace(/\/+$/, '');
|
|
93
|
+
}
|
|
94
|
+
/** Timeout for one rules fetch. Short: `ask` must never block on this. */
|
|
95
|
+
const FETCH_TIMEOUT_MS = 5000;
|
|
96
|
+
/**
|
|
97
|
+
* Fetch the brain's symbol-anchored rules.
|
|
98
|
+
*
|
|
99
|
+
* Returns null on any failure — an unreachable brain, a revoked token, a
|
|
100
|
+
* malformed body. Never throws: `ask` degrades to the code-only answer it gave
|
|
101
|
+
* before a brain was attached, which is the whole reason the cache exists.
|
|
102
|
+
*/
|
|
103
|
+
export async function fetchRules(link, fetchImpl = fetch) {
|
|
104
|
+
// The PUBLIC, token-authenticated route: graft runs on a laptop with no
|
|
105
|
+
// session, and the token it holds is scoped to this one brain rather than the
|
|
106
|
+
// workspace. The protected route is for the app itself.
|
|
107
|
+
const url = `${baseUrlFor(link)}/api/public/brains/${encodeURIComponent(link.brainId)}/rules/anchors`;
|
|
108
|
+
try {
|
|
109
|
+
const res = await fetchImpl(url, {
|
|
110
|
+
headers: { authorization: `Bearer ${link.token}`, accept: 'application/json' },
|
|
111
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
112
|
+
});
|
|
113
|
+
if (!res.ok)
|
|
114
|
+
return null;
|
|
115
|
+
const body = (await res.json());
|
|
116
|
+
if (!Array.isArray(body.anchors))
|
|
117
|
+
return null;
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const a of body.anchors) {
|
|
120
|
+
if (!a?.symbol || !a?.rule)
|
|
121
|
+
continue;
|
|
122
|
+
out.push({
|
|
123
|
+
ruleId: String(a.rule_id ?? ''),
|
|
124
|
+
symbol: a.symbol,
|
|
125
|
+
fingerprint: String(a.fingerprint ?? ''),
|
|
126
|
+
rule: a.rule,
|
|
127
|
+
sourceUrl: a.source_url || undefined,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=link.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link.js","sourceRoot":"","sources":["../../src/brain/link.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type RepoDigest } from "../app/history.js";
|
|
2
|
+
import type { GraphV1 } from "../graph/types.js";
|
|
3
|
+
import { type BrainLink } from "./link.js";
|
|
4
|
+
/** What a local ingest read, for the caller to print. */
|
|
5
|
+
export interface PushResult {
|
|
6
|
+
jobId: string;
|
|
7
|
+
repo: string;
|
|
8
|
+
commits: number;
|
|
9
|
+
threads: number;
|
|
10
|
+
symbols: number;
|
|
11
|
+
sources: number;
|
|
12
|
+
/** Set when discussion could not be read; the ingest still went ahead. */
|
|
13
|
+
warning?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* owner/name for this checkout, from its origin remote.
|
|
17
|
+
*
|
|
18
|
+
* Only GitHub, because the discussion reader and the forge links are
|
|
19
|
+
* GitHub-shaped; another forge would need its own reader, not a looser regex
|
|
20
|
+
* here that produces links to nowhere.
|
|
21
|
+
*/
|
|
22
|
+
export declare function repoSlugFromGit(root: string): {
|
|
23
|
+
owner: string;
|
|
24
|
+
name: string;
|
|
25
|
+
} | null;
|
|
26
|
+
/**
|
|
27
|
+
* A GitHub token from wherever the user already keeps one.
|
|
28
|
+
*
|
|
29
|
+
* `gh auth token` first, because anyone who works with private repositories on
|
|
30
|
+
* the command line has it and it needs no setup at all. The env vars are the
|
|
31
|
+
* CI path. Absent, the ingest still runs on commits alone — pull-request
|
|
32
|
+
* discussion is the richest source, not a required one.
|
|
33
|
+
*/
|
|
34
|
+
export declare function githubToken(): string | null;
|
|
35
|
+
/** What the brain is waiting for, when it is waiting for something. */
|
|
36
|
+
export interface ExpectedRepo {
|
|
37
|
+
slug: string;
|
|
38
|
+
status: string;
|
|
39
|
+
brainName: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The repository this brain expects, or null when it expects none.
|
|
43
|
+
*
|
|
44
|
+
* The website records it when the user chooses the local route, so this is how
|
|
45
|
+
* the CLI knows whether the directory it is standing in is the one they asked
|
|
46
|
+
* for. Mining the wrong repo into a brain is silent and very hard to notice
|
|
47
|
+
* afterwards — the rules simply describe someone else's codebase — so the round
|
|
48
|
+
* trip is worth it.
|
|
49
|
+
*
|
|
50
|
+
* Null on any failure, which keeps an older brain (or an unreachable one)
|
|
51
|
+
* working exactly as it did before this check existed.
|
|
52
|
+
*/
|
|
53
|
+
export declare function fetchExpectedRepo(link: BrainLink, fetchImpl?: typeof fetch): Promise<ExpectedRepo | null>;
|
|
54
|
+
/** Whether two repo slugs name the same repository. GitHub is case-insensitive. */
|
|
55
|
+
export declare function sameRepo(a: string, b: string): boolean;
|
|
56
|
+
/** Build the digest for the checkout at `root`. Reads nothing but text. */
|
|
57
|
+
export declare function buildLocalDigest(root: string, graph: GraphV1 | null, opts?: {
|
|
58
|
+
autoApprove?: boolean;
|
|
59
|
+
fetchImpl?: typeof fetch;
|
|
60
|
+
}): Promise<{
|
|
61
|
+
digest: RepoDigest;
|
|
62
|
+
warning?: string;
|
|
63
|
+
} | {
|
|
64
|
+
error: string;
|
|
65
|
+
}>;
|
|
66
|
+
/** Send a digest to the brain and return the job to poll. */
|
|
67
|
+
export declare function pushDigest(link: BrainLink, digest: RepoDigest, fetchImpl?: typeof fetch): Promise<{
|
|
68
|
+
jobId: string;
|
|
69
|
+
} | {
|
|
70
|
+
error: string;
|
|
71
|
+
}>;
|
|
72
|
+
//# sourceMappingURL=push.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../../src/brain/push.ts"],"names":[],"mappings":"AAiBA,OAAO,EAKL,KAAK,UAAU,EAChB,MAAM,mBAAmB,CAAC;AAY3B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAEvD,yDAAyD;AACzD,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAYD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAMpF;AAQD;;;;;;;GAOG;AACH,wBAAgB,WAAW,IAAI,MAAM,GAAG,IAAI,CAM3C;AAgBD,uEAAuE;AACvE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,GAAE,OAAO,KAAa,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAatH;AAED,mFAAmF;AACnF,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,2EAA2E;AAC3E,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,GAAG,IAAI,EACrB,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CAAO,GAC7D,OAAO,CAAC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAoDvE;AAED,6DAA6D;AAC7D,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,UAAU,EAClB,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAkBhD"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Building a brain from the repository already on this machine.
|
|
3
|
+
*
|
|
4
|
+
* The server path needs graft's GitHub App installed on the repo, which is an
|
|
5
|
+
* admin action on someone else's org and the first thing asked of a person who
|
|
6
|
+
* has not seen a single rule yet. For a private repository it is often simply
|
|
7
|
+
* unavailable.
|
|
8
|
+
*
|
|
9
|
+
* This is the way round that: the clone is already here, and the user is
|
|
10
|
+
* already authenticated to GitHub for their own work. So read it locally and
|
|
11
|
+
* send up the same digest the server would have built.
|
|
12
|
+
*
|
|
13
|
+
* What leaves the machine is what people wrote — commit messages, pull-request
|
|
14
|
+
* comments, the docs and config already in the tree — plus symbol ids and
|
|
15
|
+
* hashes. No file contents, no diffs, no source.
|
|
16
|
+
*/
|
|
17
|
+
import { spawnSync } from "node:child_process";
|
|
18
|
+
import { buildDigest, readCommits, readSymbols, readThreads, } from "../app/history.js";
|
|
19
|
+
import { budgetSources, readAgentInstructions, readBranchProtection, readCodeowners, readCodifiedRules, readDecisionDocs, readDeclinedIssues, readReverts, readTestNames, } from "../app/sources.js";
|
|
20
|
+
import { baseUrlFor } from "./link.js";
|
|
21
|
+
function git(root, args) {
|
|
22
|
+
const res = spawnSync("git", ["-c", "core.quotePath=false", ...args], {
|
|
23
|
+
cwd: root,
|
|
24
|
+
encoding: "utf8",
|
|
25
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
26
|
+
});
|
|
27
|
+
if (res.error || res.status !== 0 || typeof res.stdout !== "string")
|
|
28
|
+
return null;
|
|
29
|
+
return res.stdout.trim();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* owner/name for this checkout, from its origin remote.
|
|
33
|
+
*
|
|
34
|
+
* Only GitHub, because the discussion reader and the forge links are
|
|
35
|
+
* GitHub-shaped; another forge would need its own reader, not a looser regex
|
|
36
|
+
* here that produces links to nowhere.
|
|
37
|
+
*/
|
|
38
|
+
export function repoSlugFromGit(root) {
|
|
39
|
+
const url = git(root, ["remote", "get-url", "origin"]);
|
|
40
|
+
if (!url)
|
|
41
|
+
return null;
|
|
42
|
+
const m = url.match(/github\.com[:/]+([^/]+)\/(.+?)(?:\.git)?\/?$/i);
|
|
43
|
+
if (!m)
|
|
44
|
+
return null;
|
|
45
|
+
return { owner: m[1], name: m[2] };
|
|
46
|
+
}
|
|
47
|
+
/** The branch to attribute the ingest to. */
|
|
48
|
+
function currentBranch(root) {
|
|
49
|
+
const head = git(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
50
|
+
return head && head !== "HEAD" ? head : "";
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A GitHub token from wherever the user already keeps one.
|
|
54
|
+
*
|
|
55
|
+
* `gh auth token` first, because anyone who works with private repositories on
|
|
56
|
+
* the command line has it and it needs no setup at all. The env vars are the
|
|
57
|
+
* CI path. Absent, the ingest still runs on commits alone — pull-request
|
|
58
|
+
* discussion is the richest source, not a required one.
|
|
59
|
+
*/
|
|
60
|
+
export function githubToken() {
|
|
61
|
+
const env = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
62
|
+
if (env)
|
|
63
|
+
return env;
|
|
64
|
+
const res = spawnSync("gh", ["auth", "token"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
65
|
+
const out = typeof res.stdout === "string" ? res.stdout.trim() : "";
|
|
66
|
+
return res.status === 0 && out ? out : null;
|
|
67
|
+
}
|
|
68
|
+
/** Whether the repository is private, so the digest records it honestly. */
|
|
69
|
+
async function isPrivate(owner, name, token, fetchImpl) {
|
|
70
|
+
if (!token)
|
|
71
|
+
return true; // unknown counts as private
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetchImpl(`https://api.github.com/repos/${owner}/${name}`, {
|
|
74
|
+
headers: { authorization: `Bearer ${token}`, accept: "application/vnd.github+json" },
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok)
|
|
77
|
+
return true;
|
|
78
|
+
return (await res.json()).private !== false;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The repository this brain expects, or null when it expects none.
|
|
86
|
+
*
|
|
87
|
+
* The website records it when the user chooses the local route, so this is how
|
|
88
|
+
* the CLI knows whether the directory it is standing in is the one they asked
|
|
89
|
+
* for. Mining the wrong repo into a brain is silent and very hard to notice
|
|
90
|
+
* afterwards — the rules simply describe someone else's codebase — so the round
|
|
91
|
+
* trip is worth it.
|
|
92
|
+
*
|
|
93
|
+
* Null on any failure, which keeps an older brain (or an unreachable one)
|
|
94
|
+
* working exactly as it did before this check existed.
|
|
95
|
+
*/
|
|
96
|
+
export async function fetchExpectedRepo(link, fetchImpl = fetch) {
|
|
97
|
+
try {
|
|
98
|
+
const res = await fetchImpl(`${baseUrlFor(link)}/api/public/brains/${encodeURIComponent(link.brainId)}/repo`, {
|
|
99
|
+
headers: { authorization: `Bearer ${link.token}`, accept: "application/json" },
|
|
100
|
+
signal: AbortSignal.timeout(5000),
|
|
101
|
+
});
|
|
102
|
+
if (!res.ok)
|
|
103
|
+
return null;
|
|
104
|
+
const body = (await res.json());
|
|
105
|
+
if (!body.repo?.slug)
|
|
106
|
+
return null;
|
|
107
|
+
return { slug: body.repo.slug, status: String(body.repo.status ?? ""), brainName: String(body.brain_name ?? "") };
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Whether two repo slugs name the same repository. GitHub is case-insensitive. */
|
|
114
|
+
export function sameRepo(a, b) {
|
|
115
|
+
return a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
116
|
+
}
|
|
117
|
+
/** Build the digest for the checkout at `root`. Reads nothing but text. */
|
|
118
|
+
export async function buildLocalDigest(root, graph, opts = {}) {
|
|
119
|
+
const slug = repoSlugFromGit(root);
|
|
120
|
+
if (!slug) {
|
|
121
|
+
return { error: "this directory has no GitHub `origin` remote — graft can only push a GitHub repository today" };
|
|
122
|
+
}
|
|
123
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
124
|
+
const token = githubToken();
|
|
125
|
+
const commits = readCommits(root);
|
|
126
|
+
if (commits.length === 0) {
|
|
127
|
+
return { error: "no commits found here — is this a shallow clone with no history?" };
|
|
128
|
+
}
|
|
129
|
+
const symbols = readSymbols(graph);
|
|
130
|
+
let threads = [];
|
|
131
|
+
let warning;
|
|
132
|
+
if (token) {
|
|
133
|
+
try {
|
|
134
|
+
threads = await readThreads(slug.owner, slug.name, token, fetchImpl);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
warning = "could not read pull-request discussion; mining commits and repo files only";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
warning =
|
|
142
|
+
"no GitHub token found (`gh auth login`, or GH_TOKEN) — mining commits and repo files only, without pull-request discussion";
|
|
143
|
+
}
|
|
144
|
+
const branch = currentBranch(root);
|
|
145
|
+
const sources = budgetSources([
|
|
146
|
+
...readAgentInstructions(root),
|
|
147
|
+
...readDecisionDocs(root),
|
|
148
|
+
...readCodeowners(root),
|
|
149
|
+
...readCodifiedRules(root),
|
|
150
|
+
...readReverts(root),
|
|
151
|
+
...readTestNames(symbols.map((s) => ({ name: s.name, path: s.path }))),
|
|
152
|
+
...(token ? await readBranchProtection(slug.owner, slug.name, branch, token, fetchImpl) : []),
|
|
153
|
+
...(token ? await readDeclinedIssues(slug.owner, slug.name, token, fetchImpl) : []),
|
|
154
|
+
]);
|
|
155
|
+
const digest = buildDigest({
|
|
156
|
+
owner: slug.owner,
|
|
157
|
+
name: slug.name,
|
|
158
|
+
headSha: git(root, ["rev-parse", "HEAD"]) ?? "",
|
|
159
|
+
defaultBranch: branch,
|
|
160
|
+
isPrivate: await isPrivate(slug.owner, slug.name, token, fetchImpl),
|
|
161
|
+
commits,
|
|
162
|
+
threads,
|
|
163
|
+
symbols,
|
|
164
|
+
sources,
|
|
165
|
+
autoApprove: opts.autoApprove ?? true,
|
|
166
|
+
});
|
|
167
|
+
return { digest, warning };
|
|
168
|
+
}
|
|
169
|
+
/** Send a digest to the brain and return the job to poll. */
|
|
170
|
+
export async function pushDigest(link, digest, fetchImpl = fetch) {
|
|
171
|
+
const url = `${baseUrlFor(link)}/api/public/brains/${encodeURIComponent(link.brainId)}/repo`;
|
|
172
|
+
try {
|
|
173
|
+
const res = await fetchImpl(url, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: {
|
|
176
|
+
authorization: `Bearer ${link.token}`,
|
|
177
|
+
"content-type": "application/json",
|
|
178
|
+
accept: "application/json",
|
|
179
|
+
},
|
|
180
|
+
body: JSON.stringify(digest),
|
|
181
|
+
});
|
|
182
|
+
const text = await res.text();
|
|
183
|
+
if (!res.ok)
|
|
184
|
+
return { error: `the brain refused the ingest: ${res.status} ${text.slice(0, 200)}` };
|
|
185
|
+
return { jobId: String(JSON.parse(text).job_id ?? "") };
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
return { error: `could not reach the brain: ${e instanceof Error ? e.message : e}` };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=push.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push.js","sourceRoot":"","sources":["../../src/brain/push.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type HostTarget } from '../hosts/registry.js';
|
|
2
|
+
import { type UpsertAction } from '../hosts/sections.js';
|
|
3
|
+
import type { BrainRule } from './link.js';
|
|
4
|
+
/** What a rules write did to one host's file. */
|
|
5
|
+
export interface BrainWrite {
|
|
6
|
+
id: string;
|
|
7
|
+
path: string;
|
|
8
|
+
action: UpsertAction;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Render the rules block for an instruction file.
|
|
12
|
+
*
|
|
13
|
+
* Grouped by the file the rule governs, because that is how someone reads a
|
|
14
|
+
* codebase, and repo-wide rules (no symbol) come first — they apply everywhere,
|
|
15
|
+
* so they should not be buried under a path heading.
|
|
16
|
+
*/
|
|
17
|
+
export declare function renderBrainSection(rules: BrainRule[], repoLabel: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Every instruction file a brain section should go in.
|
|
20
|
+
*
|
|
21
|
+
* `section`-kind hosts only. An `owned`-kind host (Cursor's `.mdc`, Kiro's
|
|
22
|
+
* steering file, a `SKILL.md`) is a file graft rewrites whole from a pure
|
|
23
|
+
* template, so a second managed region inside one would be erased by the next
|
|
24
|
+
* `init` — those agents get their rules through `graft ask` instead, which they
|
|
25
|
+
* all call.
|
|
26
|
+
*
|
|
27
|
+
* Deduplicated by path: three hosts target `AGENTS.md`, and writing it three
|
|
28
|
+
* times in one pass is just three identical upserts.
|
|
29
|
+
*/
|
|
30
|
+
export declare function brainSectionTargets(repo: string, home: string, ids?: string[]): HostTarget[];
|
|
31
|
+
/**
|
|
32
|
+
* Write the brain's rules into each selected host's instruction file.
|
|
33
|
+
*
|
|
34
|
+
* Best-effort per file: one unwritable path must not stop the rest, since a
|
|
35
|
+
* partially delivered rulebook is strictly better than none.
|
|
36
|
+
*/
|
|
37
|
+
export declare function writeBrainSections(repo: string, home: string, rules: BrainRule[], repoLabel: string, ids?: string[]): BrainWrite[];
|
|
38
|
+
//# sourceMappingURL=wire.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/brain/wire.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAwC,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAC7F,OAAO,EAAgC,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEvF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAK3C,iDAAiD;AACjD,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,CAAC;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAqChF;AAiBD;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,GAAG,UAAU,EAAE,CAa5F;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,EAAE,EAClB,SAAS,EAAE,MAAM,EACjB,GAAG,CAAC,EAAE,MAAM,EAAE,GACb,UAAU,EAAE,CAed"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing a brain's rules into the instruction file each coding agent already
|
|
3
|
+
* reads.
|
|
4
|
+
*
|
|
5
|
+
* Two delivery paths exist and both matter. Every `graft ask` carries the rules
|
|
6
|
+
* for the symbols in its answer — that is the precise one. This is the broad
|
|
7
|
+
* one: the repo-wide conventions, sitting in `AGENTS.md` / `CLAUDE.md` /
|
|
8
|
+
* `.cursor/rules` where the agent picks them up whether or not it ever calls
|
|
9
|
+
* graft.
|
|
10
|
+
*
|
|
11
|
+
* The rules land in their OWN fenced block (`BRAIN_MARKERS`), never inside the
|
|
12
|
+
* instruction block: instructions are static and rewritten by `init`, rules
|
|
13
|
+
* change whenever the brain does, and `uninstall` has to be able to remove
|
|
14
|
+
* either one.
|
|
15
|
+
*/
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { HOSTS, detectHosts } from '../hosts/registry.js';
|
|
18
|
+
import { upsertSection, BRAIN_MARKERS } from '../hosts/sections.js';
|
|
19
|
+
import { statSync } from 'node:fs';
|
|
20
|
+
/** How many rules go into an instruction file. */
|
|
21
|
+
const MAX_FILE_RULES = 40;
|
|
22
|
+
/**
|
|
23
|
+
* Render the rules block for an instruction file.
|
|
24
|
+
*
|
|
25
|
+
* Grouped by the file the rule governs, because that is how someone reads a
|
|
26
|
+
* codebase, and repo-wide rules (no symbol) come first — they apply everywhere,
|
|
27
|
+
* so they should not be buried under a path heading.
|
|
28
|
+
*/
|
|
29
|
+
export function renderBrainSection(rules, repoLabel) {
|
|
30
|
+
const capped = rules.slice(0, MAX_FILE_RULES);
|
|
31
|
+
const lines = [
|
|
32
|
+
'## House rules for this codebase',
|
|
33
|
+
'',
|
|
34
|
+
`Mined from ${repoLabel}'s own history — commit messages and pull-request`,
|
|
35
|
+
'discussion — by Trail. These are decisions the team has already made, so',
|
|
36
|
+
'follow them and do not re-litigate them in passing. Each is attributed to',
|
|
37
|
+
'what established it.',
|
|
38
|
+
'',
|
|
39
|
+
];
|
|
40
|
+
const byFile = new Map();
|
|
41
|
+
for (const r of capped) {
|
|
42
|
+
// A node id is `<path>#<QualifiedName>`; everything before the '#' is the file.
|
|
43
|
+
const file = r.symbol.includes('#') ? r.symbol.slice(0, r.symbol.indexOf('#')) : '';
|
|
44
|
+
const bucket = byFile.get(file);
|
|
45
|
+
if (bucket)
|
|
46
|
+
bucket.push(r);
|
|
47
|
+
else
|
|
48
|
+
byFile.set(file, [r]);
|
|
49
|
+
}
|
|
50
|
+
const general = byFile.get('') ?? [];
|
|
51
|
+
for (const r of general)
|
|
52
|
+
lines.push(`- ${r.rule}${r.sourceUrl ? ` (${r.sourceUrl})` : ''}`);
|
|
53
|
+
if (general.length)
|
|
54
|
+
lines.push('');
|
|
55
|
+
for (const [file, group] of [...byFile].filter(([f]) => f !== '').sort()) {
|
|
56
|
+
lines.push(`### ${file}`);
|
|
57
|
+
for (const r of group)
|
|
58
|
+
lines.push(`- ${r.rule}${r.sourceUrl ? ` (${r.sourceUrl})` : ''}`);
|
|
59
|
+
lines.push('');
|
|
60
|
+
}
|
|
61
|
+
if (rules.length > capped.length) {
|
|
62
|
+
lines.push(`_${rules.length - capped.length} more rules apply to specific symbols; graft attaches those to each \`graft ask\` answer._`);
|
|
63
|
+
}
|
|
64
|
+
return lines.join('\n').trimEnd();
|
|
65
|
+
}
|
|
66
|
+
/** Build the probe `detectHosts` needs. Mirrors hosts/init.ts's own. */
|
|
67
|
+
function probeFor(repo, home) {
|
|
68
|
+
return {
|
|
69
|
+
home,
|
|
70
|
+
repo,
|
|
71
|
+
dirExists: (p) => {
|
|
72
|
+
try {
|
|
73
|
+
return statSync(p).isDirectory();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Every instruction file a brain section should go in.
|
|
83
|
+
*
|
|
84
|
+
* `section`-kind hosts only. An `owned`-kind host (Cursor's `.mdc`, Kiro's
|
|
85
|
+
* steering file, a `SKILL.md`) is a file graft rewrites whole from a pure
|
|
86
|
+
* template, so a second managed region inside one would be erased by the next
|
|
87
|
+
* `init` — those agents get their rules through `graft ask` instead, which they
|
|
88
|
+
* all call.
|
|
89
|
+
*
|
|
90
|
+
* Deduplicated by path: three hosts target `AGENTS.md`, and writing it three
|
|
91
|
+
* times in one pass is just three identical upserts.
|
|
92
|
+
*/
|
|
93
|
+
export function brainSectionTargets(repo, home, ids) {
|
|
94
|
+
const selected = ids?.length
|
|
95
|
+
? HOSTS.filter((h) => ids.includes(h.id))
|
|
96
|
+
: detectHosts(probeFor(repo, home));
|
|
97
|
+
const seen = new Set();
|
|
98
|
+
const out = [];
|
|
99
|
+
for (const h of selected) {
|
|
100
|
+
if (h.kind !== 'section')
|
|
101
|
+
continue;
|
|
102
|
+
if (seen.has(h.relPath))
|
|
103
|
+
continue;
|
|
104
|
+
seen.add(h.relPath);
|
|
105
|
+
out.push(h);
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Write the brain's rules into each selected host's instruction file.
|
|
111
|
+
*
|
|
112
|
+
* Best-effort per file: one unwritable path must not stop the rest, since a
|
|
113
|
+
* partially delivered rulebook is strictly better than none.
|
|
114
|
+
*/
|
|
115
|
+
export function writeBrainSections(repo, home, rules, repoLabel, ids) {
|
|
116
|
+
if (!rules.length)
|
|
117
|
+
return [];
|
|
118
|
+
const body = renderBrainSection(rules, repoLabel);
|
|
119
|
+
const out = [];
|
|
120
|
+
for (const host of brainSectionTargets(repo, home, ids)) {
|
|
121
|
+
const path = join(repo, host.relPath);
|
|
122
|
+
try {
|
|
123
|
+
const { action } = upsertSection(path, body, BRAIN_MARKERS);
|
|
124
|
+
out.push({ id: host.id, path, action });
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Unwritable file (read-only checkout, a directory in the way). Skipped
|
|
128
|
+
// silently here; the caller reports what it did get.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=wire.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire.js","sourceRoot":"","sources":["../../src/brain/wire.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/claude/format.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/claude/format.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAezD,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,KAAK,GAAG,MAAM,CAKjD;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,KAAK,GAAG,IAAI,EACnB,OAAO,EAAE,YAAY,GAAG,IAAI,EAC5B,GAAG,EAAE;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC7B,MAAM,EAAE,CAuBV;AAUD,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAIpE;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAI,GAAG,MAAM,GAAG,IAAI,CAWtF;AAED,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAC5B,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACxG,kFAAkF;IAClF,KAAK,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;IACjD,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;yEAEqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AA0BD,iFAAiF;AACjF,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,SAAI,GAAG,MAAM,CAMlE;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,SAAI,GAAG,MAAM,GAAG,IAAI,CAapE;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,OAAO,CAAC;AAKxC;;gBAEgB;AAChB,eAAO,MAAM,SAAS,IAAI,CAAC;AAE3B;;4DAE4D;AAC5D,wBAAgB,cAAc,CAAC,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQ7E;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,EAAE,YAAY,EAAE,GAAG,SAAI,GAAG,MAAM,GAAG,IAAI,CAgBvF;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,SAAO,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAoBjG;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI,GAAG,MAAM,CAItF"}
|
package/dist/claude/format.js
CHANGED
|
@@ -3,6 +3,7 @@ import { basename } from 'node:path';
|
|
|
3
3
|
// "did we hit a real name, or match broadly enough to trust anyway" is the same
|
|
4
4
|
// question in both places, and one set of calibrated numbers beats two.
|
|
5
5
|
import { HIGH_FLOOR, STRONG_FLOOR } from '../ask/fuse.js';
|
|
6
|
+
import { dollarsSaved, formatDollars } from '../context/price.js';
|
|
6
7
|
const C = {
|
|
7
8
|
indigo: (s) => `\x1b[38;2;84;111;255m${s}\x1b[0m`,
|
|
8
9
|
amber: (s) => `\x1b[38;2;224;165;68m${s}\x1b[0m`,
|
|
@@ -26,8 +27,14 @@ export function renderStatusline(stats, session, ctx) {
|
|
|
26
27
|
const top = [C.muted('◤ ') + C.indigo('graft'), C.text(`${stats.nodeCount} nodes / ${stats.edgeCount} edges`)];
|
|
27
28
|
top.push(freshnessSegment(stats));
|
|
28
29
|
const saved = session?.savedTokens ?? 0;
|
|
29
|
-
if (saved > 0)
|
|
30
|
-
|
|
30
|
+
if (saved > 0) {
|
|
31
|
+
// Dollars only once a turn has actually been billed — see context/price.ts.
|
|
32
|
+
// Until then (turn one, or a host with no transcript) the token count
|
|
33
|
+
// stands alone rather than carrying a rate nobody measured.
|
|
34
|
+
const usd = dollarsSaved(saved, session?.inputCostMicros, session?.inputTokensBilled);
|
|
35
|
+
const money = usd === null ? '' : ` · ~${formatDollars(usd)}`;
|
|
36
|
+
top.push(C.indigo(`~${saved.toLocaleString()} tok saved${money}`));
|
|
37
|
+
}
|
|
31
38
|
const bottom = [];
|
|
32
39
|
if (typeof ctx.ctxPct === 'number')
|
|
33
40
|
bottom.push(C.text(`ctx ${ctx.ctxPct}%`));
|
|
@@ -192,7 +199,7 @@ export function formatOrientation(indexMd, budgetBytes = 1500, staleNote) {
|
|
|
192
199
|
` In a monorepo, add --in <path>/ to ask/grep/callers to scope to one sub-project; hits are labeled [scope/].\n` +
|
|
193
200
|
` Already know the file or symbol to change? Go straight to it: graft grep "<symbol>", read the span, edit. Save ask for when you don't yet know where the code lives.\n` +
|
|
194
201
|
` Refactor, rename, or multi-file change? Run graft callers <sym> --depth all FIRST to map every connected file; editing the primary file and stopping is the classic miss (platform siblings, a new file to extract).\n` +
|
|
195
|
-
`Each tool opens its output with a "[graft] tokens saved ≈ N" line; when you used graft this turn, close your reply with a one-line tally of the total saved (e.g. 🌱 graft saved ~12k tokens this turn, 3 calls). Never pipe
|
|
202
|
+
`Each tool opens its output with a "[graft] tokens saved ≈ N" line, sometimes with its dollar value; when you used graft this turn, close your reply with a one-line tally of the total saved, dollars included when given (e.g. 🌱 graft saved ~12k tokens (~$0.04) this turn, 3 calls). Never price tokens yourself; never pipe graft through head/tail — it is already capped, and clipping drops that line.\n`;
|
|
196
203
|
const banner = staleNote ? `${staleNote}\n\n` : "";
|
|
197
204
|
return `${banner}${directive}\nrepo map (graft/INDEX.md):\n${indexMd.slice(0, budgetBytes)}`;
|
|
198
205
|
}
|