@mnemom/mnemom 0.17.31 → 0.17.32-next.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/dist/base-url-proxy.mjs +30 -0
- package/dist/commands/agent-fuel.d.ts +75 -0
- package/dist/commands/agent-fuel.js +298 -0
- package/dist/commands/agent-implicit-notice.d.ts +31 -0
- package/dist/commands/agent-implicit-notice.js +73 -0
- package/dist/commands/agent-session.d.ts +2 -1
- package/dist/commands/agent-session.js +2 -1
- package/dist/commands/agent-share-fixture.d.ts +20 -0
- package/dist/commands/agent-share-fixture.js +134 -0
- package/dist/commands/agent-share.d.ts +87 -0
- package/dist/commands/agent-share.js +251 -0
- package/dist/commands/agent.d.ts +75 -2
- package/dist/commands/agent.js +165 -28
- package/dist/commands/status.js +1 -1
- package/dist/commands/update.js +3 -1
- package/dist/commands/validate.d.ts +1 -1
- package/dist/commands/validate.js +1 -1
- package/dist/index.js +68 -12
- package/dist/lib/agent.d.ts +34 -19
- package/dist/lib/agent.js +41 -32
- package/dist/lib/api.d.ts +66 -0
- package/dist/lib/api.js +50 -10
- package/dist/lib/canonical-json.d.ts +12 -0
- package/dist/lib/canonical-json.js +22 -0
- package/dist/lib/commit-report.d.ts +109 -0
- package/dist/lib/commit-report.js +205 -0
- package/dist/lib/conduct-receipt.d.ts +81 -0
- package/dist/lib/conduct-receipt.js +177 -0
- package/dist/lib/contract-hash.d.ts +61 -0
- package/dist/lib/contract-hash.js +126 -0
- package/dist/lib/experimental.d.ts +10 -1
- package/dist/lib/experimental.js +35 -1
- package/dist/lib/oauth.d.ts +2 -1
- package/dist/lib/oauth.js +2 -1
- package/dist/lib/statusline-bin.d.ts +2 -2
- package/dist/lib/statusline-bin.js +4 -7
- package/dist/lib/statusline-feed.d.ts +33 -0
- package/dist/lib/statusline-feed.js +68 -0
- package/dist/lib/statusline.d.ts +39 -88
- package/dist/lib/statusline.js +58 -250
- package/dist/lib/update/decide.d.ts +22 -3
- package/dist/lib/update/decide.js +35 -12
- package/dist/lib/update/doctor.js +5 -1
- package/dist/lib/update/run.d.ts +2 -0
- package/dist/lib/update/run.js +22 -8
- package/package.json +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The launcher side of the fed status line (W1.4, decision 14), prerelease only.
|
|
3
|
+
*
|
|
4
|
+
* The local base-URL proxy records the gateway's `x-mnemom-lineage-key` for this session in
|
|
5
|
+
* its record-state file (the one the conduct receipt reads). While Claude Code runs, the launcher reads Record's accumulator totals for that
|
|
6
|
+
* lineage every few seconds and writes them to the feed file the status-line bin reads
|
|
7
|
+
* (lib/statusline.ts). The bin never touches the network.
|
|
8
|
+
*
|
|
9
|
+
* Fail open everywhere: no lineage yet, a dark route (404), a non-billing role (403) or any
|
|
10
|
+
* error leaves the feed as it was, so the status line stays "mnemom" until fed.
|
|
11
|
+
*/
|
|
12
|
+
import { renameSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { getRecordTotals } from "./api.js";
|
|
14
|
+
import { readRecordState } from "./conduct-receipt.js";
|
|
15
|
+
import { LINEAGE_KEY_RE, MMU_RE } from "./statusline.js";
|
|
16
|
+
/** How often the launcher re-reads the totals. */
|
|
17
|
+
export const FEED_INTERVAL_MS = 10_000;
|
|
18
|
+
/**
|
|
19
|
+
* The lineage key the proxy recorded, or null when there is none (yet) or it is not the
|
|
20
|
+
* gateway's `cid:`/`cr:` shape (the proxy's own check is looser; this read is exact).
|
|
21
|
+
*/
|
|
22
|
+
export function readLineage(path) {
|
|
23
|
+
const key = readRecordState(path).lineageKey;
|
|
24
|
+
return key && LINEAGE_KEY_RE.test(key) ? key : null;
|
|
25
|
+
}
|
|
26
|
+
/** One poll: read the lineage, fetch its totals, write the feed. Never throws. */
|
|
27
|
+
export async function feedOnce(o) {
|
|
28
|
+
const key = readLineage(o.recordStateFile);
|
|
29
|
+
if (!key)
|
|
30
|
+
return false;
|
|
31
|
+
try {
|
|
32
|
+
const totals = await (o.fetchTotals ?? getRecordTotals)(o.orgId, key);
|
|
33
|
+
if (!totals || totals.lineage_key !== key)
|
|
34
|
+
return false;
|
|
35
|
+
// Persist only amounts the bin would render: a malformed reply never replaces a good feed.
|
|
36
|
+
const mu = totals.mu_charged_mmu;
|
|
37
|
+
if (typeof totals.model_cost_mmu !== "string" || !MMU_RE.test(totals.model_cost_mmu)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (mu !== null && (typeof mu !== "string" || !MMU_RE.test(mu)))
|
|
41
|
+
return false;
|
|
42
|
+
const tmp = `${o.feedFile}.tmp`;
|
|
43
|
+
writeFileSync(tmp, JSON.stringify({
|
|
44
|
+
model_cost_mmu: totals.model_cost_mmu,
|
|
45
|
+
mu_charged_mmu: totals.mu_charged_mmu,
|
|
46
|
+
}));
|
|
47
|
+
renameSync(tmp, o.feedFile);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Poll until stopped. The timer is unref'd, so it never holds the launcher open. */
|
|
55
|
+
export function startFeed(o) {
|
|
56
|
+
let running = false;
|
|
57
|
+
const tick = () => {
|
|
58
|
+
if (running)
|
|
59
|
+
return;
|
|
60
|
+
running = true;
|
|
61
|
+
void feedOnce(o).finally(() => {
|
|
62
|
+
running = false;
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
const timer = setInterval(tick, o.intervalMs ?? FEED_INTERVAL_MS);
|
|
66
|
+
timer.unref();
|
|
67
|
+
return () => clearInterval(timer);
|
|
68
|
+
}
|
package/dist/lib/statusline.d.ts
CHANGED
|
@@ -1,108 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `mnemom agent` status-bar
|
|
3
|
-
*
|
|
2
|
+
* The `mnemom agent` status-bar segment: a Claude Code statusLine command that prints
|
|
3
|
+
* "mnemom", after the user's own status line when they have one.
|
|
4
4
|
*
|
|
5
|
-
* The
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* first-run screen always agree, and it rounds DOWN to the cent.
|
|
5
|
+
* The segment never says "saved" and never shows an estimate (decision 14). The old
|
|
6
|
+
* "mnemom saved ~$X this session" counter priced the transcript with the first-run look's
|
|
7
|
+
* benchmark estimate; it is gone on every channel, stable included.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* Measured mode (the `measured-savings` experimental feature; the launch sets
|
|
17
|
-
* MNEMOM_STATUSLINE_MEASURED=1): the gateway measures each request's saving — the stock
|
|
18
|
-
* request's input cost minus what it billed — and writes it into the response usage as
|
|
19
|
-
* `mnemom_savings` inside a `usage.iterations` entry, the one place Claude Code keeps an
|
|
20
|
-
* unknown usage key in its transcript. The counter sums those per message id
|
|
21
|
-
* ("mnemom saved $X this session", no "~": it is measured) and makes no claim before the
|
|
22
|
-
* first one arrives. Its state lives in its own file (<session>.measured.json), so the two
|
|
23
|
-
* modes never share a count.
|
|
9
|
+
* On a prerelease (`next`) launch the segment is fed: the launcher writes this session's
|
|
10
|
+
* measured model cost and μ from Record's accumulator to the file named in
|
|
11
|
+
* MNEMOM_STATUSLINE_FEED (lib/statusline-feed.ts), and the segment reads
|
|
12
|
+
* "mnemom · $0.42 · 1.70 μ". Until that file holds valid totals it is only "mnemom".
|
|
13
|
+
* Stable launches set no feed, so stable is only "mnemom".
|
|
24
14
|
*
|
|
25
15
|
* A user's own statusLine is never replaced: the launch passes it in
|
|
26
16
|
* MNEMOM_STATUSLINE_INNER, it runs with the same stdin, and our segment is appended.
|
|
27
|
-
*
|
|
28
|
-
* the user's own statusLine command, and the first-run code is held to spawning nothing.
|
|
17
|
+
* No transcript reads and no network: the only file read is the feed.
|
|
29
18
|
*/
|
|
30
|
-
export declare const STATE_DIR = "statusline";
|
|
31
|
-
export declare const MAX_BYTES_PER_TICK: number;
|
|
32
19
|
/** The launcher passes the user's own statusLine command here. */
|
|
33
20
|
export declare const INNER_ENV = "MNEMOM_STATUSLINE_INNER";
|
|
34
|
-
/** Set to 1 to launch without the
|
|
21
|
+
/** Set to 1 to launch without the segment. */
|
|
35
22
|
export declare const NO_STATUSLINE_ENV = "MNEMOM_AGENT_NO_STATUSLINE";
|
|
36
|
-
/** The launcher sets this to 1 when the gateway measures the session's saving. */
|
|
37
|
-
export declare const MEASURED_ENV = "MNEMOM_STATUSLINE_MEASURED";
|
|
38
23
|
export declare const INNER_TIMEOUT_MS = 2000;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
/** Per message id: [list-price cost, saving]. A re-written id replaces its old share. */
|
|
44
|
-
ids: Record<string, [number, number]>;
|
|
45
|
-
actual: number;
|
|
46
|
-
saved: number;
|
|
47
|
-
}
|
|
48
|
-
export declare function freshState(): SessionState;
|
|
49
|
-
/** Measured mode's state (<session>.measured.json). */
|
|
50
|
-
export interface MeasuredState {
|
|
51
|
-
v: 1;
|
|
52
|
-
files: Record<string, number>;
|
|
53
|
-
/** Per message id: the gateway's measured saving (USD). A re-written id replaces its old share. */
|
|
54
|
-
ids: Record<string, number>;
|
|
55
|
-
saved: number;
|
|
56
|
-
/** Has any record carried a measurement yet? Until then the segment makes no claim. */
|
|
57
|
-
seen: boolean;
|
|
58
|
-
}
|
|
59
|
-
export declare function freshMeasuredState(): MeasuredState;
|
|
60
|
-
/** The main transcript, then its subagent transcripts (sorted). */
|
|
61
|
-
export declare function sessionFiles(transcriptPath: string): string[];
|
|
62
|
-
/**
|
|
63
|
-
* The complete lines of `path` from `offset`, at most `maxBytes` of them, and the offset
|
|
64
|
-
* after the last complete line. null: the file is unreadable or shorter than `offset`.
|
|
65
|
-
*/
|
|
66
|
-
export declare function readNewLines(path: string, offset: number, maxBytes: number): {
|
|
67
|
-
lines: Buffer[];
|
|
68
|
-
next: number;
|
|
69
|
-
} | null;
|
|
70
|
-
/** Read what is new in the session's files into `st`. A file that shrank resets the count. */
|
|
71
|
-
export declare function update(st: SessionState, files: string[], maxBytes?: number): SessionState;
|
|
72
|
-
/** A `usage.iterations` entry's measured saving (`mnemom_savings` v1), or null. */
|
|
73
|
-
export declare function measuredSaving(entry: unknown): number | null;
|
|
24
|
+
/** What the segment says until it is fed. */
|
|
25
|
+
export declare const SEGMENT = "mnemom";
|
|
26
|
+
/** The launcher names the feed file here (prerelease launches only). */
|
|
27
|
+
export declare const FEED_ENV = "MNEMOM_STATUSLINE_FEED";
|
|
74
28
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* id (or record uuid); null when the record carries neither. Claude Code writes one record per
|
|
78
|
-
* content block, all with the same id and usage, so the id is what dedupes them.
|
|
29
|
+
* The gateway's per-conversation lineage key (deriveLineageKey): `cid:` or `cr:` + 16 hex.
|
|
30
|
+
* base-url-proxy.mjs (a standalone script) keeps its own copy; a test pins the two equal.
|
|
79
31
|
*/
|
|
80
|
-
export declare
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
export declare function
|
|
91
|
-
|
|
92
|
-
export declare function
|
|
32
|
+
export declare const LINEAGE_KEY_RE: RegExp;
|
|
33
|
+
/** A millimu amount as the record route returns it: a decimal string, no sign, no leading 0. */
|
|
34
|
+
export declare const MMU_RE: RegExp;
|
|
35
|
+
export interface FedTotals {
|
|
36
|
+
/** Integer millimu (1 μ = 1¢ = 1000 mmu), decimal string. */
|
|
37
|
+
model_cost_mmu: string;
|
|
38
|
+
/** null: no billing row yet, shown "μ not charged" (OD-9). */
|
|
39
|
+
mu_charged_mmu: string | null;
|
|
40
|
+
}
|
|
41
|
+
/** The feed file's totals, or null when it is absent or anything in it is off. */
|
|
42
|
+
export declare function readFeed(path: string): FedTotals | null;
|
|
43
|
+
/** Model cost in dollars: 1000 mmu = 1 μ = 1¢. Under half a cent reads "<$0.01", never "$0.00". */
|
|
44
|
+
export declare function formatCost(mmu: string): string;
|
|
45
|
+
/** μ charged, two places; null (no billing row) reads "μ not charged" (OD-9). */
|
|
46
|
+
export declare function formatMu(mmu: string | null): string;
|
|
47
|
+
/** The segment for these totals: "mnemom · $0.42 · 1.70 μ". */
|
|
48
|
+
export declare function fedSegment(t: FedTotals): string;
|
|
93
49
|
/** Run the user's own statusLine command with the same stdin; its first line, or "". */
|
|
94
50
|
export declare function runInner(command: string, input: string): string;
|
|
95
51
|
export interface RenderDeps {
|
|
96
|
-
stateDir: string;
|
|
97
52
|
inner?: string;
|
|
98
|
-
/** Measured mode (`measuredMode`): the gateway's measured saving, not the estimate. */
|
|
99
|
-
measured?: boolean;
|
|
100
53
|
runInner?: (command: string, input: string) => string;
|
|
54
|
+
/** The feed file (FEED_ENV); absent on stable, so the segment is only "mnemom". */
|
|
55
|
+
feed?: string;
|
|
101
56
|
}
|
|
102
|
-
/** Is the counter in measured mode? The launch's MEASURED_ENV, exactly "1". */
|
|
103
|
-
export declare function measuredMode(env: NodeJS.ProcessEnv): boolean;
|
|
104
57
|
/** Claude Code's status JSON → the one status line. Never throws. */
|
|
105
|
-
export declare function render(stdin: string, deps
|
|
58
|
+
export declare function render(stdin: string, deps?: RenderDeps): string;
|
|
106
59
|
export interface UserStatusLine {
|
|
107
60
|
command: string;
|
|
108
61
|
padding?: number;
|
|
@@ -113,9 +66,8 @@ export interface UserStatusLine {
|
|
|
113
66
|
*/
|
|
114
67
|
export declare function userStatusLine(cwd: string, configDir: string): UserStatusLine | null;
|
|
115
68
|
/**
|
|
116
|
-
* Launch args + env that add the
|
|
69
|
+
* Launch args + env that add the segment, or null when it should stay off: the opt-out
|
|
117
70
|
* env, or the user passing their own --settings (two would fight over precedence).
|
|
118
|
-
* `measured` puts the counter in measured mode (MEASURED_ENV).
|
|
119
71
|
*/
|
|
120
72
|
export declare function statuslineLaunch(o: {
|
|
121
73
|
passthrough: readonly string[];
|
|
@@ -124,7 +76,6 @@ export declare function statuslineLaunch(o: {
|
|
|
124
76
|
home?: string;
|
|
125
77
|
node: string;
|
|
126
78
|
bin: string;
|
|
127
|
-
measured?: boolean;
|
|
128
79
|
}): {
|
|
129
80
|
args: string[];
|
|
130
81
|
env: Record<string, string>;
|
package/dist/lib/statusline.js
CHANGED
|
@@ -1,240 +1,82 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `mnemom agent` status-bar
|
|
3
|
-
*
|
|
2
|
+
* The `mnemom agent` status-bar segment: a Claude Code statusLine command that prints
|
|
3
|
+
* "mnemom", after the user's own status line when they have one.
|
|
4
4
|
*
|
|
5
|
-
* The
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* first-run screen always agree, and it rounds DOWN to the cent.
|
|
5
|
+
* The segment never says "saved" and never shows an estimate (decision 14). The old
|
|
6
|
+
* "mnemom saved ~$X this session" counter priced the transcript with the first-run look's
|
|
7
|
+
* benchmark estimate; it is gone on every channel, stable included.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* Measured mode (the `measured-savings` experimental feature; the launch sets
|
|
17
|
-
* MNEMOM_STATUSLINE_MEASURED=1): the gateway measures each request's saving — the stock
|
|
18
|
-
* request's input cost minus what it billed — and writes it into the response usage as
|
|
19
|
-
* `mnemom_savings` inside a `usage.iterations` entry, the one place Claude Code keeps an
|
|
20
|
-
* unknown usage key in its transcript. The counter sums those per message id
|
|
21
|
-
* ("mnemom saved $X this session", no "~": it is measured) and makes no claim before the
|
|
22
|
-
* first one arrives. Its state lives in its own file (<session>.measured.json), so the two
|
|
23
|
-
* modes never share a count.
|
|
9
|
+
* On a prerelease (`next`) launch the segment is fed: the launcher writes this session's
|
|
10
|
+
* measured model cost and μ from Record's accumulator to the file named in
|
|
11
|
+
* MNEMOM_STATUSLINE_FEED (lib/statusline-feed.ts), and the segment reads
|
|
12
|
+
* "mnemom · $0.42 · 1.70 μ". Until that file holds valid totals it is only "mnemom".
|
|
13
|
+
* Stable launches set no feed, so stable is only "mnemom".
|
|
24
14
|
*
|
|
25
15
|
* A user's own statusLine is never replaced: the launch passes it in
|
|
26
16
|
* MNEMOM_STATUSLINE_INNER, it runs with the same stdin, and our segment is appended.
|
|
27
|
-
*
|
|
28
|
-
* the user's own statusLine command, and the first-run code is held to spawning nothing.
|
|
17
|
+
* No transcript reads and no network: the only file read is the feed.
|
|
29
18
|
*/
|
|
30
19
|
import { execSync } from "node:child_process";
|
|
31
|
-
import {
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
32
21
|
import { homedir } from "node:os";
|
|
33
22
|
import { join } from "node:path";
|
|
34
|
-
import { origContext, parseUsageLine, rowCost, ruleRate } from "./first-run/backtest.js";
|
|
35
23
|
import { parseJson } from "./first-run/lines.js";
|
|
36
|
-
export const STATE_DIR = "statusline";
|
|
37
|
-
export const MAX_BYTES_PER_TICK = 8 * 1024 * 1024;
|
|
38
24
|
/** The launcher passes the user's own statusLine command here. */
|
|
39
25
|
export const INNER_ENV = "MNEMOM_STATUSLINE_INNER";
|
|
40
|
-
/** Set to 1 to launch without the
|
|
26
|
+
/** Set to 1 to launch without the segment. */
|
|
41
27
|
export const NO_STATUSLINE_ENV = "MNEMOM_AGENT_NO_STATUSLINE";
|
|
42
|
-
/** The launcher sets this to 1 when the gateway measures the session's saving. */
|
|
43
|
-
export const MEASURED_ENV = "MNEMOM_STATUSLINE_MEASURED";
|
|
44
28
|
export const INNER_TIMEOUT_MS = 2000;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
export
|
|
49
|
-
return { v: 1, files: {}, ids: {}, saved: 0, seen: false };
|
|
50
|
-
}
|
|
51
|
-
/** The main transcript, then its subagent transcripts (sorted). */
|
|
52
|
-
export function sessionFiles(transcriptPath) {
|
|
53
|
-
const dir = join(transcriptPath.replace(/\.jsonl$/, ""), "subagents");
|
|
54
|
-
let subs = [];
|
|
55
|
-
try {
|
|
56
|
-
subs = readdirSync(dir)
|
|
57
|
-
.filter((f) => f.endsWith(".jsonl"))
|
|
58
|
-
.sort()
|
|
59
|
-
.map((f) => join(dir, f));
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
/* no subagents */
|
|
63
|
-
}
|
|
64
|
-
return [transcriptPath, ...subs];
|
|
65
|
-
}
|
|
29
|
+
/** What the segment says until it is fed. */
|
|
30
|
+
export const SEGMENT = "mnemom";
|
|
31
|
+
/** The launcher names the feed file here (prerelease launches only). */
|
|
32
|
+
export const FEED_ENV = "MNEMOM_STATUSLINE_FEED";
|
|
66
33
|
/**
|
|
67
|
-
* The
|
|
68
|
-
*
|
|
34
|
+
* The gateway's per-conversation lineage key (deriveLineageKey): `cid:` or `cr:` + 16 hex.
|
|
35
|
+
* base-url-proxy.mjs (a standalone script) keeps its own copy; a test pins the two equal.
|
|
69
36
|
*/
|
|
70
|
-
export
|
|
71
|
-
|
|
37
|
+
export const LINEAGE_KEY_RE = /^(cid|cr):[0-9a-f]{16}$/;
|
|
38
|
+
/** A millimu amount as the record route returns it: a decimal string, no sign, no leading 0. */
|
|
39
|
+
export const MMU_RE = /^(0|[1-9][0-9]{0,18})$/;
|
|
40
|
+
/** The feed file's totals, or null when it is absent or anything in it is off. */
|
|
41
|
+
export function readFeed(path) {
|
|
72
42
|
try {
|
|
73
|
-
|
|
43
|
+
const d = parseJson(readFileSync(path, "utf8"));
|
|
44
|
+
if (!d)
|
|
45
|
+
return null;
|
|
46
|
+
const cost = d.model_cost_mmu;
|
|
47
|
+
const mu = d.mu_charged_mmu;
|
|
48
|
+
if (typeof cost !== "string" || !MMU_RE.test(cost))
|
|
49
|
+
return null;
|
|
50
|
+
if (mu !== null && (typeof mu !== "string" || !MMU_RE.test(mu)))
|
|
51
|
+
return null;
|
|
52
|
+
return { model_cost_mmu: cost, mu_charged_mmu: mu };
|
|
74
53
|
}
|
|
75
54
|
catch {
|
|
76
55
|
return null;
|
|
77
56
|
}
|
|
78
|
-
try {
|
|
79
|
-
const size = fstatSync(fd).size;
|
|
80
|
-
if (size < offset)
|
|
81
|
-
return null;
|
|
82
|
-
const n = Math.min(size - offset, maxBytes);
|
|
83
|
-
if (n <= 0)
|
|
84
|
-
return { lines: [], next: offset };
|
|
85
|
-
const buf = Buffer.allocUnsafe(n);
|
|
86
|
-
const got = readSync(fd, buf, 0, n, offset);
|
|
87
|
-
const view = buf.subarray(0, got);
|
|
88
|
-
const last = view.lastIndexOf(0x0a);
|
|
89
|
-
// One line longer than the whole budget: skip past it rather than stall forever.
|
|
90
|
-
if (last === -1)
|
|
91
|
-
return { lines: [], next: got === maxBytes ? offset + got : offset };
|
|
92
|
-
const lines = [];
|
|
93
|
-
let start = 0;
|
|
94
|
-
for (let nl = view.indexOf(0x0a); nl !== -1 && nl <= last; nl = view.indexOf(0x0a, start)) {
|
|
95
|
-
lines.push(view.subarray(start, nl));
|
|
96
|
-
start = nl + 1;
|
|
97
|
-
}
|
|
98
|
-
return { lines, next: offset + last + 1 };
|
|
99
|
-
}
|
|
100
|
-
finally {
|
|
101
|
-
closeSync(fd);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* The incremental read both modes share: each file's new complete lines, within the tick's
|
|
106
|
-
* byte budget, go to `add` (with the key a record without an id is counted under). A file
|
|
107
|
-
* that shrank or vanished starts over from `fresh()`.
|
|
108
|
-
*/
|
|
109
|
-
function readSession(st, files, maxBytes, fresh, add) {
|
|
110
|
-
let budget = maxBytes;
|
|
111
|
-
for (const f of files) {
|
|
112
|
-
if (budget <= 0)
|
|
113
|
-
break;
|
|
114
|
-
const from = st.files[f] ?? 0;
|
|
115
|
-
const r = readNewLines(f, from, budget);
|
|
116
|
-
if (r === null) {
|
|
117
|
-
if (from > 0)
|
|
118
|
-
return readSession(fresh(), files, maxBytes, fresh, add);
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
budget -= r.next - from;
|
|
122
|
-
const subagent = f.includes("/subagents/");
|
|
123
|
-
r.lines.forEach((line, i) => add(st, line, subagent, `anon-${f}-${from}-${i}`));
|
|
124
|
-
st.files[f] = r.next;
|
|
125
|
-
}
|
|
126
|
-
return st;
|
|
127
57
|
}
|
|
128
|
-
/**
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return;
|
|
134
|
-
const key = p.id ?? anonKey;
|
|
135
|
-
const cost = rowCost(p.row);
|
|
136
|
-
const saved = ruleRate(origContext(p.row)).screen * cost;
|
|
137
|
-
const prev = s.ids[key];
|
|
138
|
-
if (prev) {
|
|
139
|
-
s.actual -= prev[0];
|
|
140
|
-
s.saved -= prev[1];
|
|
141
|
-
}
|
|
142
|
-
s.ids[key] = [cost, saved];
|
|
143
|
-
s.actual += cost;
|
|
144
|
-
s.saved += saved;
|
|
145
|
-
});
|
|
58
|
+
/** `n / 10^scale` rounded half up, as a decimal with `places` digits, in BigInt (never a float). */
|
|
59
|
+
function fixed(mmu, divisor, places) {
|
|
60
|
+
const v = (BigInt(mmu) * 10n ** BigInt(places) * 2n + divisor) / (2n * divisor);
|
|
61
|
+
const s = v.toString().padStart(places + 1, "0");
|
|
62
|
+
return `${s.slice(0, -places)}.${s.slice(-places)}`;
|
|
146
63
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* One transcript line → the gateway's measured saving for its request, or null (not an
|
|
156
|
-
* assistant record, or no measurement in its `message.usage.iterations`). `id` is the message
|
|
157
|
-
* id (or record uuid); null when the record carries neither. Claude Code writes one record per
|
|
158
|
-
* content block, all with the same id and usage, so the id is what dedupes them.
|
|
159
|
-
*/
|
|
160
|
-
export function parseMeasuredLine(line) {
|
|
161
|
-
if (line.indexOf(SAVINGS) === -1 || line.indexOf(ASSISTANT) === -1)
|
|
162
|
-
return null;
|
|
163
|
-
const d = parseJson(line);
|
|
164
|
-
if (d?.type !== "assistant")
|
|
165
|
-
return null;
|
|
166
|
-
const m = d.message;
|
|
167
|
-
const iterations = m?.usage?.iterations;
|
|
168
|
-
if (!Array.isArray(iterations))
|
|
169
|
-
return null;
|
|
170
|
-
const saved = iterations.map(measuredSaving).find((v) => v !== null);
|
|
171
|
-
if (saved === undefined)
|
|
172
|
-
return null;
|
|
173
|
-
const id = (typeof m?.id === "string" && m.id) || (typeof d.uuid === "string" && d.uuid) || null;
|
|
174
|
-
return { id, saved };
|
|
64
|
+
/** Model cost in dollars: 1000 mmu = 1 μ = 1¢. Under half a cent reads "<$0.01", never "$0.00". */
|
|
65
|
+
export function formatCost(mmu) {
|
|
66
|
+
const big = BigInt(mmu);
|
|
67
|
+
if (big > 0n && big < 500n)
|
|
68
|
+
return "<$0.01";
|
|
69
|
+
return `$${fixed(mmu, 100000n, 2)}`;
|
|
175
70
|
}
|
|
176
|
-
/**
|
|
177
|
-
export function
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return;
|
|
182
|
-
const key = p.id ?? anonKey;
|
|
183
|
-
s.saved += p.saved - (s.ids[key] ?? 0);
|
|
184
|
-
s.ids[key] = p.saved;
|
|
185
|
-
s.seen = true;
|
|
186
|
-
});
|
|
71
|
+
/** μ charged, two places; null (no billing row) reads "μ not charged" (OD-9). */
|
|
72
|
+
export function formatMu(mmu) {
|
|
73
|
+
if (mmu === null)
|
|
74
|
+
return "μ not charged";
|
|
75
|
+
return `${fixed(mmu, 1000n, 2)} μ`;
|
|
187
76
|
}
|
|
188
|
-
/**
|
|
189
|
-
function
|
|
190
|
-
return (
|
|
191
|
-
}
|
|
192
|
-
/** The segment, rounded DOWN to the cent and never below $0.00. */
|
|
193
|
-
export function segment(saved) {
|
|
194
|
-
return `mnemom saved ~$${cents(saved)} this session`;
|
|
195
|
-
}
|
|
196
|
-
/** Measured mode's segment: no claim until the gateway has measured something; no "~". */
|
|
197
|
-
export function measuredSegment(st) {
|
|
198
|
-
return st.seen ? `mnemom saved $${cents(st.saved)} this session` : "mnemom";
|
|
199
|
-
}
|
|
200
|
-
function stateName(sessionId) {
|
|
201
|
-
return sessionId.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
202
|
-
}
|
|
203
|
-
function statePath(stateDir, sessionId) {
|
|
204
|
-
return join(stateDir, `${stateName(sessionId)}.json`);
|
|
205
|
-
}
|
|
206
|
-
/** Measured mode's own file: a sanitized id has no ".", so it never collides with statePath. */
|
|
207
|
-
function measuredStatePath(stateDir, sessionId) {
|
|
208
|
-
return join(stateDir, `${stateName(sessionId)}.measured.json`);
|
|
209
|
-
}
|
|
210
|
-
export function loadState(path) {
|
|
211
|
-
try {
|
|
212
|
-
const v = parseJson(readFileSync(path, "utf8"));
|
|
213
|
-
if (v && v.v === 1 && v.files && v.ids)
|
|
214
|
-
return v;
|
|
215
|
-
}
|
|
216
|
-
catch {
|
|
217
|
-
/* none yet */
|
|
218
|
-
}
|
|
219
|
-
return freshState();
|
|
220
|
-
}
|
|
221
|
-
export function loadMeasuredState(path) {
|
|
222
|
-
try {
|
|
223
|
-
const v = parseJson(readFileSync(path, "utf8"));
|
|
224
|
-
if (v && v.v === 1 && v.files && v.ids && typeof v.seen === "boolean") {
|
|
225
|
-
return v;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
catch {
|
|
229
|
-
/* none yet */
|
|
230
|
-
}
|
|
231
|
-
return freshMeasuredState();
|
|
232
|
-
}
|
|
233
|
-
export function saveState(path, st) {
|
|
234
|
-
mkdirSync(join(path, ".."), { recursive: true, mode: 0o700 });
|
|
235
|
-
const tmp = `${path}.${process.pid}.tmp`;
|
|
236
|
-
writeFileSync(tmp, JSON.stringify(st), { mode: 0o600 });
|
|
237
|
-
renameSync(tmp, path);
|
|
77
|
+
/** The segment for these totals: "mnemom · $0.42 · 1.70 μ". */
|
|
78
|
+
export function fedSegment(t) {
|
|
79
|
+
return `${SEGMENT} · ${formatCost(t.model_cost_mmu)} · ${formatMu(t.mu_charged_mmu)}`;
|
|
238
80
|
}
|
|
239
81
|
/** Run the user's own statusLine command with the same stdin; its first line, or "". */
|
|
240
82
|
export function runInner(command, input) {
|
|
@@ -251,43 +93,12 @@ export function runInner(command, input) {
|
|
|
251
93
|
return "";
|
|
252
94
|
}
|
|
253
95
|
}
|
|
254
|
-
/** Is the counter in measured mode? The launch's MEASURED_ENV, exactly "1". */
|
|
255
|
-
export function measuredMode(env) {
|
|
256
|
-
return env[MEASURED_ENV] === "1";
|
|
257
|
-
}
|
|
258
|
-
function saveQuietly(path, st) {
|
|
259
|
-
try {
|
|
260
|
-
saveState(path, st);
|
|
261
|
-
}
|
|
262
|
-
catch {
|
|
263
|
-
/* an unwritable state dir only means the next tick re-reads */
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
96
|
/** Claude Code's status JSON → the one status line. Never throws. */
|
|
267
|
-
export function render(stdin, deps) {
|
|
97
|
+
export function render(stdin, deps = {}) {
|
|
268
98
|
const innerOut = deps.inner ? (deps.runInner ?? runInner)(deps.inner, stdin) : "";
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const tp = typeof d?.transcript_path === "string" ? d.transcript_path : null;
|
|
273
|
-
const sid = typeof d?.session_id === "string" && d.session_id ? d.session_id : null;
|
|
274
|
-
if (tp && sid && deps.measured) {
|
|
275
|
-
const path = measuredStatePath(deps.stateDir, sid);
|
|
276
|
-
const st = updateMeasured(loadMeasuredState(path), sessionFiles(tp));
|
|
277
|
-
seg = measuredSegment(st);
|
|
278
|
-
saveQuietly(path, st);
|
|
279
|
-
}
|
|
280
|
-
else if (tp && sid) {
|
|
281
|
-
const path = statePath(deps.stateDir, sid);
|
|
282
|
-
const st = update(loadState(path), sessionFiles(tp));
|
|
283
|
-
seg = segment(st.saved);
|
|
284
|
-
saveQuietly(path, st);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
/* the counter never breaks the status bar */
|
|
289
|
-
}
|
|
290
|
-
return innerOut ? `${innerOut} · ${seg}` : seg;
|
|
99
|
+
const totals = deps.feed ? readFeed(deps.feed) : null;
|
|
100
|
+
const segment = totals ? fedSegment(totals) : SEGMENT;
|
|
101
|
+
return innerOut ? `${innerOut} · ${segment}` : segment;
|
|
291
102
|
}
|
|
292
103
|
/**
|
|
293
104
|
* The statusLine Claude Code would use without us: the first settings file (highest
|
|
@@ -319,9 +130,8 @@ export function userStatusLine(cwd, configDir) {
|
|
|
319
130
|
}
|
|
320
131
|
const sq = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
321
132
|
/**
|
|
322
|
-
* Launch args + env that add the
|
|
133
|
+
* Launch args + env that add the segment, or null when it should stay off: the opt-out
|
|
323
134
|
* env, or the user passing their own --settings (two would fight over precedence).
|
|
324
|
-
* `measured` puts the counter in measured mode (MEASURED_ENV).
|
|
325
135
|
*/
|
|
326
136
|
export function statuslineLaunch(o) {
|
|
327
137
|
if (o.env[NO_STATUSLINE_ENV] === "1")
|
|
@@ -337,8 +147,6 @@ export function statuslineLaunch(o) {
|
|
|
337
147
|
if (user?.padding !== undefined)
|
|
338
148
|
statusLine.padding = user.padding;
|
|
339
149
|
const env = user ? { [INNER_ENV]: user.command } : {};
|
|
340
|
-
if (o.measured)
|
|
341
|
-
env[MEASURED_ENV] = "1";
|
|
342
150
|
// `settings` is the same layer as `args`, for a caller that merges it with its own (Claude Code
|
|
343
151
|
// keeps only the last --settings it is given).
|
|
344
152
|
return { args: ["--settings", JSON.stringify({ statusLine })], env, settings: { statusLine } };
|