claude-maestro 0.1.20 → 0.1.21
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/out/main/index.js +708 -226
- package/out/preload/index.js +4 -0
- package/out/renderer/assets/{index-C2xG1-2F.js → index-BDJGybQo.js} +2 -2
- package/out/renderer/assets/{index-CALj_g-h.js → index-BDxfkk76.js} +2 -2
- package/out/renderer/assets/{index-BX4eMUiW.js → index-BNIPHMhg.js} +2 -2
- package/out/renderer/assets/{index-BSnEdUx8.js → index-BPiKmKfX.js} +5 -5
- package/out/renderer/assets/{index-BUr9qTcP.js → index-Bf87-cF0.js} +5 -5
- package/out/renderer/assets/{index--LsdCcT2.js → index-Bm6BMufC.js} +2 -2
- package/out/renderer/assets/{index-KjJOaoK3.js → index-Bw6wDwNB.js} +2 -2
- package/out/renderer/assets/{index-CwrdXZxY.js → index-C2wfbMG1.js} +2 -2
- package/out/renderer/assets/{index-BD5wVgZG.js → index-C3_9l5Zo.js} +2 -2
- package/out/renderer/assets/{index-2Q5NJLLa.js → index-CIAp39oC.js} +2 -2
- package/out/renderer/assets/{index-B4pxYlCv.css → index-CTyPr1hG.css} +942 -102
- package/out/renderer/assets/{index-B9wQ40iJ.js → index-CYo0nynQ.js} +4 -4
- package/out/renderer/assets/{index-BdkQGOfF.js → index-CZmL3oq-.js} +5 -5
- package/out/renderer/assets/{index-CqEbh7gN.js → index-CdG7xnB7.js} +2 -2
- package/out/renderer/assets/{index-Dpj9EP42.js → index-Cfvyl_8T.js} +3 -3
- package/out/renderer/assets/{index-NXP0WfIH.js → index-Ck4WZgFA.js} +2 -2
- package/out/renderer/assets/{index-DFnn9t0U.js → index-D9lxbtli.js} +5 -5
- package/out/renderer/assets/{index-DMLpIsZn.js → index-DD6EoqPp.js} +2 -2
- package/out/renderer/assets/{index-BKVqTAbd.js → index-DGNONcNh.js} +3 -3
- package/out/renderer/assets/{index-K7jPnu8A.js → index-DroXAl3A.js} +1 -1
- package/out/renderer/assets/{index-BYrBOYyo.js → index-DzjUOrFM.js} +5 -5
- package/out/renderer/assets/{index-BeXyvqqV.js → index-Uh6FxvAQ.js} +4358 -2417
- package/out/renderer/assets/{index-DwCelZNB.js → index-iIulTEbp.js} +5 -5
- package/out/renderer/assets/{index-RU5VUPJx.js → index-kJ0KF5bI.js} +2 -2
- package/out/renderer/index.html +2 -2
- package/package.json +2 -1
package/out/main/index.js
CHANGED
|
@@ -463,6 +463,12 @@ async function gitChangedFiles(folder) {
|
|
|
463
463
|
}
|
|
464
464
|
}
|
|
465
465
|
const MAX_DIFF_CHARS = 5e5;
|
|
466
|
+
function toFileDiff(text) {
|
|
467
|
+
const binary = /^Binary files .* differ$/m.test(text);
|
|
468
|
+
if (text.length <= MAX_DIFF_CHARS) return { diff: text, binary, truncated: false };
|
|
469
|
+
const cut = text.lastIndexOf("\n", MAX_DIFF_CHARS);
|
|
470
|
+
return { diff: text.slice(0, cut > 0 ? cut : MAX_DIFF_CHARS), binary, truncated: true };
|
|
471
|
+
}
|
|
466
472
|
async function gitFileDiff(folder, path2) {
|
|
467
473
|
const empty = { diff: "", binary: false, truncated: false };
|
|
468
474
|
try {
|
|
@@ -476,11 +482,47 @@ async function gitFileDiff(folder, path2) {
|
|
|
476
482
|
} else {
|
|
477
483
|
res = await git(root2, ["diff", "--no-index", "--", "/dev/null", path2]);
|
|
478
484
|
}
|
|
485
|
+
return toFileDiff(res.stdout);
|
|
486
|
+
} catch {
|
|
487
|
+
return empty;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function parseNameStatus(out) {
|
|
491
|
+
const files = [];
|
|
492
|
+
for (const line of out.split(/\r?\n/)) {
|
|
493
|
+
if (!line.trim()) continue;
|
|
494
|
+
const parts = line.split(" ");
|
|
495
|
+
const letter = parts[0]?.[0] ?? "M";
|
|
496
|
+
if ((letter === "R" || letter === "C") && parts.length >= 3) {
|
|
497
|
+
files.push({ path: parts[2], status: letter, origPath: parts[1] });
|
|
498
|
+
} else if (parts[1]) {
|
|
499
|
+
files.push({ path: parts[1], status: letter });
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
503
|
+
return files;
|
|
504
|
+
}
|
|
505
|
+
async function branchDiff(folder, branch, baseBranch) {
|
|
506
|
+
const empty = { diff: "", truncated: false, files: [], branch, baseBranch };
|
|
507
|
+
try {
|
|
508
|
+
const mb = await git(folder, ["merge-base", baseBranch, branch]);
|
|
509
|
+
const base = mb.code === 0 ? mb.stdout.trim() : "";
|
|
510
|
+
if (!base) return empty;
|
|
511
|
+
const ns = await git(folder, ["diff", "--name-status", base, branch]);
|
|
512
|
+
const files = ns.code === 0 ? parseNameStatus(ns.stdout) : [];
|
|
513
|
+
const res = await git(folder, ["diff", base, branch]);
|
|
479
514
|
const text = res.stdout;
|
|
480
|
-
|
|
481
|
-
|
|
515
|
+
if (text.length <= MAX_DIFF_CHARS) {
|
|
516
|
+
return { diff: text, truncated: false, files, branch, baseBranch };
|
|
517
|
+
}
|
|
482
518
|
const cut = text.lastIndexOf("\n", MAX_DIFF_CHARS);
|
|
483
|
-
return {
|
|
519
|
+
return {
|
|
520
|
+
diff: text.slice(0, cut > 0 ? cut : MAX_DIFF_CHARS),
|
|
521
|
+
truncated: true,
|
|
522
|
+
files,
|
|
523
|
+
branch,
|
|
524
|
+
baseBranch
|
|
525
|
+
};
|
|
484
526
|
} catch {
|
|
485
527
|
return empty;
|
|
486
528
|
}
|
|
@@ -859,6 +901,33 @@ async function listCheckpoints(folder) {
|
|
|
859
901
|
}
|
|
860
902
|
return out;
|
|
861
903
|
}
|
|
904
|
+
async function checkpointDiff(folder, id) {
|
|
905
|
+
const empty = { diff: "", binary: false, truncated: false };
|
|
906
|
+
if (!isValidCheckpointId(id)) return empty;
|
|
907
|
+
const root2 = await repoRootOf(folder);
|
|
908
|
+
const ref = CHECKPOINT_REF_PREFIX + id;
|
|
909
|
+
const resolved = await git(root2, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
|
|
910
|
+
if (resolved.code !== 0 || !resolved.stdout.trim()) return empty;
|
|
911
|
+
const hash = resolved.stdout.trim();
|
|
912
|
+
const indexFile = path.join(os.tmpdir(), `maestro-ckpt-diff-${id}-${checkpointSeq++}.index`);
|
|
913
|
+
try {
|
|
914
|
+
const indexEnv = { GIT_INDEX_FILE: indexFile };
|
|
915
|
+
const add2 = await git(root2, ["add", "-A"], indexEnv);
|
|
916
|
+
if (add2.code !== 0) return empty;
|
|
917
|
+
const writeTree = await git(root2, ["write-tree"], indexEnv);
|
|
918
|
+
if (writeTree.code !== 0) return empty;
|
|
919
|
+
const curTree = writeTree.stdout.trim();
|
|
920
|
+
const res = await git(root2, ["diff", hash, curTree]);
|
|
921
|
+
return toFileDiff(res.stdout);
|
|
922
|
+
} catch {
|
|
923
|
+
return empty;
|
|
924
|
+
} finally {
|
|
925
|
+
try {
|
|
926
|
+
fs.rmSync(indexFile, { force: true });
|
|
927
|
+
} catch {
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
862
931
|
async function restoreCheckpoint(folder, id) {
|
|
863
932
|
if (!isValidCheckpointId(id)) {
|
|
864
933
|
return { ok: false, output: `Invalid checkpoint id: ${id}`, safety: null };
|
|
@@ -939,6 +1008,10 @@ class StatusDetector {
|
|
|
939
1008
|
get lastOutput() {
|
|
940
1009
|
return this.lastOutputAt;
|
|
941
1010
|
}
|
|
1011
|
+
/** ANSI-stripped tail of recent output, for prompt-content checks. */
|
|
1012
|
+
get recentText() {
|
|
1013
|
+
return stripAnsi(this.tail);
|
|
1014
|
+
}
|
|
942
1015
|
/** ms epoch when the current status was continuously entered (watchdog clock). */
|
|
943
1016
|
get since() {
|
|
944
1017
|
return this.statusSince;
|
|
@@ -2497,7 +2570,7 @@ const MIN_CONFIDENCE = 0.6;
|
|
|
2497
2570
|
const MAX_SUGGESTIONS_PER_DETECT = 2;
|
|
2498
2571
|
const JUDGE_DAILY_CAP = 12;
|
|
2499
2572
|
const MAX_SUGGESTIONS = 60;
|
|
2500
|
-
function localDay$
|
|
2573
|
+
function localDay$2() {
|
|
2501
2574
|
const d = /* @__PURE__ */ new Date();
|
|
2502
2575
|
return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
|
|
2503
2576
|
}
|
|
@@ -2679,7 +2752,7 @@ class FactoryService {
|
|
|
2679
2752
|
return;
|
|
2680
2753
|
}
|
|
2681
2754
|
const turnsAtStart = g.turnsSinceDetect;
|
|
2682
|
-
const day = localDay$
|
|
2755
|
+
const day = localDay$2();
|
|
2683
2756
|
const callsToday = g.judgeDay === day ? g.judgeCallsToday : 0;
|
|
2684
2757
|
if (callsToday >= JUDGE_DAILY_CAP) return;
|
|
2685
2758
|
const recent = messages.filter((m) => !m.pending && m.text?.trim()).slice(-6);
|
|
@@ -2705,7 +2778,7 @@ class FactoryService {
|
|
|
2705
2778
|
this.inFlight = null;
|
|
2706
2779
|
this.setBusy(false);
|
|
2707
2780
|
const g2 = this.store.loadGrowth();
|
|
2708
|
-
const day2 = localDay$
|
|
2781
|
+
const day2 = localDay$2();
|
|
2709
2782
|
this.store.setGrowth({
|
|
2710
2783
|
...g2,
|
|
2711
2784
|
lastDetectAt: Date.now(),
|
|
@@ -3858,7 +3931,10 @@ class FsService {
|
|
|
3858
3931
|
}
|
|
3859
3932
|
}
|
|
3860
3933
|
}
|
|
3934
|
+
const TOKENS_PER_XP = 1e3;
|
|
3861
3935
|
const XP_TABLE = {
|
|
3936
|
+
"tokens.burn": 0,
|
|
3937
|
+
// computed from meta.tokens in xpForEvent
|
|
3862
3938
|
"session.turn": 5,
|
|
3863
3939
|
"conductor.turn": 5,
|
|
3864
3940
|
"action.run": 8,
|
|
@@ -3875,6 +3951,9 @@ const XP_TABLE = {
|
|
|
3875
3951
|
"feature.merge": 80
|
|
3876
3952
|
};
|
|
3877
3953
|
function xpForEvent(e) {
|
|
3954
|
+
if (e.type === "tokens.burn") {
|
|
3955
|
+
return Math.max(0, Math.floor((e.meta?.tokens ?? 0) / TOKENS_PER_XP));
|
|
3956
|
+
}
|
|
3878
3957
|
let xp = XP_TABLE[e.type] ?? 0;
|
|
3879
3958
|
if (e.type === "worktree.merge") xp += Math.min(e.meta?.commits ?? 0, 10) * 3;
|
|
3880
3959
|
return xp;
|
|
@@ -3903,6 +3982,8 @@ const DEFAULT_GAME_STATE = {
|
|
|
3903
3982
|
counters: {},
|
|
3904
3983
|
nightTurns: 0,
|
|
3905
3984
|
earlyTurns: 0,
|
|
3985
|
+
tokensBurned: 0,
|
|
3986
|
+
usageTokensSeen: -1,
|
|
3906
3987
|
createdAt: 0
|
|
3907
3988
|
};
|
|
3908
3989
|
const c = (ctx, t) => ctx.counters[t] ?? 0;
|
|
@@ -3930,6 +4011,23 @@ const ACHIEVEMENTS = [
|
|
|
3930
4011
|
{ id: "first-agent", title: "Agent Architect", desc: "Create your first agent.", icon: "🤖", category: "factory", predicate: (x) => c(x, "factory.agent") >= 1 },
|
|
3931
4012
|
{ id: "toolsmith", title: "Toolsmith", desc: "Create 5 skills/agents.", icon: "⚒", category: "factory", predicate: (x) => c(x, "factory.skill") + c(x, "factory.agent") >= 5 },
|
|
3932
4013
|
{ id: "master-toolsmith", title: "Master Toolsmith", desc: "Create 20 skills/agents.", icon: "🏭", category: "factory", predicate: (x) => c(x, "factory.skill") + c(x, "factory.agent") >= 20 },
|
|
4014
|
+
// workflow (checkpoints, actions, feature specs, auto-expand)
|
|
4015
|
+
{ id: "first-checkpoint", title: "Safe Keeper", desc: "Make your first checkpoint.", icon: "📍", category: "workflow", predicate: (x) => c(x, "checkpoint.create") >= 1 },
|
|
4016
|
+
{ id: "checkpoint-25", title: "Time Traveler", desc: "Make 25 checkpoints.", icon: "⏳", category: "workflow", predicate: (x) => c(x, "checkpoint.create") >= 25 },
|
|
4017
|
+
{ id: "first-action", title: "Press Start", desc: "Run your first reusable action.", icon: "▶️", category: "workflow", predicate: (x) => c(x, "action.run") >= 1 },
|
|
4018
|
+
{ id: "action-50", title: "Power User", desc: "Run 50 actions.", icon: "⚡", category: "workflow", predicate: (x) => c(x, "action.run") >= 50 },
|
|
4019
|
+
{ id: "first-feature-spec", title: "Drawing Board", desc: "Save your first feature spec.", icon: "📝", category: "workflow", predicate: (x) => c(x, "feature.save") >= 1 },
|
|
4020
|
+
{ id: "feature-architect", title: "Spec Architect", desc: "Save 10 feature specs.", icon: "📐", category: "workflow", predicate: (x) => c(x, "feature.save") >= 10 },
|
|
4021
|
+
{ id: "first-autoexpand", title: "Brainstormer", desc: "Finish your first Auto-Expand run.", icon: "💡", category: "workflow", predicate: (x) => c(x, "autoexpand.done") >= 1 },
|
|
4022
|
+
{ id: "autoexpand-10", title: "Idea Machine", desc: "Finish 10 Auto-Expand runs.", icon: "🧠", category: "workflow", predicate: (x) => c(x, "autoexpand.done") >= 10 },
|
|
4023
|
+
// guardian (Sentinels)
|
|
4024
|
+
{ id: "first-sentinel", title: "On Watch", desc: "Run your first Sentinel check.", icon: "🛡", category: "guardian", predicate: (x) => c(x, "sentinel.run") >= 1 },
|
|
4025
|
+
{ id: "sentinel-25", title: "Vigilant", desc: "Run 25 Sentinel checks.", icon: "👁", category: "guardian", predicate: (x) => c(x, "sentinel.run") >= 25 },
|
|
4026
|
+
{ id: "sentinel-100", title: "Guardian", desc: "Run 100 Sentinel checks.", icon: "🦾", category: "guardian", predicate: (x) => c(x, "sentinel.run") >= 100 },
|
|
4027
|
+
// tokens (burning tokens levels you up)
|
|
4028
|
+
{ id: "tokens-1m", title: "Token Tinkerer", desc: "Burn 1M tokens.", icon: "🪙", category: "tokens", predicate: (x) => x.tokensBurned >= 1e6 },
|
|
4029
|
+
{ id: "tokens-10m", title: "Token Furnace", desc: "Burn 10M tokens.", icon: "🔥", category: "tokens", predicate: (x) => x.tokensBurned >= 1e7 },
|
|
4030
|
+
{ id: "tokens-100m", title: "Token Inferno", desc: "Burn 100M tokens.", icon: "🌋", category: "tokens", predicate: (x) => x.tokensBurned >= 1e8 },
|
|
3933
4031
|
// streak
|
|
3934
4032
|
{ id: "streak-3", title: "Warmed Up", desc: "Keep a 3-day streak.", icon: "🔥", category: "streak", predicate: (x) => x.streakLongest >= 3 },
|
|
3935
4033
|
{ id: "streak-7", title: "On Fire", desc: "Keep a 7-day streak.", icon: "🔥", category: "streak", predicate: (x) => x.streakLongest >= 7 },
|
|
@@ -3939,7 +4037,11 @@ const ACHIEVEMENTS = [
|
|
|
3939
4037
|
{ id: "early-bird", title: "Early Bird", desc: "Finish a turn between 5 and 8am.", icon: "🌅", category: "time", predicate: (x) => x.earlyTurns >= 1 },
|
|
3940
4038
|
// level
|
|
3941
4039
|
{ id: "level-10", title: "Double Digits", desc: "Reach level 10.", icon: "⭐", category: "level", predicate: (x) => x.level >= 10 },
|
|
3942
|
-
{ id: "level-25", title: "Maestro Prime", desc: "Reach level 25.", icon: "🌟", category: "level", predicate: (x) => x.level >= 25 }
|
|
4040
|
+
{ id: "level-25", title: "Maestro Prime", desc: "Reach level 25.", icon: "🌟", category: "level", predicate: (x) => x.level >= 25 },
|
|
4041
|
+
{ id: "level-50", title: "Maestro Legend", desc: "Reach level 50.", icon: "👑", category: "level", predicate: (x) => x.level >= 50 },
|
|
4042
|
+
// mastery (cross-cutting capstones)
|
|
4043
|
+
{ id: "polymath", title: "Polymath", desc: "Use sessions, parallel tasks, the Factory, feature specs and Sentinels.", icon: "🧩", category: "mastery", predicate: (x) => c(x, "session.create") >= 1 && c(x, "worktree.create") >= 1 && c(x, "factory.skill") + c(x, "factory.agent") >= 1 && c(x, "feature.save") >= 1 && c(x, "sentinel.run") >= 1 },
|
|
4044
|
+
{ id: "all-rounder", title: "All-Rounder", desc: "Merge a worktree, ship a feature and open a PR.", icon: "🏅", category: "mastery", predicate: (x) => c(x, "worktree.merge") >= 1 && c(x, "feature.merge") >= 1 && c(x, "worktree.pr") >= 1 }
|
|
3943
4045
|
];
|
|
3944
4046
|
const DAILY_QUEST_POOL = [
|
|
3945
4047
|
{ id: "q-turns-5", title: "Finish 5 Claude turns", target: 5, reward: 30, events: ["session.turn"] },
|
|
@@ -3951,7 +4053,11 @@ const DAILY_QUEST_POOL = [
|
|
|
3951
4053
|
{ id: "q-conductor-3", title: "Have 3 Conductor turns", target: 3, reward: 30, events: ["conductor.turn"] },
|
|
3952
4054
|
{ id: "q-factory-1", title: "Create a skill or agent", target: 1, reward: 75, events: ["factory.skill", "factory.agent"] },
|
|
3953
4055
|
{ id: "q-feature-1", title: "Save a feature spec", target: 1, reward: 30, events: ["feature.save"] },
|
|
3954
|
-
{ id: "q-pr-1", title: "Open a pull request", target: 1, reward: 50, events: ["worktree.pr"] }
|
|
4056
|
+
{ id: "q-pr-1", title: "Open a pull request", target: 1, reward: 50, events: ["worktree.pr"] },
|
|
4057
|
+
{ id: "q-action-3", title: "Run 3 reusable actions", target: 3, reward: 30, events: ["action.run"] },
|
|
4058
|
+
{ id: "q-sentinel-1", title: "Run a Sentinel check", target: 1, reward: 25, events: ["sentinel.run"] },
|
|
4059
|
+
{ id: "q-autoexpand-1", title: "Finish an Auto-Expand run", target: 1, reward: 40, events: ["autoexpand.done"] },
|
|
4060
|
+
{ id: "q-checkpoint-3", title: "Make 3 checkpoints", target: 3, reward: 40, events: ["checkpoint.create"] }
|
|
3955
4061
|
];
|
|
3956
4062
|
const questDef = (id) => DAILY_QUEST_POOL.find((q) => q.id === id);
|
|
3957
4063
|
function hashStr(s) {
|
|
@@ -4017,6 +4123,8 @@ class GamificationStore {
|
|
|
4017
4123
|
}
|
|
4018
4124
|
s.nightTurns = num(s.nightTurns, 0);
|
|
4019
4125
|
s.earlyTurns = num(s.earlyTurns, 0);
|
|
4126
|
+
s.tokensBurned = num(s.tokensBurned, 0);
|
|
4127
|
+
s.usageTokensSeen = num(s.usageTokensSeen, -1);
|
|
4020
4128
|
s.xp = num(s.xp, 0);
|
|
4021
4129
|
if (!s.createdAt) s.createdAt = Date.now();
|
|
4022
4130
|
this.state = s;
|
|
@@ -4053,20 +4161,66 @@ class GamificationStore {
|
|
|
4053
4161
|
}
|
|
4054
4162
|
}
|
|
4055
4163
|
const ACHIEVEMENT_XP = 25;
|
|
4164
|
+
const TOKEN_POLL_MS = 6e4;
|
|
4056
4165
|
class GamificationService {
|
|
4057
|
-
|
|
4166
|
+
/**
|
|
4167
|
+
* @param getUsageTokens returns the lifetime input+output token total (from
|
|
4168
|
+
* UsageService). Polled to award XP for tokens burned since the last check.
|
|
4169
|
+
*/
|
|
4170
|
+
constructor(getWin2, getUsageTokens) {
|
|
4058
4171
|
this.getWin = getWin2;
|
|
4172
|
+
this.getUsageTokens = getUsageTokens;
|
|
4059
4173
|
this.state = this.store.load();
|
|
4060
4174
|
}
|
|
4061
4175
|
store = new GamificationStore();
|
|
4062
4176
|
state;
|
|
4177
|
+
tokenTimer = null;
|
|
4178
|
+
/** Begin polling burned tokens (baselined on the first tick, so historical
|
|
4179
|
+
* usage is never retroactively dumped into XP). */
|
|
4180
|
+
start() {
|
|
4181
|
+
if (this.tokenTimer || !this.getUsageTokens) return;
|
|
4182
|
+
this.pollTokenBurn();
|
|
4183
|
+
this.tokenTimer = setInterval(() => this.pollTokenBurn(), TOKEN_POLL_MS);
|
|
4184
|
+
}
|
|
4063
4185
|
/** GameState + derived level fields (for the initial renderer fetch). */
|
|
4064
4186
|
snapshot() {
|
|
4065
4187
|
return { ...this.state, ...levelInfo(this.state.xp) };
|
|
4066
4188
|
}
|
|
4067
4189
|
dispose() {
|
|
4190
|
+
if (this.tokenTimer) {
|
|
4191
|
+
clearInterval(this.tokenTimer);
|
|
4192
|
+
this.tokenTimer = null;
|
|
4193
|
+
}
|
|
4068
4194
|
this.store.saveNow();
|
|
4069
4195
|
}
|
|
4196
|
+
/**
|
|
4197
|
+
* Reconcile burned tokens into XP. The first observation only records the
|
|
4198
|
+
* baseline (no award). Afterward, each whole `TOKENS_PER_XP` of new input+
|
|
4199
|
+
* output tokens grants 1 XP via a `tokens.burn` event; the sub-unit remainder
|
|
4200
|
+
* is carried (the baseline advances only by tokens actually converted), so
|
|
4201
|
+
* even light usage eventually counts. A drop in the total (pruned transcripts)
|
|
4202
|
+
* silently re-baselines.
|
|
4203
|
+
*/
|
|
4204
|
+
pollTokenBurn() {
|
|
4205
|
+
if (!this.getUsageTokens) return;
|
|
4206
|
+
try {
|
|
4207
|
+
const total = this.getUsageTokens();
|
|
4208
|
+
if (!Number.isFinite(total) || total < 0) return;
|
|
4209
|
+
const seen = this.state.usageTokensSeen;
|
|
4210
|
+
if (seen < 0 || total < seen) {
|
|
4211
|
+
this.state.usageTokensSeen = total;
|
|
4212
|
+
this.store.set(this.state);
|
|
4213
|
+
return;
|
|
4214
|
+
}
|
|
4215
|
+
const units = Math.floor((total - seen) / TOKENS_PER_XP);
|
|
4216
|
+
if (units <= 0) return;
|
|
4217
|
+
const consumed = units * TOKENS_PER_XP;
|
|
4218
|
+
this.state.usageTokensSeen = seen + consumed;
|
|
4219
|
+
this.award({ type: "tokens.burn", meta: { tokens: consumed } });
|
|
4220
|
+
} catch (err) {
|
|
4221
|
+
console.error("Token-burn poll failed (ignored):", err);
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4070
4224
|
/**
|
|
4071
4225
|
* Apply one event: bump counters, roll the day (streak + quests), add XP,
|
|
4072
4226
|
* advance quests, unlock achievements — each guarded so nothing double-counts.
|
|
@@ -4092,7 +4246,11 @@ class GamificationService {
|
|
|
4092
4246
|
});
|
|
4093
4247
|
}
|
|
4094
4248
|
}
|
|
4095
|
-
|
|
4249
|
+
if (e.type === "tokens.burn") {
|
|
4250
|
+
s.tokensBurned += Math.max(0, e.meta?.tokens ?? 0);
|
|
4251
|
+
} else {
|
|
4252
|
+
s.counters[e.type] = (s.counters[e.type] ?? 0) + 1;
|
|
4253
|
+
}
|
|
4096
4254
|
if (e.type === "session.turn" || e.type === "conductor.turn") {
|
|
4097
4255
|
const hour = e.meta?.hour ?? now.getHours();
|
|
4098
4256
|
if (hour >= 0 && hour < 5) s.nightTurns += 1;
|
|
@@ -4125,7 +4283,8 @@ class GamificationService {
|
|
|
4125
4283
|
nightTurns: s.nightTurns,
|
|
4126
4284
|
earlyTurns: s.earlyTurns,
|
|
4127
4285
|
streakLongest: s.streak.longest,
|
|
4128
|
-
level: levelForXp(s.xp)
|
|
4286
|
+
level: levelForXp(s.xp),
|
|
4287
|
+
tokensBurned: s.tokensBurned
|
|
4129
4288
|
};
|
|
4130
4289
|
for (const a of ACHIEVEMENTS) {
|
|
4131
4290
|
if (s.achievements[a.id]) continue;
|
|
@@ -4281,6 +4440,182 @@ async function clearBackgroundImage() {
|
|
|
4281
4440
|
} catch {
|
|
4282
4441
|
}
|
|
4283
4442
|
}
|
|
4443
|
+
const PROJECTS_DIR$1 = path.join(os.homedir(), ".claude", "projects");
|
|
4444
|
+
const PREVIEW_MAX_CHARS = 160;
|
|
4445
|
+
function encodeFolder(folder) {
|
|
4446
|
+
return folder.replace(/[^a-zA-Z0-9]/g, "-");
|
|
4447
|
+
}
|
|
4448
|
+
function messageText(message) {
|
|
4449
|
+
if (!message || typeof message !== "object") return null;
|
|
4450
|
+
const content = message.content;
|
|
4451
|
+
if (typeof content === "string") return content.trim() || null;
|
|
4452
|
+
if (Array.isArray(content)) {
|
|
4453
|
+
const parts = [];
|
|
4454
|
+
for (const block of content) {
|
|
4455
|
+
if (block && typeof block === "object" && block.type === "text" && typeof block.text === "string") {
|
|
4456
|
+
parts.push(block.text);
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
const joined = parts.join(" ").trim();
|
|
4460
|
+
return joined || null;
|
|
4461
|
+
}
|
|
4462
|
+
return null;
|
|
4463
|
+
}
|
|
4464
|
+
function toPreview(text) {
|
|
4465
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
4466
|
+
return oneLine.length > PREVIEW_MAX_CHARS ? oneLine.slice(0, PREVIEW_MAX_CHARS) + "…" : oneLine;
|
|
4467
|
+
}
|
|
4468
|
+
function summarizeFile(path2, id) {
|
|
4469
|
+
let raw;
|
|
4470
|
+
try {
|
|
4471
|
+
raw = fs.readFileSync(path2, "utf8");
|
|
4472
|
+
} catch {
|
|
4473
|
+
return null;
|
|
4474
|
+
}
|
|
4475
|
+
let messageCount = 0;
|
|
4476
|
+
let lastActivityAt = 0;
|
|
4477
|
+
let preview = "";
|
|
4478
|
+
for (const line of raw.split("\n")) {
|
|
4479
|
+
if (!line.trim()) continue;
|
|
4480
|
+
let obj;
|
|
4481
|
+
try {
|
|
4482
|
+
obj = JSON.parse(line);
|
|
4483
|
+
} catch {
|
|
4484
|
+
continue;
|
|
4485
|
+
}
|
|
4486
|
+
const type = obj.type;
|
|
4487
|
+
if (type !== "user" && type !== "assistant") continue;
|
|
4488
|
+
messageCount++;
|
|
4489
|
+
const at = Date.parse(typeof obj.timestamp === "string" ? obj.timestamp : "");
|
|
4490
|
+
if (Number.isFinite(at) && at > lastActivityAt) lastActivityAt = at;
|
|
4491
|
+
if (!preview && type === "user" && obj.isMeta !== true) {
|
|
4492
|
+
const text = messageText(obj.message);
|
|
4493
|
+
if (text) preview = toPreview(text);
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
return { id, lastActivityAt, messageCount, preview };
|
|
4497
|
+
}
|
|
4498
|
+
function listConversations(folder) {
|
|
4499
|
+
const dir = path.join(PROJECTS_DIR$1, encodeFolder(folder));
|
|
4500
|
+
if (!fs.existsSync(dir)) return [];
|
|
4501
|
+
let files;
|
|
4502
|
+
try {
|
|
4503
|
+
files = fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
4504
|
+
} catch {
|
|
4505
|
+
return [];
|
|
4506
|
+
}
|
|
4507
|
+
const out = [];
|
|
4508
|
+
for (const file of files) {
|
|
4509
|
+
const summary = summarizeFile(path.join(dir, file), file.slice(0, -".jsonl".length));
|
|
4510
|
+
if (summary) out.push(summary);
|
|
4511
|
+
}
|
|
4512
|
+
out.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
|
|
4513
|
+
return out;
|
|
4514
|
+
}
|
|
4515
|
+
const SEARCH_MIN_QUERY = 2;
|
|
4516
|
+
const SEARCH_MAX_PER_CONVERSATION = 50;
|
|
4517
|
+
const SEARCH_MAX_RESULTS = 100;
|
|
4518
|
+
const SNIPPET_BEFORE = 40;
|
|
4519
|
+
const SNIPPET_AFTER = 140;
|
|
4520
|
+
const searchCache = /* @__PURE__ */ new Map();
|
|
4521
|
+
function makeSnippet(text, matchIndex, queryLength) {
|
|
4522
|
+
const start = Math.max(0, matchIndex - SNIPPET_BEFORE);
|
|
4523
|
+
const end = Math.min(text.length, matchIndex + queryLength + SNIPPET_AFTER);
|
|
4524
|
+
const core = text.slice(start, end).replace(/\s+/g, " ").trim();
|
|
4525
|
+
return (start > 0 ? "… " : "") + core + (end < text.length ? " …" : "");
|
|
4526
|
+
}
|
|
4527
|
+
function parseForSearch(path2, id) {
|
|
4528
|
+
const conv = { id, cwd: "", lastActivityAt: 0, messages: [] };
|
|
4529
|
+
let raw;
|
|
4530
|
+
try {
|
|
4531
|
+
raw = fs.readFileSync(path2, "utf8");
|
|
4532
|
+
} catch {
|
|
4533
|
+
return conv;
|
|
4534
|
+
}
|
|
4535
|
+
for (const line of raw.split("\n")) {
|
|
4536
|
+
if (!line.includes('"user"') && !line.includes('"assistant"')) continue;
|
|
4537
|
+
let obj;
|
|
4538
|
+
try {
|
|
4539
|
+
obj = JSON.parse(line);
|
|
4540
|
+
} catch {
|
|
4541
|
+
continue;
|
|
4542
|
+
}
|
|
4543
|
+
if (obj.type !== "user" && obj.type !== "assistant") continue;
|
|
4544
|
+
const text = messageText(obj.message);
|
|
4545
|
+
if (text) conv.messages.push(text);
|
|
4546
|
+
if (typeof obj.cwd === "string" && obj.cwd) conv.cwd = obj.cwd;
|
|
4547
|
+
const at = Date.parse(typeof obj.timestamp === "string" ? obj.timestamp : "");
|
|
4548
|
+
if (Number.isFinite(at) && at > conv.lastActivityAt) conv.lastActivityAt = at;
|
|
4549
|
+
}
|
|
4550
|
+
return conv;
|
|
4551
|
+
}
|
|
4552
|
+
function searchableFor(path2, id) {
|
|
4553
|
+
let stat;
|
|
4554
|
+
try {
|
|
4555
|
+
stat = fs.statSync(path2);
|
|
4556
|
+
} catch {
|
|
4557
|
+
searchCache.delete(path2);
|
|
4558
|
+
return { id, cwd: "", lastActivityAt: 0, messages: [] };
|
|
4559
|
+
}
|
|
4560
|
+
const cached = searchCache.get(path2);
|
|
4561
|
+
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) return cached.conv;
|
|
4562
|
+
const conv = parseForSearch(path2, id);
|
|
4563
|
+
searchCache.set(path2, { mtimeMs: stat.mtimeMs, size: stat.size, conv });
|
|
4564
|
+
return conv;
|
|
4565
|
+
}
|
|
4566
|
+
function searchConversations(query) {
|
|
4567
|
+
const q = query.trim();
|
|
4568
|
+
if (q.length < SEARCH_MIN_QUERY) return [];
|
|
4569
|
+
const needle = q.toLowerCase();
|
|
4570
|
+
let projectDirs = [];
|
|
4571
|
+
try {
|
|
4572
|
+
if (fs.existsSync(PROJECTS_DIR$1)) projectDirs = fs.readdirSync(PROJECTS_DIR$1);
|
|
4573
|
+
} catch {
|
|
4574
|
+
return [];
|
|
4575
|
+
}
|
|
4576
|
+
const hits = [];
|
|
4577
|
+
const liveFiles = /* @__PURE__ */ new Set();
|
|
4578
|
+
for (const dir of projectDirs) {
|
|
4579
|
+
const dirPath = path.join(PROJECTS_DIR$1, dir);
|
|
4580
|
+
let files;
|
|
4581
|
+
try {
|
|
4582
|
+
files = fs.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
|
|
4583
|
+
} catch {
|
|
4584
|
+
continue;
|
|
4585
|
+
}
|
|
4586
|
+
for (const file of files) {
|
|
4587
|
+
const path$1 = path.join(dirPath, file);
|
|
4588
|
+
liveFiles.add(path$1);
|
|
4589
|
+
const conv = searchableFor(path$1, file.slice(0, -".jsonl".length));
|
|
4590
|
+
let matchCount = 0;
|
|
4591
|
+
let snippet = null;
|
|
4592
|
+
for (const text of conv.messages) {
|
|
4593
|
+
const lower = text.toLowerCase();
|
|
4594
|
+
let idx = lower.indexOf(needle);
|
|
4595
|
+
while (idx !== -1) {
|
|
4596
|
+
if (!snippet) snippet = makeSnippet(text, idx, q.length);
|
|
4597
|
+
if (++matchCount >= SEARCH_MAX_PER_CONVERSATION) break;
|
|
4598
|
+
idx = lower.indexOf(needle, idx + needle.length);
|
|
4599
|
+
}
|
|
4600
|
+
if (matchCount >= SEARCH_MAX_PER_CONVERSATION) break;
|
|
4601
|
+
}
|
|
4602
|
+
if (matchCount > 0 && snippet) {
|
|
4603
|
+
hits.push({
|
|
4604
|
+
conversationId: conv.id,
|
|
4605
|
+
cwd: conv.cwd,
|
|
4606
|
+
lastActivityAt: conv.lastActivityAt,
|
|
4607
|
+
matchCount,
|
|
4608
|
+
snippet
|
|
4609
|
+
});
|
|
4610
|
+
}
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
for (const path2 of [...searchCache.keys()]) {
|
|
4614
|
+
if (!liveFiles.has(path2)) searchCache.delete(path2);
|
|
4615
|
+
}
|
|
4616
|
+
hits.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
|
|
4617
|
+
return hits.slice(0, SEARCH_MAX_RESULTS);
|
|
4618
|
+
}
|
|
4284
4619
|
const CREDENTIALS_PATH = path.join(os.homedir(), ".claude", ".credentials.json");
|
|
4285
4620
|
const USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
4286
4621
|
const OAUTH_BETA = "oauth-2025-04-20";
|
|
@@ -4357,216 +4692,74 @@ class UsageLimitsService {
|
|
|
4357
4692
|
};
|
|
4358
4693
|
}
|
|
4359
4694
|
}
|
|
4360
|
-
|
|
4361
|
-
const BLOCK_MS = 5 * 60 * 60 * 1e3;
|
|
4362
|
-
const HOUR_MS = 60 * 60 * 1e3;
|
|
4363
|
-
const PRICING = [
|
|
4364
|
-
{ match: /fable/, input: 10, output: 50 },
|
|
4365
|
-
{ match: /opus-4-[5-9]/, input: 5, output: 25 },
|
|
4366
|
-
{ match: /opus/, input: 15, output: 75 },
|
|
4367
|
-
{ match: /sonnet/, input: 3, output: 15 },
|
|
4368
|
-
{ match: /haiku-4/, input: 1, output: 5 },
|
|
4369
|
-
{ match: /3-5-haiku|haiku-3-5/, input: 0.8, output: 4 },
|
|
4370
|
-
{ match: /haiku/, input: 0.25, output: 1.25 }
|
|
4371
|
-
];
|
|
4372
|
-
function zeroTotals() {
|
|
4373
|
-
return { inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0, costUSD: 0 };
|
|
4374
|
-
}
|
|
4375
|
-
function add(t, e) {
|
|
4376
|
-
t.inputTokens += e.input;
|
|
4377
|
-
t.outputTokens += e.output;
|
|
4378
|
-
t.cacheWriteTokens += e.cacheWrite5m + e.cacheWrite1h;
|
|
4379
|
-
t.cacheReadTokens += e.cacheRead;
|
|
4380
|
-
t.costUSD += e.costUSD;
|
|
4381
|
-
}
|
|
4382
|
-
function localDay(at) {
|
|
4695
|
+
function localDay$1(at) {
|
|
4383
4696
|
const d = new Date(at);
|
|
4384
4697
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
4385
4698
|
const day = String(d.getDate()).padStart(2, "0");
|
|
4386
4699
|
return `${d.getFullYear()}-${m}-${day}`;
|
|
4387
4700
|
}
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
return (entry.input * price.input + entry.output * price.output + entry.cacheWrite5m * price.input * 1.25 + entry.cacheWrite1h * price.input * 2 + entry.cacheRead * price.input * 0.1) / 1e6;
|
|
4392
|
-
}
|
|
4393
|
-
function computeProjection(points, now) {
|
|
4394
|
-
if (points.length === 0) return null;
|
|
4395
|
-
points.sort((a, b) => a.at - b.at);
|
|
4396
|
-
const blocks = [];
|
|
4397
|
-
let cur = null;
|
|
4398
|
-
for (const p of points) {
|
|
4399
|
-
if (!cur || p.at >= cur.start + BLOCK_MS || p.at - cur.lastAt >= BLOCK_MS) {
|
|
4400
|
-
cur = { start: Math.floor(p.at / HOUR_MS) * HOUR_MS, firstAt: p.at, lastAt: p.at, tokens: 0 };
|
|
4401
|
-
blocks.push(cur);
|
|
4402
|
-
}
|
|
4403
|
-
cur.lastAt = p.at;
|
|
4404
|
-
cur.tokens += p.tokens;
|
|
4701
|
+
class BudgetAlerts {
|
|
4702
|
+
constructor(getWin2) {
|
|
4703
|
+
this.getWin = getWin2;
|
|
4405
4704
|
}
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4705
|
+
daily = { period: "", fired: /* @__PURE__ */ new Set() };
|
|
4706
|
+
monthly = { period: "", fired: /* @__PURE__ */ new Set() };
|
|
4707
|
+
/** Re-evaluate both budgets against the latest snapshot; fire any new alerts. */
|
|
4708
|
+
check(snap, settings) {
|
|
4709
|
+
const threshold = clampThreshold(settings.budgetAlertThreshold);
|
|
4710
|
+
const day = localDay$1(snap.updatedAt);
|
|
4711
|
+
this.checkBudget(this.daily, "daily", day, snap.today.costUSD, settings.dailyBudgetUSD, threshold);
|
|
4712
|
+
this.checkBudget(
|
|
4713
|
+
this.monthly,
|
|
4714
|
+
"monthly",
|
|
4715
|
+
day.slice(0, 7),
|
|
4716
|
+
snap.month.costUSD,
|
|
4717
|
+
settings.monthlyBudgetUSD,
|
|
4718
|
+
threshold
|
|
4719
|
+
);
|
|
4412
4720
|
}
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4721
|
+
checkBudget(state, scope, period, spend, budget, thresholdPct) {
|
|
4722
|
+
if (budget == null || budget <= 0) return;
|
|
4723
|
+
if (state.period !== period) {
|
|
4724
|
+
state.period = period;
|
|
4725
|
+
state.fired.clear();
|
|
4726
|
+
}
|
|
4727
|
+
if (spend <= 0) return;
|
|
4728
|
+
const thresholdValue = budget * thresholdPct / 100;
|
|
4729
|
+
if (spend >= budget) {
|
|
4730
|
+
if (!state.fired.has("limit")) {
|
|
4731
|
+
this.fire(scope, "limit", spend, budget);
|
|
4732
|
+
state.fired.add("limit");
|
|
4733
|
+
state.fired.add("threshold");
|
|
4734
|
+
}
|
|
4735
|
+
} else if (spend >= thresholdValue) {
|
|
4736
|
+
if (!state.fired.has("threshold")) {
|
|
4737
|
+
this.fire(scope, "threshold", spend, budget);
|
|
4738
|
+
state.fired.add("threshold");
|
|
4739
|
+
}
|
|
4422
4740
|
}
|
|
4423
4741
|
}
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
} catch {
|
|
4438
|
-
return [];
|
|
4439
|
-
}
|
|
4440
|
-
const entries = [];
|
|
4441
|
-
for (const line of raw.split("\n")) {
|
|
4442
|
-
if (!line.includes('"assistant"') || !line.includes('"usage"')) continue;
|
|
4443
|
-
let obj;
|
|
4444
|
-
try {
|
|
4445
|
-
obj = JSON.parse(line);
|
|
4446
|
-
} catch {
|
|
4447
|
-
continue;
|
|
4448
|
-
}
|
|
4449
|
-
if (obj.type !== "assistant") continue;
|
|
4450
|
-
const message = obj.message;
|
|
4451
|
-
const usage = message?.usage;
|
|
4452
|
-
const model = typeof message?.model === "string" ? message.model : "";
|
|
4453
|
-
if (!usage || !model || model === "<synthetic>") continue;
|
|
4454
|
-
const num = (v) => typeof v === "number" && isFinite(v) ? v : 0;
|
|
4455
|
-
const cacheCreation = usage.cache_creation;
|
|
4456
|
-
const totalWrite = num(usage.cache_creation_input_tokens);
|
|
4457
|
-
const write1h = num(cacheCreation?.ephemeral_1h_input_tokens);
|
|
4458
|
-
const write5m = cacheCreation ? num(cacheCreation.ephemeral_5m_input_tokens) : totalWrite;
|
|
4459
|
-
const at = Date.parse(typeof obj.timestamp === "string" ? obj.timestamp : "") || 0;
|
|
4460
|
-
const msgId = typeof message?.id === "string" ? message.id : obj.uuid ?? "";
|
|
4461
|
-
const entry = {
|
|
4462
|
-
key: `${msgId}:${typeof obj.requestId === "string" ? obj.requestId : ""}`,
|
|
4463
|
-
day: localDay(at),
|
|
4464
|
-
at,
|
|
4465
|
-
model,
|
|
4466
|
-
input: num(usage.input_tokens),
|
|
4467
|
-
output: num(usage.output_tokens),
|
|
4468
|
-
cacheWrite5m: write5m,
|
|
4469
|
-
cacheWrite1h: write1h,
|
|
4470
|
-
cacheRead: num(usage.cache_read_input_tokens),
|
|
4471
|
-
costUSD: 0
|
|
4472
|
-
};
|
|
4473
|
-
const precomputed = num(obj.costUSD);
|
|
4474
|
-
entry.costUSD = precomputed > 0 ? precomputed : computeCost(entry);
|
|
4475
|
-
entries.push(entry);
|
|
4742
|
+
fire(scope, level, spend, budget) {
|
|
4743
|
+
const win2 = this.getWin();
|
|
4744
|
+
const pct = Math.round(spend / budget * 100);
|
|
4745
|
+
const label = scope === "daily" ? "Daily" : "Monthly";
|
|
4746
|
+
const title = level === "limit" ? `${label} spend budget reached` : `${label} spend budget alert`;
|
|
4747
|
+
const body = `Spent ${fmtUSD(spend)} of your ${fmtUSD(budget)} ${scope} budget (${pct}%).`;
|
|
4748
|
+
if (!(win2?.isFocused() ?? false)) win2?.flashFrame(true);
|
|
4749
|
+
const notification = new electron.Notification({ title, body });
|
|
4750
|
+
notification.on("click", () => {
|
|
4751
|
+
win2?.show();
|
|
4752
|
+
win2?.focus();
|
|
4753
|
+
});
|
|
4754
|
+
notification.show();
|
|
4476
4755
|
}
|
|
4477
|
-
return entries;
|
|
4478
4756
|
}
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
const total = zeroTotals();
|
|
4486
|
-
const todayTotals = zeroTotals();
|
|
4487
|
-
const month = zeroTotals();
|
|
4488
|
-
const perModel = /* @__PURE__ */ new Map();
|
|
4489
|
-
const perProject = [];
|
|
4490
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4491
|
-
const liveFiles = /* @__PURE__ */ new Set();
|
|
4492
|
-
const points = [];
|
|
4493
|
-
let projectDirs = [];
|
|
4494
|
-
try {
|
|
4495
|
-
if (fs.existsSync(PROJECTS_DIR)) projectDirs = fs.readdirSync(PROJECTS_DIR);
|
|
4496
|
-
} catch {
|
|
4497
|
-
projectDirs = [];
|
|
4498
|
-
}
|
|
4499
|
-
for (const dir of projectDirs) {
|
|
4500
|
-
const dirPath = path.join(PROJECTS_DIR, dir);
|
|
4501
|
-
let files;
|
|
4502
|
-
try {
|
|
4503
|
-
files = fs.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
|
|
4504
|
-
} catch {
|
|
4505
|
-
continue;
|
|
4506
|
-
}
|
|
4507
|
-
const project = {
|
|
4508
|
-
dir,
|
|
4509
|
-
total: zeroTotals(),
|
|
4510
|
-
today: zeroTotals(),
|
|
4511
|
-
lastActivityAt: 0
|
|
4512
|
-
};
|
|
4513
|
-
for (const file of files) {
|
|
4514
|
-
const path$1 = path.join(dirPath, file);
|
|
4515
|
-
liveFiles.add(path$1);
|
|
4516
|
-
for (const entry of this.entriesFor(path$1)) {
|
|
4517
|
-
if (seen.has(entry.key)) continue;
|
|
4518
|
-
seen.add(entry.key);
|
|
4519
|
-
if (entry.at > 0) {
|
|
4520
|
-
points.push({
|
|
4521
|
-
at: entry.at,
|
|
4522
|
-
tokens: entry.input + entry.output + entry.cacheWrite5m + entry.cacheWrite1h + entry.cacheRead
|
|
4523
|
-
});
|
|
4524
|
-
}
|
|
4525
|
-
add(total, entry);
|
|
4526
|
-
add(project.total, entry);
|
|
4527
|
-
if (entry.day === today) {
|
|
4528
|
-
add(todayTotals, entry);
|
|
4529
|
-
add(project.today, entry);
|
|
4530
|
-
}
|
|
4531
|
-
if (entry.day.startsWith(monthPrefix)) add(month, entry);
|
|
4532
|
-
let m = perModel.get(entry.model);
|
|
4533
|
-
if (!m) perModel.set(entry.model, m = zeroTotals());
|
|
4534
|
-
add(m, entry);
|
|
4535
|
-
if (entry.at > project.lastActivityAt) project.lastActivityAt = entry.at;
|
|
4536
|
-
}
|
|
4537
|
-
}
|
|
4538
|
-
if (project.total.costUSD > 0 || project.total.outputTokens > 0) perProject.push(project);
|
|
4539
|
-
}
|
|
4540
|
-
for (const path2 of [...this.fileCache.keys()]) {
|
|
4541
|
-
if (!liveFiles.has(path2)) this.fileCache.delete(path2);
|
|
4542
|
-
}
|
|
4543
|
-
perProject.sort((a, b) => b.total.costUSD - a.total.costUSD);
|
|
4544
|
-
return {
|
|
4545
|
-
total,
|
|
4546
|
-
today: todayTotals,
|
|
4547
|
-
month,
|
|
4548
|
-
perProject,
|
|
4549
|
-
perModel: [...perModel.entries()].map(([model, totals]) => ({ model, totals })).sort((a, b) => b.totals.costUSD - a.totals.costUSD),
|
|
4550
|
-
projection: computeProjection(points, now),
|
|
4551
|
-
updatedAt: now
|
|
4552
|
-
};
|
|
4553
|
-
}
|
|
4554
|
-
entriesFor(path2) {
|
|
4555
|
-
let stat;
|
|
4556
|
-
try {
|
|
4557
|
-
stat = fs.statSync(path2);
|
|
4558
|
-
} catch {
|
|
4559
|
-
this.fileCache.delete(path2);
|
|
4560
|
-
return [];
|
|
4561
|
-
}
|
|
4562
|
-
const cached = this.fileCache.get(path2);
|
|
4563
|
-
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
4564
|
-
return cached.entries;
|
|
4565
|
-
}
|
|
4566
|
-
const entries = parseFile(path2);
|
|
4567
|
-
this.fileCache.set(path2, { mtimeMs: stat.mtimeMs, size: stat.size, entries });
|
|
4568
|
-
return entries;
|
|
4569
|
-
}
|
|
4757
|
+
function clampThreshold(pct) {
|
|
4758
|
+
if (typeof pct !== "number" || !isFinite(pct) || pct <= 0) return 80;
|
|
4759
|
+
return Math.min(100, pct);
|
|
4760
|
+
}
|
|
4761
|
+
function fmtUSD(v) {
|
|
4762
|
+
return `$${v.toFixed(2)}`;
|
|
4570
4763
|
}
|
|
4571
4764
|
const CONDUCTOR_ATTACH_SCOPE = "conductor";
|
|
4572
4765
|
function tokenize(template) {
|
|
@@ -4576,7 +4769,7 @@ function tokenize(template) {
|
|
|
4576
4769
|
while (m = re.exec(template)) tokens.push(m[1] ?? m[2]);
|
|
4577
4770
|
return tokens;
|
|
4578
4771
|
}
|
|
4579
|
-
function registerIpc(sessions, fs2, persistence2, sentinels, features, autoExpand, conductor, factory, agents, tokenEff, gamification, getWin2) {
|
|
4772
|
+
function registerIpc(sessions, fs2, persistence2, sentinels, features, autoExpand, conductor, factory, agents, tokenEff, gamification, usage, getWin2) {
|
|
4580
4773
|
const rootOf = (id) => {
|
|
4581
4774
|
const config = sessions.getConfig(id);
|
|
4582
4775
|
if (!config) throw new Error(`Unknown session: ${id}`);
|
|
@@ -4634,6 +4827,7 @@ function registerIpc(sessions, fs2, persistence2, sentinels, features, autoExpan
|
|
|
4634
4827
|
(_e, sessionId, path2) => sessions.getGitFileDiff(sessionId, path2)
|
|
4635
4828
|
);
|
|
4636
4829
|
electron.ipcMain.handle("git:branches", (_e, sessionId) => sessions.listBranches(sessionId));
|
|
4830
|
+
electron.ipcMain.handle("git:branchDiff", (_e, sessionId) => sessions.getBranchDiff(sessionId));
|
|
4637
4831
|
electron.ipcMain.handle(
|
|
4638
4832
|
"checkpoint:create",
|
|
4639
4833
|
(_e, sessionId, label) => sessions.createCheckpoint(sessionId, label)
|
|
@@ -4642,6 +4836,10 @@ function registerIpc(sessions, fs2, persistence2, sentinels, features, autoExpan
|
|
|
4642
4836
|
"checkpoint:list",
|
|
4643
4837
|
(_e, sessionId) => sessions.listCheckpoints(sessionId)
|
|
4644
4838
|
);
|
|
4839
|
+
electron.ipcMain.handle(
|
|
4840
|
+
"checkpoint:diff",
|
|
4841
|
+
(_e, sessionId, id) => sessions.getCheckpointDiff(sessionId, id)
|
|
4842
|
+
);
|
|
4645
4843
|
electron.ipcMain.handle(
|
|
4646
4844
|
"checkpoint:restore",
|
|
4647
4845
|
(_e, sessionId, id) => sessions.restoreCheckpoint(sessionId, id)
|
|
@@ -4913,10 +5111,16 @@ function registerIpc(sessions, fs2, persistence2, sentinels, features, autoExpan
|
|
|
4913
5111
|
"tokenEff:detectTools",
|
|
4914
5112
|
(_e, refresh) => tokenEff.detectTools(refresh ?? false)
|
|
4915
5113
|
);
|
|
4916
|
-
const
|
|
4917
|
-
electron.ipcMain.handle("usage:get", () =>
|
|
5114
|
+
const budgetAlerts = new BudgetAlerts(getWin2);
|
|
5115
|
+
electron.ipcMain.handle("usage:get", () => {
|
|
5116
|
+
const snap = usage.snapshot();
|
|
5117
|
+
budgetAlerts.check(snap, persistence2.state.settings);
|
|
5118
|
+
return snap;
|
|
5119
|
+
});
|
|
4918
5120
|
const usageLimits = new UsageLimitsService();
|
|
4919
5121
|
electron.ipcMain.handle("usage:limits", () => usageLimits.limits());
|
|
5122
|
+
electron.ipcMain.handle("conversations:list", (_e, folder) => listConversations(folder));
|
|
5123
|
+
electron.ipcMain.handle("conversations:search", (_e, query) => searchConversations(query));
|
|
4920
5124
|
electron.ipcMain.handle(
|
|
4921
5125
|
"transcript:export",
|
|
4922
5126
|
async (_e, sessionId, fileName, content) => {
|
|
@@ -4982,7 +5186,10 @@ const DEFAULT_SETTINGS = {
|
|
|
4982
5186
|
accentColor: null,
|
|
4983
5187
|
gamificationEnabled: true,
|
|
4984
5188
|
gamificationReduceMotion: false,
|
|
4985
|
-
gamificationSound: false
|
|
5189
|
+
gamificationSound: false,
|
|
5190
|
+
dailyBudgetUSD: null,
|
|
5191
|
+
monthlyBudgetUSD: null,
|
|
5192
|
+
budgetAlertThreshold: 80
|
|
4986
5193
|
};
|
|
4987
5194
|
const DEFAULT_CATEGORIES = [
|
|
4988
5195
|
{
|
|
@@ -5587,6 +5794,8 @@ const ACTION_SHELL_READY_MS = 1200;
|
|
|
5587
5794
|
const CLAUDE_SUBMIT_DELAY_MS = 300;
|
|
5588
5795
|
const QUEUE_IDLE_DELAY_MS = 3e3;
|
|
5589
5796
|
const AUTO_COMPLETE_IDLE_DELAY_MS = 9e4;
|
|
5797
|
+
const PLAN_APPROVAL_RE = /(?:auto-accept edits|keep planning|manually approve)/i;
|
|
5798
|
+
const PLAN_ACCEPT_DELAY_MS = 600;
|
|
5590
5799
|
const WATCHDOG_TICK_MS = 5e3;
|
|
5591
5800
|
const SCROLLBACK_DIVIDER = "\r\n\x1B[0m\x1B[2m── restored from previous session ──\x1B[0m\r\n\r\n";
|
|
5592
5801
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -5635,6 +5844,10 @@ class SessionManager {
|
|
|
5635
5844
|
autoCompleteTimers = /* @__PURE__ */ new Map();
|
|
5636
5845
|
/** Worktree session ids with an auto-complete action in flight (fires once). */
|
|
5637
5846
|
autoCompleteInFlight = /* @__PURE__ */ new Set();
|
|
5847
|
+
/** Worktree session ids whose current plan-approval prompt Maestro has already
|
|
5848
|
+
* answered — re-armed when claude goes back to working, so each distinct plan
|
|
5849
|
+
* is auto-accepted exactly once. */
|
|
5850
|
+
planAccepted = /* @__PURE__ */ new Set();
|
|
5638
5851
|
/** On-disk tail of each terminal's output, replayed on app restart. */
|
|
5639
5852
|
scrollback = new ScrollbackStore();
|
|
5640
5853
|
/**
|
|
@@ -5699,14 +5912,15 @@ class SessionManager {
|
|
|
5699
5912
|
claudeArgs: [],
|
|
5700
5913
|
startMode: "continue"
|
|
5701
5914
|
};
|
|
5915
|
+
const terminals = opts?.terminals ?? [claudeTerminal];
|
|
5702
5916
|
const config = {
|
|
5703
5917
|
id: crypto.randomUUID(),
|
|
5704
5918
|
name: opts?.name ?? path.basename(folder) ?? folder,
|
|
5705
5919
|
folder,
|
|
5706
5920
|
color: opts?.color ?? null,
|
|
5707
5921
|
order: Math.max(0, ...this.state.sessions.map((s) => s.order + 1)),
|
|
5708
|
-
terminals
|
|
5709
|
-
activeTerminalId:
|
|
5922
|
+
terminals,
|
|
5923
|
+
activeTerminalId: terminals[0]?.id ?? null,
|
|
5710
5924
|
expandedPaths: [],
|
|
5711
5925
|
categoryId: opts?.categoryId ?? null
|
|
5712
5926
|
};
|
|
@@ -5786,6 +6000,19 @@ class SessionManager {
|
|
|
5786
6000
|
if (!config) return { branches: [], current: null, defaultBranch: null };
|
|
5787
6001
|
return listBranches(config.folder);
|
|
5788
6002
|
}
|
|
6003
|
+
/**
|
|
6004
|
+
* Cumulative diff of a worktree task's branch against its base branch (their
|
|
6005
|
+
* merge-base), for the read-only "Review changes" tab. Returns an empty diff
|
|
6006
|
+
* for a non-worktree session — the UI only offers this on worktree tasks.
|
|
6007
|
+
*/
|
|
6008
|
+
async getBranchDiff(sessionId) {
|
|
6009
|
+
const config = this.getConfig(sessionId);
|
|
6010
|
+
const wt = config?.worktree;
|
|
6011
|
+
if (!config || !wt) {
|
|
6012
|
+
return { diff: "", truncated: false, files: [], branch: "", baseBranch: "" };
|
|
6013
|
+
}
|
|
6014
|
+
return branchDiff(config.folder, wt.branch, wt.baseBranch);
|
|
6015
|
+
}
|
|
5789
6016
|
/**
|
|
5790
6017
|
* Initialize a git repository in a session's folder (so a non-repo session can
|
|
5791
6018
|
* host parallel tasks). Returns the resulting git facts; throws git's message
|
|
@@ -5816,6 +6043,12 @@ class SessionManager {
|
|
|
5816
6043
|
if (!config) return [];
|
|
5817
6044
|
return listCheckpoints(config.folder);
|
|
5818
6045
|
}
|
|
6046
|
+
/** Read-only diff between a checkpoint and the current working tree. */
|
|
6047
|
+
async getCheckpointDiff(sessionId, id) {
|
|
6048
|
+
const config = this.getConfig(sessionId);
|
|
6049
|
+
if (!config) return { diff: "", binary: false, truncated: false };
|
|
6050
|
+
return checkpointDiff(config.folder, id);
|
|
6051
|
+
}
|
|
5819
6052
|
/** Restore a session's working tree back to a checkpoint (guarded, reversible). */
|
|
5820
6053
|
async restoreCheckpoint(sessionId, id) {
|
|
5821
6054
|
const config = this.getConfig(sessionId);
|
|
@@ -5868,13 +6101,15 @@ class SessionManager {
|
|
|
5868
6101
|
console.error("copyWorktreeIncludes failed", err);
|
|
5869
6102
|
}
|
|
5870
6103
|
}
|
|
6104
|
+
const claudeArgs = [];
|
|
6105
|
+
if (opts.plan) claudeArgs.push("--permission-mode", "plan");
|
|
6106
|
+
if (opts.model) claudeArgs.push("--model", opts.model);
|
|
5871
6107
|
const claudeTerminal = {
|
|
5872
6108
|
id: crypto.randomUUID(),
|
|
5873
6109
|
kind: "claude",
|
|
5874
6110
|
title: "claude",
|
|
5875
6111
|
order: 0,
|
|
5876
|
-
|
|
5877
|
-
claudeArgs: opts.model ? ["--model", opts.model] : [],
|
|
6112
|
+
claudeArgs,
|
|
5878
6113
|
startMode: "fresh"
|
|
5879
6114
|
};
|
|
5880
6115
|
const config = {
|
|
@@ -5895,7 +6130,9 @@ class SessionManager {
|
|
|
5895
6130
|
// Default to direct merge; only persist a PR/auto choice when set, so
|
|
5896
6131
|
// existing tasks and the common case stay exactly as before.
|
|
5897
6132
|
...opts.completion && opts.completion !== "merge" ? { completion: opts.completion } : {},
|
|
5898
|
-
...opts.autoComplete ? { autoComplete: true } : {}
|
|
6133
|
+
...opts.autoComplete ? { autoComplete: true } : {},
|
|
6134
|
+
...opts.plan ? { plan: true } : {},
|
|
6135
|
+
...opts.plan && opts.autoAcceptPlan ? { autoAcceptPlan: true } : {}
|
|
5899
6136
|
}
|
|
5900
6137
|
};
|
|
5901
6138
|
this.state.sessions.push(config);
|
|
@@ -6104,10 +6341,10 @@ ${commit.output}` };
|
|
|
6104
6341
|
// ---------- auto-complete (auto-merge / auto-PR when claude finishes) --------
|
|
6105
6342
|
/**
|
|
6106
6343
|
* (Re)start the auto-complete idle countdown for a worktree task whose claude
|
|
6107
|
-
* just
|
|
6108
|
-
* boot-time
|
|
6109
|
-
* draining or an action is already in flight / done. The fire
|
|
6110
|
-
* re-checks everything, so a terminal that wakes mid-countdown is safe.
|
|
6344
|
+
* just settled (done/idle). Only arms once the task's claude has actually
|
|
6345
|
+
* worked (so the boot-time settle never counts), and never while a prompt
|
|
6346
|
+
* queue is still draining or an action is already in flight / done. The fire
|
|
6347
|
+
* handler re-checks everything, so a terminal that wakes mid-countdown is safe.
|
|
6111
6348
|
*/
|
|
6112
6349
|
scheduleAutoComplete(sessionId) {
|
|
6113
6350
|
const config = this.getConfig(sessionId);
|
|
@@ -6131,6 +6368,28 @@ ${commit.output}` };
|
|
|
6131
6368
|
if (timer) clearTimeout(timer);
|
|
6132
6369
|
this.autoCompleteTimers.delete(sessionId);
|
|
6133
6370
|
}
|
|
6371
|
+
/**
|
|
6372
|
+
* Auto-approve the plan-mode prompt for an opted-in task. Runs once per
|
|
6373
|
+
* attention episode (StatusDetector doesn't re-emit a held status, so a single
|
|
6374
|
+
* timer per episode is enough). The regex is evaluated only AFTER a short
|
|
6375
|
+
* settle — the terminal bell that raises attention can land a beat before the
|
|
6376
|
+
* menu text finishes rendering — and only then, if the approval menu is
|
|
6377
|
+
* actually on screen and not already handled, Maestro presses Enter to take
|
|
6378
|
+
* the default option ("Yes, and auto-accept edits"). Non-plan attention
|
|
6379
|
+
* prompts simply don't match the regex and are left for the user.
|
|
6380
|
+
* `planAccepted` is cleared when claude resumes working, so a later plan (it
|
|
6381
|
+
* can re-enter plan mode) is accepted too.
|
|
6382
|
+
*/
|
|
6383
|
+
maybeAutoAcceptPlan(sessionId, terminalId) {
|
|
6384
|
+
if (this.planAccepted.has(sessionId)) return;
|
|
6385
|
+
setTimeout(() => {
|
|
6386
|
+
const live = this.ptys.get(terminalId);
|
|
6387
|
+
if (!live?.alive || this.planAccepted.has(sessionId)) return;
|
|
6388
|
+
if (!PLAN_APPROVAL_RE.test(live.detector.recentText)) return;
|
|
6389
|
+
this.planAccepted.add(sessionId);
|
|
6390
|
+
live.write("\r");
|
|
6391
|
+
}, PLAN_ACCEPT_DELAY_MS);
|
|
6392
|
+
}
|
|
6134
6393
|
/**
|
|
6135
6394
|
* Run a task's chosen completion automatically. Re-checks the gates that may
|
|
6136
6395
|
* have changed during the countdown (still a worktree task, claude alive and
|
|
@@ -6146,7 +6405,8 @@ ${commit.output}` };
|
|
|
6146
6405
|
if (config.promptQueue?.length) return;
|
|
6147
6406
|
const terminal = this.claudeTargetTerminal(config);
|
|
6148
6407
|
const pty2 = terminal ? this.ptys.get(terminal.id) : null;
|
|
6149
|
-
|
|
6408
|
+
const settled = pty2?.detector.current === "done" || pty2?.detector.current === "idle";
|
|
6409
|
+
if (!pty2?.alive || !settled) return;
|
|
6150
6410
|
const dirty = await dirtyCount(config.folder) ?? 0;
|
|
6151
6411
|
const ahead = await aheadCount(wt.baseFolder, wt.baseBranch, wt.branch) ?? 0;
|
|
6152
6412
|
if (dirty === 0 && ahead === 0) return;
|
|
@@ -6264,6 +6524,7 @@ ${err.message}`
|
|
|
6264
6524
|
this.tokenEff.clearApplied(sessionId);
|
|
6265
6525
|
this.worktreeWorked.delete(sessionId);
|
|
6266
6526
|
this.autoCompleteInFlight.delete(sessionId);
|
|
6527
|
+
this.planAccepted.delete(sessionId);
|
|
6267
6528
|
this.state.sessions = this.state.sessions.filter((s) => s.id !== sessionId);
|
|
6268
6529
|
if (this.state.activeSessionId === sessionId) this.state.activeSessionId = null;
|
|
6269
6530
|
this.persistence.scheduleSave();
|
|
@@ -6634,9 +6895,13 @@ ${err.message}`
|
|
|
6634
6895
|
if (status === "done" || status === "idle") this.scheduleQueueDispatch(config.id);
|
|
6635
6896
|
if (config.worktree?.autoComplete) {
|
|
6636
6897
|
if (status === "working") this.worktreeWorked.add(config.id);
|
|
6637
|
-
if (status === "idle") this.scheduleAutoComplete(config.id);
|
|
6898
|
+
if (status === "done" || status === "idle") this.scheduleAutoComplete(config.id);
|
|
6638
6899
|
else this.clearAutoCompleteTimer(config.id);
|
|
6639
6900
|
}
|
|
6901
|
+
if (config.worktree?.autoAcceptPlan) {
|
|
6902
|
+
if (status === "working") this.planAccepted.delete(config.id);
|
|
6903
|
+
if (status === "needs-attention") this.maybeAutoAcceptPlan(config.id, terminalId);
|
|
6904
|
+
}
|
|
6640
6905
|
if (status !== "needs-attention") return;
|
|
6641
6906
|
const focused = win2?.isFocused() ?? false;
|
|
6642
6907
|
const isActive = this.state.activeSessionId === config.id && config.activeTerminalId === terminalId;
|
|
@@ -7573,6 +7838,217 @@ class TokenEfficiencyService {
|
|
|
7573
7838
|
}
|
|
7574
7839
|
}
|
|
7575
7840
|
}
|
|
7841
|
+
const PROJECTS_DIR = path.join(os.homedir(), ".claude", "projects");
|
|
7842
|
+
const BLOCK_MS = 5 * 60 * 60 * 1e3;
|
|
7843
|
+
const HOUR_MS = 60 * 60 * 1e3;
|
|
7844
|
+
const PRICING = [
|
|
7845
|
+
{ match: /fable/, input: 10, output: 50 },
|
|
7846
|
+
{ match: /opus-4-[5-9]/, input: 5, output: 25 },
|
|
7847
|
+
{ match: /opus/, input: 15, output: 75 },
|
|
7848
|
+
{ match: /sonnet/, input: 3, output: 15 },
|
|
7849
|
+
{ match: /haiku-4/, input: 1, output: 5 },
|
|
7850
|
+
{ match: /3-5-haiku|haiku-3-5/, input: 0.8, output: 4 },
|
|
7851
|
+
{ match: /haiku/, input: 0.25, output: 1.25 }
|
|
7852
|
+
];
|
|
7853
|
+
function zeroTotals() {
|
|
7854
|
+
return { inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0, costUSD: 0 };
|
|
7855
|
+
}
|
|
7856
|
+
function add(t, e) {
|
|
7857
|
+
t.inputTokens += e.input;
|
|
7858
|
+
t.outputTokens += e.output;
|
|
7859
|
+
t.cacheWriteTokens += e.cacheWrite5m + e.cacheWrite1h;
|
|
7860
|
+
t.cacheReadTokens += e.cacheRead;
|
|
7861
|
+
t.costUSD += e.costUSD;
|
|
7862
|
+
}
|
|
7863
|
+
function localDay(at) {
|
|
7864
|
+
const d = new Date(at);
|
|
7865
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
7866
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
7867
|
+
return `${d.getFullYear()}-${m}-${day}`;
|
|
7868
|
+
}
|
|
7869
|
+
function computeCost(entry) {
|
|
7870
|
+
const price = PRICING.find((p) => p.match.test(entry.model));
|
|
7871
|
+
if (!price) return 0;
|
|
7872
|
+
return (entry.input * price.input + entry.output * price.output + entry.cacheWrite5m * price.input * 1.25 + entry.cacheWrite1h * price.input * 2 + entry.cacheRead * price.input * 0.1) / 1e6;
|
|
7873
|
+
}
|
|
7874
|
+
function computeProjection(points, now) {
|
|
7875
|
+
if (points.length === 0) return null;
|
|
7876
|
+
points.sort((a, b) => a.at - b.at);
|
|
7877
|
+
const blocks = [];
|
|
7878
|
+
let cur = null;
|
|
7879
|
+
for (const p of points) {
|
|
7880
|
+
if (!cur || p.at >= cur.start + BLOCK_MS || p.at - cur.lastAt >= BLOCK_MS) {
|
|
7881
|
+
cur = { start: Math.floor(p.at / HOUR_MS) * HOUR_MS, firstAt: p.at, lastAt: p.at, tokens: 0 };
|
|
7882
|
+
blocks.push(cur);
|
|
7883
|
+
}
|
|
7884
|
+
cur.lastAt = p.at;
|
|
7885
|
+
cur.tokens += p.tokens;
|
|
7886
|
+
}
|
|
7887
|
+
const last = blocks[blocks.length - 1];
|
|
7888
|
+
if (now < last.start || now >= last.start + BLOCK_MS) return null;
|
|
7889
|
+
const blockEndAt = last.start + BLOCK_MS;
|
|
7890
|
+
let maxBlockTokens = 0;
|
|
7891
|
+
for (const b of blocks) {
|
|
7892
|
+
if (b !== last && b.tokens > maxBlockTokens) maxBlockTokens = b.tokens;
|
|
7893
|
+
}
|
|
7894
|
+
const elapsedMin = Math.max(1, (now - last.firstAt) / 6e4);
|
|
7895
|
+
const tokensPerMin = last.tokens / elapsedMin;
|
|
7896
|
+
let runsOutAt = null;
|
|
7897
|
+
if (maxBlockTokens > 0) {
|
|
7898
|
+
if (last.tokens >= maxBlockTokens) {
|
|
7899
|
+
runsOutAt = now;
|
|
7900
|
+
} else if (tokensPerMin > 0) {
|
|
7901
|
+
const eta = now + (maxBlockTokens - last.tokens) / tokensPerMin * 6e4;
|
|
7902
|
+
if (eta < blockEndAt) runsOutAt = eta;
|
|
7903
|
+
}
|
|
7904
|
+
}
|
|
7905
|
+
return {
|
|
7906
|
+
blockStartAt: last.start,
|
|
7907
|
+
blockEndAt,
|
|
7908
|
+
blockTokens: last.tokens,
|
|
7909
|
+
maxBlockTokens,
|
|
7910
|
+
tokensPerMin,
|
|
7911
|
+
runsOutAt
|
|
7912
|
+
};
|
|
7913
|
+
}
|
|
7914
|
+
function parseFile(path2) {
|
|
7915
|
+
let raw;
|
|
7916
|
+
try {
|
|
7917
|
+
raw = fs.readFileSync(path2, "utf8");
|
|
7918
|
+
} catch {
|
|
7919
|
+
return [];
|
|
7920
|
+
}
|
|
7921
|
+
const entries = [];
|
|
7922
|
+
for (const line of raw.split("\n")) {
|
|
7923
|
+
if (!line.includes('"assistant"') || !line.includes('"usage"')) continue;
|
|
7924
|
+
let obj;
|
|
7925
|
+
try {
|
|
7926
|
+
obj = JSON.parse(line);
|
|
7927
|
+
} catch {
|
|
7928
|
+
continue;
|
|
7929
|
+
}
|
|
7930
|
+
if (obj.type !== "assistant") continue;
|
|
7931
|
+
const message = obj.message;
|
|
7932
|
+
const usage = message?.usage;
|
|
7933
|
+
const model = typeof message?.model === "string" ? message.model : "";
|
|
7934
|
+
if (!usage || !model || model === "<synthetic>") continue;
|
|
7935
|
+
const num = (v) => typeof v === "number" && isFinite(v) ? v : 0;
|
|
7936
|
+
const cacheCreation = usage.cache_creation;
|
|
7937
|
+
const totalWrite = num(usage.cache_creation_input_tokens);
|
|
7938
|
+
const write1h = num(cacheCreation?.ephemeral_1h_input_tokens);
|
|
7939
|
+
const write5m = cacheCreation ? num(cacheCreation.ephemeral_5m_input_tokens) : totalWrite;
|
|
7940
|
+
const at = Date.parse(typeof obj.timestamp === "string" ? obj.timestamp : "") || 0;
|
|
7941
|
+
const msgId = typeof message?.id === "string" ? message.id : obj.uuid ?? "";
|
|
7942
|
+
const entry = {
|
|
7943
|
+
key: `${msgId}:${typeof obj.requestId === "string" ? obj.requestId : ""}`,
|
|
7944
|
+
day: localDay(at),
|
|
7945
|
+
at,
|
|
7946
|
+
model,
|
|
7947
|
+
input: num(usage.input_tokens),
|
|
7948
|
+
output: num(usage.output_tokens),
|
|
7949
|
+
cacheWrite5m: write5m,
|
|
7950
|
+
cacheWrite1h: write1h,
|
|
7951
|
+
cacheRead: num(usage.cache_read_input_tokens),
|
|
7952
|
+
costUSD: 0
|
|
7953
|
+
};
|
|
7954
|
+
const precomputed = num(obj.costUSD);
|
|
7955
|
+
entry.costUSD = precomputed > 0 ? precomputed : computeCost(entry);
|
|
7956
|
+
entries.push(entry);
|
|
7957
|
+
}
|
|
7958
|
+
return entries;
|
|
7959
|
+
}
|
|
7960
|
+
class UsageService {
|
|
7961
|
+
fileCache = /* @__PURE__ */ new Map();
|
|
7962
|
+
snapshot() {
|
|
7963
|
+
const now = Date.now();
|
|
7964
|
+
const today = localDay(now);
|
|
7965
|
+
const monthPrefix = today.slice(0, 7);
|
|
7966
|
+
const total = zeroTotals();
|
|
7967
|
+
const todayTotals = zeroTotals();
|
|
7968
|
+
const month = zeroTotals();
|
|
7969
|
+
const perModel = /* @__PURE__ */ new Map();
|
|
7970
|
+
const perProject = [];
|
|
7971
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7972
|
+
const liveFiles = /* @__PURE__ */ new Set();
|
|
7973
|
+
const points = [];
|
|
7974
|
+
let projectDirs = [];
|
|
7975
|
+
try {
|
|
7976
|
+
if (fs.existsSync(PROJECTS_DIR)) projectDirs = fs.readdirSync(PROJECTS_DIR);
|
|
7977
|
+
} catch {
|
|
7978
|
+
projectDirs = [];
|
|
7979
|
+
}
|
|
7980
|
+
for (const dir of projectDirs) {
|
|
7981
|
+
const dirPath = path.join(PROJECTS_DIR, dir);
|
|
7982
|
+
let files;
|
|
7983
|
+
try {
|
|
7984
|
+
files = fs.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
|
|
7985
|
+
} catch {
|
|
7986
|
+
continue;
|
|
7987
|
+
}
|
|
7988
|
+
const project = {
|
|
7989
|
+
dir,
|
|
7990
|
+
total: zeroTotals(),
|
|
7991
|
+
today: zeroTotals(),
|
|
7992
|
+
lastActivityAt: 0
|
|
7993
|
+
};
|
|
7994
|
+
for (const file of files) {
|
|
7995
|
+
const path$1 = path.join(dirPath, file);
|
|
7996
|
+
liveFiles.add(path$1);
|
|
7997
|
+
for (const entry of this.entriesFor(path$1)) {
|
|
7998
|
+
if (seen.has(entry.key)) continue;
|
|
7999
|
+
seen.add(entry.key);
|
|
8000
|
+
if (entry.at > 0) {
|
|
8001
|
+
points.push({
|
|
8002
|
+
at: entry.at,
|
|
8003
|
+
tokens: entry.input + entry.output + entry.cacheWrite5m + entry.cacheWrite1h + entry.cacheRead
|
|
8004
|
+
});
|
|
8005
|
+
}
|
|
8006
|
+
add(total, entry);
|
|
8007
|
+
add(project.total, entry);
|
|
8008
|
+
if (entry.day === today) {
|
|
8009
|
+
add(todayTotals, entry);
|
|
8010
|
+
add(project.today, entry);
|
|
8011
|
+
}
|
|
8012
|
+
if (entry.day.startsWith(monthPrefix)) add(month, entry);
|
|
8013
|
+
let m = perModel.get(entry.model);
|
|
8014
|
+
if (!m) perModel.set(entry.model, m = zeroTotals());
|
|
8015
|
+
add(m, entry);
|
|
8016
|
+
if (entry.at > project.lastActivityAt) project.lastActivityAt = entry.at;
|
|
8017
|
+
}
|
|
8018
|
+
}
|
|
8019
|
+
if (project.total.costUSD > 0 || project.total.outputTokens > 0) perProject.push(project);
|
|
8020
|
+
}
|
|
8021
|
+
for (const path2 of [...this.fileCache.keys()]) {
|
|
8022
|
+
if (!liveFiles.has(path2)) this.fileCache.delete(path2);
|
|
8023
|
+
}
|
|
8024
|
+
perProject.sort((a, b) => b.total.costUSD - a.total.costUSD);
|
|
8025
|
+
return {
|
|
8026
|
+
total,
|
|
8027
|
+
today: todayTotals,
|
|
8028
|
+
month,
|
|
8029
|
+
perProject,
|
|
8030
|
+
perModel: [...perModel.entries()].map(([model, totals]) => ({ model, totals })).sort((a, b) => b.totals.costUSD - a.totals.costUSD),
|
|
8031
|
+
projection: computeProjection(points, now),
|
|
8032
|
+
updatedAt: now
|
|
8033
|
+
};
|
|
8034
|
+
}
|
|
8035
|
+
entriesFor(path2) {
|
|
8036
|
+
let stat;
|
|
8037
|
+
try {
|
|
8038
|
+
stat = fs.statSync(path2);
|
|
8039
|
+
} catch {
|
|
8040
|
+
this.fileCache.delete(path2);
|
|
8041
|
+
return [];
|
|
8042
|
+
}
|
|
8043
|
+
const cached = this.fileCache.get(path2);
|
|
8044
|
+
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
8045
|
+
return cached.entries;
|
|
8046
|
+
}
|
|
8047
|
+
const entries = parseFile(path2);
|
|
8048
|
+
this.fileCache.set(path2, { mtimeMs: stat.mtimeMs, size: stat.size, entries });
|
|
8049
|
+
return entries;
|
|
8050
|
+
}
|
|
8051
|
+
}
|
|
7576
8052
|
let win = null;
|
|
7577
8053
|
const getWin = () => win;
|
|
7578
8054
|
const persistence = new Persistence();
|
|
@@ -7654,7 +8130,11 @@ if (!gotLock) {
|
|
|
7654
8130
|
const conductor = new ConductorService(persistence, sessions, features, autoExpand, getWin);
|
|
7655
8131
|
const factory = new FactoryService(getWin, emitGame);
|
|
7656
8132
|
const agentRegistry = new AgentRegistryService(persistence, getWin);
|
|
7657
|
-
const
|
|
8133
|
+
const usage = new UsageService();
|
|
8134
|
+
const gamification = new GamificationService(getWin, () => {
|
|
8135
|
+
const t = usage.snapshot().total;
|
|
8136
|
+
return t.inputTokens + t.outputTokens;
|
|
8137
|
+
});
|
|
7658
8138
|
gameBus.on("game", (e) => gamification.award(e));
|
|
7659
8139
|
registerIpc(
|
|
7660
8140
|
sessions,
|
|
@@ -7668,6 +8148,7 @@ if (!gotLock) {
|
|
|
7668
8148
|
agentRegistry,
|
|
7669
8149
|
tokenEff,
|
|
7670
8150
|
gamification,
|
|
8151
|
+
usage,
|
|
7671
8152
|
getWin
|
|
7672
8153
|
);
|
|
7673
8154
|
createWindow();
|
|
@@ -7677,6 +8158,7 @@ if (!gotLock) {
|
|
|
7677
8158
|
autoExpand.start();
|
|
7678
8159
|
tokenEff.start();
|
|
7679
8160
|
factory.start();
|
|
8161
|
+
gamification.start();
|
|
7680
8162
|
conductor.onTurnComplete((messages) => {
|
|
7681
8163
|
factory.considerConversation(messages);
|
|
7682
8164
|
emitGame({ type: "conductor.turn" });
|