claude-maestro 0.1.19 → 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 +1119 -274
- package/out/preload/index.js +7 -0
- package/out/renderer/assets/{index-9AHdXE8U.js → index-BDJGybQo.js} +2 -2
- package/out/renderer/assets/{index-CVWvgy2Y.js → index-BDxfkk76.js} +2 -2
- package/out/renderer/assets/{index-C0rsWi9C.js → index-BNIPHMhg.js} +2 -2
- package/out/renderer/assets/{index-JMVyecfQ.js → index-BPiKmKfX.js} +5 -5
- package/out/renderer/assets/{index-C479DZmL.js → index-Bf87-cF0.js} +5 -5
- package/out/renderer/assets/{index-CNNAMsV1.js → index-Bm6BMufC.js} +2 -2
- package/out/renderer/assets/{index-LW-gCnC-.js → index-Bw6wDwNB.js} +2 -2
- package/out/renderer/assets/{index-1Z03T0zz.js → index-C2wfbMG1.js} +2 -2
- package/out/renderer/assets/{index-DI2ly48w.js → index-C3_9l5Zo.js} +2 -2
- package/out/renderer/assets/{index-Dhxn3JIv.js → index-CIAp39oC.js} +2 -2
- package/out/renderer/assets/{index-Dgaj6c_K.css → index-CTyPr1hG.css} +1384 -104
- package/out/renderer/assets/{index-B59uuZRU.js → index-CYo0nynQ.js} +4 -4
- package/out/renderer/assets/{index-D9GPva9-.js → index-CZmL3oq-.js} +5 -5
- package/out/renderer/assets/{index-Bg4ondS2.js → index-CdG7xnB7.js} +2 -2
- package/out/renderer/assets/{index-CoyUYEik.js → index-Cfvyl_8T.js} +3 -3
- package/out/renderer/assets/{index-DJwKAmOm.js → index-Ck4WZgFA.js} +2 -2
- package/out/renderer/assets/{index-jAA5WJm3.js → index-D9lxbtli.js} +5 -5
- package/out/renderer/assets/{index-CZP8wVw-.js → index-DD6EoqPp.js} +2 -2
- package/out/renderer/assets/{index-CWk6CwGd.js → index-DGNONcNh.js} +3 -3
- package/out/renderer/assets/{index-BkOzhsuz.js → index-DroXAl3A.js} +1 -1
- package/out/renderer/assets/{index-CuHjzw7d.js → index-DzjUOrFM.js} +5 -5
- package/out/renderer/assets/{index-CTxGDYbk.js → index-Uh6FxvAQ.js} +6157 -3651
- package/out/renderer/assets/{index-Cq5xQaOf.js → index-iIulTEbp.js} +5 -5
- package/out/renderer/assets/{index-CXeHg_Qc.js → index-kJ0KF5bI.js} +2 -2
- package/out/renderer/index.html +2 -2
- package/package.json +2 -1
package/out/main/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const electron = require("electron");
|
|
3
|
+
const events = require("events");
|
|
3
4
|
const path = require("path");
|
|
4
5
|
const fs = require("fs");
|
|
5
6
|
const os = require("os");
|
|
@@ -462,6 +463,12 @@ async function gitChangedFiles(folder) {
|
|
|
462
463
|
}
|
|
463
464
|
}
|
|
464
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
|
+
}
|
|
465
472
|
async function gitFileDiff(folder, path2) {
|
|
466
473
|
const empty = { diff: "", binary: false, truncated: false };
|
|
467
474
|
try {
|
|
@@ -475,11 +482,47 @@ async function gitFileDiff(folder, path2) {
|
|
|
475
482
|
} else {
|
|
476
483
|
res = await git(root2, ["diff", "--no-index", "--", "/dev/null", path2]);
|
|
477
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]);
|
|
478
514
|
const text = res.stdout;
|
|
479
|
-
|
|
480
|
-
|
|
515
|
+
if (text.length <= MAX_DIFF_CHARS) {
|
|
516
|
+
return { diff: text, truncated: false, files, branch, baseBranch };
|
|
517
|
+
}
|
|
481
518
|
const cut = text.lastIndexOf("\n", MAX_DIFF_CHARS);
|
|
482
|
-
return {
|
|
519
|
+
return {
|
|
520
|
+
diff: text.slice(0, cut > 0 ? cut : MAX_DIFF_CHARS),
|
|
521
|
+
truncated: true,
|
|
522
|
+
files,
|
|
523
|
+
branch,
|
|
524
|
+
baseBranch
|
|
525
|
+
};
|
|
483
526
|
} catch {
|
|
484
527
|
return empty;
|
|
485
528
|
}
|
|
@@ -858,6 +901,33 @@ async function listCheckpoints(folder) {
|
|
|
858
901
|
}
|
|
859
902
|
return out;
|
|
860
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
|
+
}
|
|
861
931
|
async function restoreCheckpoint(folder, id) {
|
|
862
932
|
if (!isValidCheckpointId(id)) {
|
|
863
933
|
return { ok: false, output: `Invalid checkpoint id: ${id}`, safety: null };
|
|
@@ -938,6 +1008,10 @@ class StatusDetector {
|
|
|
938
1008
|
get lastOutput() {
|
|
939
1009
|
return this.lastOutputAt;
|
|
940
1010
|
}
|
|
1011
|
+
/** ANSI-stripped tail of recent output, for prompt-content checks. */
|
|
1012
|
+
get recentText() {
|
|
1013
|
+
return stripAnsi(this.tail);
|
|
1014
|
+
}
|
|
941
1015
|
/** ms epoch when the current status was continuously entered (watchdog clock). */
|
|
942
1016
|
get since() {
|
|
943
1017
|
return this.statusSince;
|
|
@@ -1009,8 +1083,8 @@ function resolveClaude() {
|
|
|
1009
1083
|
if (!IS_WIN$1) {
|
|
1010
1084
|
return candidates[0] ? { file: candidates[0], argsPrefix: [] } : null;
|
|
1011
1085
|
}
|
|
1012
|
-
const exe = candidates.find((
|
|
1013
|
-
const cmd = candidates.find((
|
|
1086
|
+
const exe = candidates.find((c2) => c2.toLowerCase().endsWith(".exe"));
|
|
1087
|
+
const cmd = candidates.find((c2) => /\.(cmd|bat)$/i.test(c2));
|
|
1014
1088
|
if (exe) return { file: exe, argsPrefix: [] };
|
|
1015
1089
|
if (cmd) return { file: process.env.ComSpec ?? "cmd.exe", argsPrefix: ["/c", cmd] };
|
|
1016
1090
|
return null;
|
|
@@ -1323,10 +1397,12 @@ const ALLOWED_TOOLS$2 = [
|
|
|
1323
1397
|
"Bash(git rev-parse:*)"
|
|
1324
1398
|
];
|
|
1325
1399
|
class AutoExpandService {
|
|
1326
|
-
constructor(persistence2, features, getWin2) {
|
|
1400
|
+
constructor(persistence2, features, getWin2, emitGame = () => {
|
|
1401
|
+
}) {
|
|
1327
1402
|
this.persistence = persistence2;
|
|
1328
1403
|
this.features = features;
|
|
1329
1404
|
this.getWin = getWin2;
|
|
1405
|
+
this.emitGame = emitGame;
|
|
1330
1406
|
}
|
|
1331
1407
|
timer = null;
|
|
1332
1408
|
/** Next scheduled run (ms epoch) per session id. */
|
|
@@ -1523,6 +1599,7 @@ class AutoExpandService {
|
|
|
1523
1599
|
run.finishedAt = Date.now();
|
|
1524
1600
|
run.status = status;
|
|
1525
1601
|
this.broadcast(run.sessionId);
|
|
1602
|
+
if (status === "done") this.emitGame({ type: "autoexpand.done" });
|
|
1526
1603
|
}
|
|
1527
1604
|
broadcast(sessionId) {
|
|
1528
1605
|
this.getWin()?.webContents.send("autoexpand:runs", sessionId, this.listRuns(sessionId));
|
|
@@ -2021,9 +2098,9 @@ class ConductorService {
|
|
|
2021
2098
|
const list = focusId ? this.sessions.list().filter((s) => s.config.id === focusId) : this.sessions.list();
|
|
2022
2099
|
const sessions = await Promise.all(
|
|
2023
2100
|
list.map(async (s) => {
|
|
2024
|
-
const
|
|
2025
|
-
const git2 = await this.sessions.getGitStatus(
|
|
2026
|
-
const feats = this.features.list(
|
|
2101
|
+
const c2 = s.config;
|
|
2102
|
+
const git2 = await this.sessions.getGitStatus(c2.id).catch(() => null);
|
|
2103
|
+
const feats = this.features.list(c2.id).map((f) => ({
|
|
2027
2104
|
id: f.id,
|
|
2028
2105
|
title: f.title,
|
|
2029
2106
|
status: f.status,
|
|
@@ -2031,19 +2108,19 @@ class ConductorService {
|
|
|
2031
2108
|
openSpecs: f.specs.filter((sp) => !sp.done).length
|
|
2032
2109
|
}));
|
|
2033
2110
|
const entry = {
|
|
2034
|
-
id:
|
|
2035
|
-
name:
|
|
2036
|
-
folder:
|
|
2037
|
-
isActive:
|
|
2111
|
+
id: c2.id,
|
|
2112
|
+
name: c2.name,
|
|
2113
|
+
folder: c2.folder,
|
|
2114
|
+
isActive: c2.id === activeId,
|
|
2038
2115
|
status: s.status,
|
|
2039
|
-
categoryId:
|
|
2116
|
+
categoryId: c2.categoryId ?? null,
|
|
2040
2117
|
terminals: s.terminals.map((t) => ({ kind: t.config.kind, status: t.status })),
|
|
2041
|
-
worktree:
|
|
2042
|
-
parentSessionId:
|
|
2043
|
-
branch:
|
|
2044
|
-
baseBranch:
|
|
2118
|
+
worktree: c2.worktree ? {
|
|
2119
|
+
parentSessionId: c2.worktree.parentSessionId,
|
|
2120
|
+
branch: c2.worktree.branch,
|
|
2121
|
+
baseBranch: c2.worktree.baseBranch
|
|
2045
2122
|
} : null,
|
|
2046
|
-
autoExpand:
|
|
2123
|
+
autoExpand: c2.autoExpand ? { enabled: c2.autoExpand.enabled, branch: c2.autoExpand.branch } : null,
|
|
2047
2124
|
git: git2 ? {
|
|
2048
2125
|
branch: git2.branch,
|
|
2049
2126
|
ahead: git2.ahead,
|
|
@@ -2052,8 +2129,8 @@ class ConductorService {
|
|
|
2052
2129
|
} : null,
|
|
2053
2130
|
features: feats
|
|
2054
2131
|
};
|
|
2055
|
-
if (
|
|
2056
|
-
const task = await this.sessions.getWorktreeTaskState(
|
|
2132
|
+
if (c2.worktree) {
|
|
2133
|
+
const task = await this.sessions.getWorktreeTaskState(c2.id).catch(() => null);
|
|
2057
2134
|
if (task) {
|
|
2058
2135
|
entry.task = {
|
|
2059
2136
|
dirty: task.dirty,
|
|
@@ -2493,7 +2570,7 @@ const MIN_CONFIDENCE = 0.6;
|
|
|
2493
2570
|
const MAX_SUGGESTIONS_PER_DETECT = 2;
|
|
2494
2571
|
const JUDGE_DAILY_CAP = 12;
|
|
2495
2572
|
const MAX_SUGGESTIONS = 60;
|
|
2496
|
-
function localDay$
|
|
2573
|
+
function localDay$2() {
|
|
2497
2574
|
const d = /* @__PURE__ */ new Date();
|
|
2498
2575
|
return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
|
|
2499
2576
|
}
|
|
@@ -2504,8 +2581,10 @@ const KNOWN_LABELS = {
|
|
|
2504
2581
|
github: "GitHub"
|
|
2505
2582
|
};
|
|
2506
2583
|
class FactoryService {
|
|
2507
|
-
constructor(getWin2) {
|
|
2584
|
+
constructor(getWin2, emitGame = () => {
|
|
2585
|
+
}) {
|
|
2508
2586
|
this.getWin = getWin2;
|
|
2587
|
+
this.emitGame = emitGame;
|
|
2509
2588
|
this.state = this.store.load();
|
|
2510
2589
|
this.runs = restoreRuns(this.store.loadRuns());
|
|
2511
2590
|
for (const s of this.state.suggestions) {
|
|
@@ -2623,23 +2702,23 @@ class FactoryService {
|
|
|
2623
2702
|
absorbScanSuggestions(run) {
|
|
2624
2703
|
let newest = null;
|
|
2625
2704
|
let added = 0;
|
|
2626
|
-
for (const
|
|
2627
|
-
if (
|
|
2628
|
-
if (this.suggestionDuplicate(
|
|
2705
|
+
for (const c2 of run.candidates) {
|
|
2706
|
+
if (c2.status !== "proposed") continue;
|
|
2707
|
+
if (this.suggestionDuplicate(c2.kind, c2.name, c2.description)) continue;
|
|
2629
2708
|
newest = this.enqueueSuggestion({
|
|
2630
|
-
suggestedKind:
|
|
2631
|
-
name:
|
|
2632
|
-
title:
|
|
2633
|
-
description:
|
|
2634
|
-
rationale:
|
|
2709
|
+
suggestedKind: c2.kind,
|
|
2710
|
+
name: c2.name,
|
|
2711
|
+
title: c2.description,
|
|
2712
|
+
description: c2.description,
|
|
2713
|
+
rationale: c2.rationale,
|
|
2635
2714
|
origin: "scan",
|
|
2636
2715
|
sourceRef: run.source,
|
|
2637
2716
|
sourceLabel: run.sourceLabel,
|
|
2638
2717
|
source: run.source,
|
|
2639
2718
|
context: run.summary,
|
|
2640
|
-
topics:
|
|
2641
|
-
keywords:
|
|
2642
|
-
existing:
|
|
2719
|
+
topics: c2.topics,
|
|
2720
|
+
keywords: c2.keywords,
|
|
2721
|
+
existing: c2.existing,
|
|
2643
2722
|
confidence: 0.8
|
|
2644
2723
|
});
|
|
2645
2724
|
added++;
|
|
@@ -2673,7 +2752,7 @@ class FactoryService {
|
|
|
2673
2752
|
return;
|
|
2674
2753
|
}
|
|
2675
2754
|
const turnsAtStart = g.turnsSinceDetect;
|
|
2676
|
-
const day = localDay$
|
|
2755
|
+
const day = localDay$2();
|
|
2677
2756
|
const callsToday = g.judgeDay === day ? g.judgeCallsToday : 0;
|
|
2678
2757
|
if (callsToday >= JUDGE_DAILY_CAP) return;
|
|
2679
2758
|
const recent = messages.filter((m) => !m.pending && m.text?.trim()).slice(-6);
|
|
@@ -2699,7 +2778,7 @@ class FactoryService {
|
|
|
2699
2778
|
this.inFlight = null;
|
|
2700
2779
|
this.setBusy(false);
|
|
2701
2780
|
const g2 = this.store.loadGrowth();
|
|
2702
|
-
const day2 = localDay$
|
|
2781
|
+
const day2 = localDay$2();
|
|
2703
2782
|
this.store.setGrowth({
|
|
2704
2783
|
...g2,
|
|
2705
2784
|
lastDetectAt: Date.now(),
|
|
@@ -3262,7 +3341,7 @@ Lessons learned (respect these):
|
|
|
3262
3341
|
/** Approve a candidate: author its file content and write it to ~/.claude. An errored candidate can be retried. */
|
|
3263
3342
|
async approve(runId, candidateId) {
|
|
3264
3343
|
const run = this.runs.find((r) => r.id === runId);
|
|
3265
|
-
const candidate = run?.candidates.find((
|
|
3344
|
+
const candidate = run?.candidates.find((c2) => c2.id === candidateId);
|
|
3266
3345
|
if (!run || !candidate || candidate.status !== "proposed" && candidate.status !== "error") return;
|
|
3267
3346
|
if (this.busy) return;
|
|
3268
3347
|
this.setBusy(true);
|
|
@@ -3316,14 +3395,14 @@ Lessons learned (respect these):
|
|
|
3316
3395
|
async approveAll(runId) {
|
|
3317
3396
|
const run = this.runs.find((r) => r.id === runId);
|
|
3318
3397
|
if (!run) return;
|
|
3319
|
-
for (const
|
|
3320
|
-
if (
|
|
3398
|
+
for (const c2 of run.candidates) {
|
|
3399
|
+
if (c2.status === "proposed") await this.approve(runId, c2.id);
|
|
3321
3400
|
if (this.cancelRequested) break;
|
|
3322
3401
|
}
|
|
3323
3402
|
}
|
|
3324
3403
|
reject(runId, candidateId) {
|
|
3325
3404
|
const run = this.runs.find((r) => r.id === runId);
|
|
3326
|
-
const candidate = run?.candidates.find((
|
|
3405
|
+
const candidate = run?.candidates.find((c2) => c2.id === candidateId);
|
|
3327
3406
|
if (!candidate || candidate.status !== "proposed") return;
|
|
3328
3407
|
candidate.status = "rejected";
|
|
3329
3408
|
this.broadcastRuns();
|
|
@@ -3423,6 +3502,10 @@ Lessons learned (respect these):
|
|
|
3423
3502
|
createdAt: now,
|
|
3424
3503
|
updatedAt: now
|
|
3425
3504
|
});
|
|
3505
|
+
this.emitGame({
|
|
3506
|
+
type: input.kind === "agent" ? "factory.agent" : "factory.skill",
|
|
3507
|
+
meta: { name: input.name }
|
|
3508
|
+
});
|
|
3426
3509
|
}
|
|
3427
3510
|
for (const a of this.state.artifacts) {
|
|
3428
3511
|
if (related.includes(a.name) && !a.relatedArtifacts.includes(input.name)) {
|
|
@@ -3590,10 +3673,10 @@ function restoreRuns(runs) {
|
|
|
3590
3673
|
run.finishedAt = run.finishedAt ?? Date.now();
|
|
3591
3674
|
run.summary = run.summary || "Interrupted — the app was closed mid-scan.";
|
|
3592
3675
|
}
|
|
3593
|
-
for (const
|
|
3594
|
-
if (
|
|
3595
|
-
|
|
3596
|
-
|
|
3676
|
+
for (const c2 of run.candidates) {
|
|
3677
|
+
if (c2.status === "authoring") {
|
|
3678
|
+
c2.status = "proposed";
|
|
3679
|
+
c2.result = void 0;
|
|
3597
3680
|
}
|
|
3598
3681
|
}
|
|
3599
3682
|
}
|
|
@@ -3626,9 +3709,11 @@ Feature: ${feature.title}
|
|
|
3626
3709
|
Read that spec file in full, then implement every spec it lists. Make your changes in this worktree and commit them as each part works. If any spec is ambiguous, ask before guessing.`;
|
|
3627
3710
|
}
|
|
3628
3711
|
class FeatureService {
|
|
3629
|
-
constructor(persistence2, sessions) {
|
|
3712
|
+
constructor(persistence2, sessions, emitGame = () => {
|
|
3713
|
+
}) {
|
|
3630
3714
|
this.persistence = persistence2;
|
|
3631
3715
|
this.sessions = sessions;
|
|
3716
|
+
this.emitGame = emitGame;
|
|
3632
3717
|
}
|
|
3633
3718
|
get features() {
|
|
3634
3719
|
return this.persistence.state.features;
|
|
@@ -3649,8 +3734,12 @@ class FeatureService {
|
|
|
3649
3734
|
save(feature) {
|
|
3650
3735
|
const list = this.features;
|
|
3651
3736
|
const idx = list.findIndex((f) => f.id === feature.id);
|
|
3652
|
-
if (idx >= 0)
|
|
3653
|
-
|
|
3737
|
+
if (idx >= 0) {
|
|
3738
|
+
list[idx] = feature;
|
|
3739
|
+
} else {
|
|
3740
|
+
list.push(feature);
|
|
3741
|
+
this.emitGame({ type: "feature.save" });
|
|
3742
|
+
}
|
|
3654
3743
|
this.persistence.scheduleSave();
|
|
3655
3744
|
}
|
|
3656
3745
|
delete(id) {
|
|
@@ -3842,6 +3931,396 @@ class FsService {
|
|
|
3842
3931
|
}
|
|
3843
3932
|
}
|
|
3844
3933
|
}
|
|
3934
|
+
const TOKENS_PER_XP = 1e3;
|
|
3935
|
+
const XP_TABLE = {
|
|
3936
|
+
"tokens.burn": 0,
|
|
3937
|
+
// computed from meta.tokens in xpForEvent
|
|
3938
|
+
"session.turn": 5,
|
|
3939
|
+
"conductor.turn": 5,
|
|
3940
|
+
"action.run": 8,
|
|
3941
|
+
"checkpoint.create": 10,
|
|
3942
|
+
"sentinel.run": 12,
|
|
3943
|
+
"session.create": 20,
|
|
3944
|
+
"worktree.create": 25,
|
|
3945
|
+
"feature.save": 30,
|
|
3946
|
+
"autoexpand.done": 40,
|
|
3947
|
+
"worktree.pr": 50,
|
|
3948
|
+
"worktree.merge": 60,
|
|
3949
|
+
"factory.skill": 75,
|
|
3950
|
+
"factory.agent": 75,
|
|
3951
|
+
"feature.merge": 80
|
|
3952
|
+
};
|
|
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
|
+
}
|
|
3957
|
+
let xp = XP_TABLE[e.type] ?? 0;
|
|
3958
|
+
if (e.type === "worktree.merge") xp += Math.min(e.meta?.commits ?? 0, 10) * 3;
|
|
3959
|
+
return xp;
|
|
3960
|
+
}
|
|
3961
|
+
function xpForLevel(n) {
|
|
3962
|
+
if (n <= 1) return 0;
|
|
3963
|
+
const k = n - 1;
|
|
3964
|
+
return 50 * k * k + 50 * k;
|
|
3965
|
+
}
|
|
3966
|
+
function levelForXp(xp) {
|
|
3967
|
+
if (xp <= 0) return 1;
|
|
3968
|
+
return Math.max(1, Math.floor((-50 + Math.sqrt(2500 + 200 * xp)) / 100) + 1);
|
|
3969
|
+
}
|
|
3970
|
+
function levelInfo(xp) {
|
|
3971
|
+
const level = levelForXp(xp);
|
|
3972
|
+
const floor = xpForLevel(level);
|
|
3973
|
+
const next = xpForLevel(level + 1);
|
|
3974
|
+
return { level, xpIntoLevel: xp - floor, xpForNextLevel: next - floor };
|
|
3975
|
+
}
|
|
3976
|
+
const DEFAULT_GAME_STATE = {
|
|
3977
|
+
xp: 0,
|
|
3978
|
+
streak: { current: 0, longest: 0, lastDay: "" },
|
|
3979
|
+
achievements: {},
|
|
3980
|
+
todaysQuests: [],
|
|
3981
|
+
questDay: "",
|
|
3982
|
+
counters: {},
|
|
3983
|
+
nightTurns: 0,
|
|
3984
|
+
earlyTurns: 0,
|
|
3985
|
+
tokensBurned: 0,
|
|
3986
|
+
usageTokensSeen: -1,
|
|
3987
|
+
createdAt: 0
|
|
3988
|
+
};
|
|
3989
|
+
const c = (ctx, t) => ctx.counters[t] ?? 0;
|
|
3990
|
+
const ACHIEVEMENTS = [
|
|
3991
|
+
// sessions
|
|
3992
|
+
{ id: "first-session", title: "Hello, Maestro", desc: "Start your first session.", icon: "🎬", category: "sessions", predicate: (x) => c(x, "session.create") >= 1 },
|
|
3993
|
+
{ id: "ten-sessions", title: "Regular", desc: "Start 10 sessions.", icon: "📁", category: "sessions", predicate: (x) => c(x, "session.create") >= 10 },
|
|
3994
|
+
{ id: "worktree-novice", title: "Branching Out", desc: "Create your first parallel task.", icon: "🌱", category: "sessions", predicate: (x) => c(x, "worktree.create") >= 1 },
|
|
3995
|
+
{ id: "worktree-adept", title: "Multitasker", desc: "Create 10 parallel tasks.", icon: "🌳", category: "sessions", predicate: (x) => c(x, "worktree.create") >= 10 },
|
|
3996
|
+
// merges
|
|
3997
|
+
{ id: "first-merge", title: "Merge One", desc: "Merge your first worktree.", icon: "🔀", category: "merges", predicate: (x) => c(x, "worktree.merge") >= 1 },
|
|
3998
|
+
{ id: "ten-merges", title: "Merge Maestro", desc: "Merge 10 worktrees.", icon: "🧬", category: "merges", predicate: (x) => c(x, "worktree.merge") >= 10 },
|
|
3999
|
+
{ id: "fifty-merges", title: "Merge Machine", desc: "Merge 50 worktrees.", icon: "⚙️", category: "merges", predicate: (x) => c(x, "worktree.merge") >= 50 },
|
|
4000
|
+
{ id: "pr-opener", title: "Pull Request", desc: "Open your first PR.", icon: "📤", category: "merges", predicate: (x) => c(x, "worktree.pr") >= 1 },
|
|
4001
|
+
{ id: "pr-prolific", title: "PR Prolific", desc: "Open 10 PRs.", icon: "🚀", category: "merges", predicate: (x) => c(x, "worktree.pr") >= 10 },
|
|
4002
|
+
{ id: "feature-shipper", title: "Ship It", desc: "Merge a feature you specced.", icon: "📦", category: "merges", predicate: (x) => c(x, "feature.merge") >= 1 },
|
|
4003
|
+
// turns
|
|
4004
|
+
{ id: "first-turn", title: "First Light", desc: "Finish your first Claude turn.", icon: "✶", category: "turns", predicate: (x) => c(x, "session.turn") >= 1 },
|
|
4005
|
+
{ id: "hundred-turns", title: "Century", desc: "Finish 100 turns.", icon: "💯", category: "turns", predicate: (x) => c(x, "session.turn") >= 100 },
|
|
4006
|
+
{ id: "thousand-turns", title: "Marathoner", desc: "Finish 1,000 turns.", icon: "🏃", category: "turns", predicate: (x) => c(x, "session.turn") >= 1e3 },
|
|
4007
|
+
{ id: "conductor-curious", title: "Conductor Curious", desc: "Have your first Conductor turn.", icon: "✦", category: "turns", predicate: (x) => c(x, "conductor.turn") >= 1 },
|
|
4008
|
+
{ id: "conductor-regular", title: "Maestro of Maestro", desc: "Have 50 Conductor turns.", icon: "🎼", category: "turns", predicate: (x) => c(x, "conductor.turn") >= 50 },
|
|
4009
|
+
// factory
|
|
4010
|
+
{ id: "first-skill", title: "Skill Smith", desc: "Create your first skill.", icon: "🛠", category: "factory", predicate: (x) => c(x, "factory.skill") >= 1 },
|
|
4011
|
+
{ id: "first-agent", title: "Agent Architect", desc: "Create your first agent.", icon: "🤖", category: "factory", predicate: (x) => c(x, "factory.agent") >= 1 },
|
|
4012
|
+
{ id: "toolsmith", title: "Toolsmith", desc: "Create 5 skills/agents.", icon: "⚒", category: "factory", predicate: (x) => c(x, "factory.skill") + c(x, "factory.agent") >= 5 },
|
|
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 },
|
|
4031
|
+
// streak
|
|
4032
|
+
{ id: "streak-3", title: "Warmed Up", desc: "Keep a 3-day streak.", icon: "🔥", category: "streak", predicate: (x) => x.streakLongest >= 3 },
|
|
4033
|
+
{ id: "streak-7", title: "On Fire", desc: "Keep a 7-day streak.", icon: "🔥", category: "streak", predicate: (x) => x.streakLongest >= 7 },
|
|
4034
|
+
{ id: "streak-30", title: "Unstoppable", desc: "Keep a 30-day streak.", icon: "☄️", category: "streak", predicate: (x) => x.streakLongest >= 30 },
|
|
4035
|
+
// time of day
|
|
4036
|
+
{ id: "night-owl", title: "Night Owl", desc: "Finish a turn between midnight and 5am.", icon: "🦉", category: "time", predicate: (x) => x.nightTurns >= 1 },
|
|
4037
|
+
{ id: "early-bird", title: "Early Bird", desc: "Finish a turn between 5 and 8am.", icon: "🌅", category: "time", predicate: (x) => x.earlyTurns >= 1 },
|
|
4038
|
+
// level
|
|
4039
|
+
{ id: "level-10", title: "Double Digits", desc: "Reach level 10.", icon: "⭐", category: "level", predicate: (x) => x.level >= 10 },
|
|
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 }
|
|
4045
|
+
];
|
|
4046
|
+
const DAILY_QUEST_POOL = [
|
|
4047
|
+
{ id: "q-turns-5", title: "Finish 5 Claude turns", target: 5, reward: 30, events: ["session.turn"] },
|
|
4048
|
+
{ id: "q-merge-1", title: "Merge a worktree", target: 1, reward: 40, events: ["worktree.merge"] },
|
|
4049
|
+
{ id: "q-merge-3", title: "Merge 3 worktrees", target: 3, reward: 100, events: ["worktree.merge"] },
|
|
4050
|
+
{ id: "q-create-1", title: "Start a session", target: 1, reward: 20, events: ["session.create"] },
|
|
4051
|
+
{ id: "q-worktree-2", title: "Spin up 2 parallel tasks", target: 2, reward: 50, events: ["worktree.create"] },
|
|
4052
|
+
{ id: "q-checkpoint", title: "Make a checkpoint", target: 1, reward: 20, events: ["checkpoint.create"] },
|
|
4053
|
+
{ id: "q-conductor-3", title: "Have 3 Conductor turns", target: 3, reward: 30, events: ["conductor.turn"] },
|
|
4054
|
+
{ id: "q-factory-1", title: "Create a skill or agent", target: 1, reward: 75, events: ["factory.skill", "factory.agent"] },
|
|
4055
|
+
{ id: "q-feature-1", title: "Save a feature spec", target: 1, reward: 30, events: ["feature.save"] },
|
|
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"] }
|
|
4061
|
+
];
|
|
4062
|
+
const questDef = (id) => DAILY_QUEST_POOL.find((q) => q.id === id);
|
|
4063
|
+
function hashStr(s) {
|
|
4064
|
+
let h = 2166136261;
|
|
4065
|
+
for (let i = 0; i < s.length; i++) {
|
|
4066
|
+
h ^= s.charCodeAt(i);
|
|
4067
|
+
h = Math.imul(h, 16777619);
|
|
4068
|
+
}
|
|
4069
|
+
return h >>> 0;
|
|
4070
|
+
}
|
|
4071
|
+
function mulberry32(seed) {
|
|
4072
|
+
let a = seed >>> 0;
|
|
4073
|
+
return () => {
|
|
4074
|
+
a = a + 1831565813 | 0;
|
|
4075
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
4076
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
4077
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
4078
|
+
};
|
|
4079
|
+
}
|
|
4080
|
+
function pickDailyQuests(dayKey2, n = 3) {
|
|
4081
|
+
const rnd = mulberry32(hashStr("quest:" + dayKey2));
|
|
4082
|
+
const pool = [...DAILY_QUEST_POOL];
|
|
4083
|
+
for (let i = pool.length - 1; i > 0; i--) {
|
|
4084
|
+
const j = Math.floor(rnd() * (i + 1));
|
|
4085
|
+
[pool[i], pool[j]] = [pool[j], pool[i]];
|
|
4086
|
+
}
|
|
4087
|
+
return pool.slice(0, Math.min(n, pool.length)).map((q) => ({
|
|
4088
|
+
id: q.id,
|
|
4089
|
+
target: q.target,
|
|
4090
|
+
progress: 0,
|
|
4091
|
+
rewarded: false
|
|
4092
|
+
}));
|
|
4093
|
+
}
|
|
4094
|
+
function dayKey(d) {
|
|
4095
|
+
return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
|
|
4096
|
+
}
|
|
4097
|
+
function prevDayKey(d) {
|
|
4098
|
+
const y = new Date(d.getFullYear(), d.getMonth(), d.getDate() - 1);
|
|
4099
|
+
return dayKey(y);
|
|
4100
|
+
}
|
|
4101
|
+
class GamificationStore {
|
|
4102
|
+
file = path.join(electron.app.getPath("userData"), "gamification.json");
|
|
4103
|
+
timer = null;
|
|
4104
|
+
state = structuredClone(DEFAULT_GAME_STATE);
|
|
4105
|
+
/** Load saved progress (best-effort; defaults on any error, back-compat via spread). */
|
|
4106
|
+
load() {
|
|
4107
|
+
try {
|
|
4108
|
+
const raw = JSON.parse(fs.readFileSync(this.file, "utf8"));
|
|
4109
|
+
const s = { ...structuredClone(DEFAULT_GAME_STATE), ...raw ?? {} };
|
|
4110
|
+
const num = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : Number.isFinite(Number(v)) ? Number(v) : fallback;
|
|
4111
|
+
if (!s.streak || typeof s.streak !== "object") s.streak = { current: 0, longest: 0, lastDay: "" };
|
|
4112
|
+
s.streak.current = num(s.streak.current, 0);
|
|
4113
|
+
s.streak.longest = num(s.streak.longest, 0);
|
|
4114
|
+
if (typeof s.streak.lastDay !== "string") s.streak.lastDay = "";
|
|
4115
|
+
if (!s.achievements || typeof s.achievements !== "object" || Array.isArray(s.achievements)) {
|
|
4116
|
+
s.achievements = {};
|
|
4117
|
+
}
|
|
4118
|
+
if (!Array.isArray(s.todaysQuests)) s.todaysQuests = [];
|
|
4119
|
+
s.todaysQuests = s.todaysQuests.filter((q) => q && typeof q.id === "string").map((q) => ({ id: q.id, target: num(q.target, 1), progress: num(q.progress, 0), rewarded: !!q.rewarded }));
|
|
4120
|
+
if (!s.counters || typeof s.counters !== "object" || Array.isArray(s.counters)) s.counters = {};
|
|
4121
|
+
for (const k of Object.keys(s.counters)) {
|
|
4122
|
+
s.counters[k] = num(s.counters[k], 0);
|
|
4123
|
+
}
|
|
4124
|
+
s.nightTurns = num(s.nightTurns, 0);
|
|
4125
|
+
s.earlyTurns = num(s.earlyTurns, 0);
|
|
4126
|
+
s.tokensBurned = num(s.tokensBurned, 0);
|
|
4127
|
+
s.usageTokensSeen = num(s.usageTokensSeen, -1);
|
|
4128
|
+
s.xp = num(s.xp, 0);
|
|
4129
|
+
if (!s.createdAt) s.createdAt = Date.now();
|
|
4130
|
+
this.state = s;
|
|
4131
|
+
} catch {
|
|
4132
|
+
this.state = structuredClone(DEFAULT_GAME_STATE);
|
|
4133
|
+
this.state.createdAt = Date.now();
|
|
4134
|
+
}
|
|
4135
|
+
return this.state;
|
|
4136
|
+
}
|
|
4137
|
+
get() {
|
|
4138
|
+
return this.state;
|
|
4139
|
+
}
|
|
4140
|
+
set(state) {
|
|
4141
|
+
this.state = state;
|
|
4142
|
+
this.scheduleSave();
|
|
4143
|
+
}
|
|
4144
|
+
scheduleSave() {
|
|
4145
|
+
if (this.timer) clearTimeout(this.timer);
|
|
4146
|
+
this.timer = setTimeout(() => this.saveNow(), 500);
|
|
4147
|
+
}
|
|
4148
|
+
saveNow() {
|
|
4149
|
+
if (this.timer) {
|
|
4150
|
+
clearTimeout(this.timer);
|
|
4151
|
+
this.timer = null;
|
|
4152
|
+
}
|
|
4153
|
+
try {
|
|
4154
|
+
fs.mkdirSync(path.dirname(this.file), { recursive: true });
|
|
4155
|
+
const tmp = this.file + ".tmp";
|
|
4156
|
+
fs.writeFileSync(tmp, JSON.stringify(this.state, null, 2), "utf8");
|
|
4157
|
+
fs.renameSync(tmp, this.file);
|
|
4158
|
+
} catch (err) {
|
|
4159
|
+
console.error("Failed to persist gamification state:", err);
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
const ACHIEVEMENT_XP = 25;
|
|
4164
|
+
const TOKEN_POLL_MS = 6e4;
|
|
4165
|
+
class GamificationService {
|
|
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) {
|
|
4171
|
+
this.getWin = getWin2;
|
|
4172
|
+
this.getUsageTokens = getUsageTokens;
|
|
4173
|
+
this.state = this.store.load();
|
|
4174
|
+
}
|
|
4175
|
+
store = new GamificationStore();
|
|
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
|
+
}
|
|
4185
|
+
/** GameState + derived level fields (for the initial renderer fetch). */
|
|
4186
|
+
snapshot() {
|
|
4187
|
+
return { ...this.state, ...levelInfo(this.state.xp) };
|
|
4188
|
+
}
|
|
4189
|
+
dispose() {
|
|
4190
|
+
if (this.tokenTimer) {
|
|
4191
|
+
clearInterval(this.tokenTimer);
|
|
4192
|
+
this.tokenTimer = null;
|
|
4193
|
+
}
|
|
4194
|
+
this.store.saveNow();
|
|
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
|
+
}
|
|
4224
|
+
/**
|
|
4225
|
+
* Apply one event: bump counters, roll the day (streak + quests), add XP,
|
|
4226
|
+
* advance quests, unlock achievements — each guarded so nothing double-counts.
|
|
4227
|
+
* Wrapped so a gamification failure can never break the caller's turn/merge.
|
|
4228
|
+
*/
|
|
4229
|
+
award(e) {
|
|
4230
|
+
try {
|
|
4231
|
+
const s = this.state;
|
|
4232
|
+
const now = /* @__PURE__ */ new Date();
|
|
4233
|
+
const today = dayKey(now);
|
|
4234
|
+
const celebrations = [];
|
|
4235
|
+
if (s.streak.lastDay !== today) {
|
|
4236
|
+
s.streak.current = s.streak.lastDay === prevDayKey(now) ? s.streak.current + 1 : 1;
|
|
4237
|
+
s.streak.longest = Math.max(s.streak.longest, s.streak.current);
|
|
4238
|
+
s.streak.lastDay = today;
|
|
4239
|
+
s.todaysQuests = pickDailyQuests(today);
|
|
4240
|
+
s.questDay = today;
|
|
4241
|
+
if (s.streak.current >= 2) {
|
|
4242
|
+
celebrations.push({
|
|
4243
|
+
kind: "streak",
|
|
4244
|
+
seed: `streak:${today}:${s.streak.current}`,
|
|
4245
|
+
current: s.streak.current
|
|
4246
|
+
});
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
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
|
+
}
|
|
4254
|
+
if (e.type === "session.turn" || e.type === "conductor.turn") {
|
|
4255
|
+
const hour = e.meta?.hour ?? now.getHours();
|
|
4256
|
+
if (hour >= 0 && hour < 5) s.nightTurns += 1;
|
|
4257
|
+
else if (hour >= 5 && hour < 8) s.earlyTurns += 1;
|
|
4258
|
+
}
|
|
4259
|
+
const beforeLevel = levelForXp(s.xp);
|
|
4260
|
+
s.xp += xpForEvent(e);
|
|
4261
|
+
for (const q of s.todaysQuests) {
|
|
4262
|
+
if (q.rewarded) continue;
|
|
4263
|
+
const def = questDef(q.id);
|
|
4264
|
+
if (!def || !def.events.includes(e.type)) continue;
|
|
4265
|
+
q.progress = Math.min(q.target, q.progress + 1);
|
|
4266
|
+
if (q.progress >= q.target) {
|
|
4267
|
+
q.rewarded = true;
|
|
4268
|
+
s.xp += def.reward;
|
|
4269
|
+
celebrations.push({
|
|
4270
|
+
kind: "quest",
|
|
4271
|
+
seed: `quest:${today}:${q.id}`,
|
|
4272
|
+
id: q.id,
|
|
4273
|
+
title: def.title,
|
|
4274
|
+
xp: def.reward
|
|
4275
|
+
});
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
let unlockedThisPass = true;
|
|
4279
|
+
while (unlockedThisPass) {
|
|
4280
|
+
unlockedThisPass = false;
|
|
4281
|
+
const ctx = {
|
|
4282
|
+
counters: s.counters,
|
|
4283
|
+
nightTurns: s.nightTurns,
|
|
4284
|
+
earlyTurns: s.earlyTurns,
|
|
4285
|
+
streakLongest: s.streak.longest,
|
|
4286
|
+
level: levelForXp(s.xp),
|
|
4287
|
+
tokensBurned: s.tokensBurned
|
|
4288
|
+
};
|
|
4289
|
+
for (const a of ACHIEVEMENTS) {
|
|
4290
|
+
if (s.achievements[a.id]) continue;
|
|
4291
|
+
if (a.predicate(ctx)) {
|
|
4292
|
+
s.achievements[a.id] = { unlockedAt: Date.now() };
|
|
4293
|
+
s.xp += ACHIEVEMENT_XP;
|
|
4294
|
+
unlockedThisPass = true;
|
|
4295
|
+
celebrations.push({
|
|
4296
|
+
kind: "achievement",
|
|
4297
|
+
seed: `ach:${a.id}`,
|
|
4298
|
+
id: a.id,
|
|
4299
|
+
title: a.title,
|
|
4300
|
+
icon: a.icon,
|
|
4301
|
+
xp: ACHIEVEMENT_XP
|
|
4302
|
+
});
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
}
|
|
4306
|
+
const afterLevel = levelForXp(s.xp);
|
|
4307
|
+
for (let l = beforeLevel + 1; l <= afterLevel; l++) {
|
|
4308
|
+
celebrations.push({ kind: "level-up", seed: `level:${l}`, level: l });
|
|
4309
|
+
}
|
|
4310
|
+
this.store.set(s);
|
|
4311
|
+
this.broadcast();
|
|
4312
|
+
for (const c2 of celebrations) this.celebrate(c2);
|
|
4313
|
+
} catch (err) {
|
|
4314
|
+
console.error("Gamification award failed (ignored):", err);
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
broadcast() {
|
|
4318
|
+
this.getWin()?.webContents.send("gamification:changed", this.snapshot());
|
|
4319
|
+
}
|
|
4320
|
+
celebrate(c2) {
|
|
4321
|
+
this.getWin()?.webContents.send("gamification:celebrate", c2);
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
3845
4324
|
const IMAGE_EXTS$1 = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"]);
|
|
3846
4325
|
const THUMB_HEIGHT = 64;
|
|
3847
4326
|
const MAX_LISTED = 100;
|
|
@@ -3961,6 +4440,182 @@ async function clearBackgroundImage() {
|
|
|
3961
4440
|
} catch {
|
|
3962
4441
|
}
|
|
3963
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
|
+
}
|
|
3964
4619
|
const CREDENTIALS_PATH = path.join(os.homedir(), ".claude", ".credentials.json");
|
|
3965
4620
|
const USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
3966
4621
|
const OAUTH_BETA = "oauth-2025-04-20";
|
|
@@ -4037,216 +4692,74 @@ class UsageLimitsService {
|
|
|
4037
4692
|
};
|
|
4038
4693
|
}
|
|
4039
4694
|
}
|
|
4040
|
-
|
|
4041
|
-
const BLOCK_MS = 5 * 60 * 60 * 1e3;
|
|
4042
|
-
const HOUR_MS = 60 * 60 * 1e3;
|
|
4043
|
-
const PRICING = [
|
|
4044
|
-
{ match: /fable/, input: 10, output: 50 },
|
|
4045
|
-
{ match: /opus-4-[5-9]/, input: 5, output: 25 },
|
|
4046
|
-
{ match: /opus/, input: 15, output: 75 },
|
|
4047
|
-
{ match: /sonnet/, input: 3, output: 15 },
|
|
4048
|
-
{ match: /haiku-4/, input: 1, output: 5 },
|
|
4049
|
-
{ match: /3-5-haiku|haiku-3-5/, input: 0.8, output: 4 },
|
|
4050
|
-
{ match: /haiku/, input: 0.25, output: 1.25 }
|
|
4051
|
-
];
|
|
4052
|
-
function zeroTotals() {
|
|
4053
|
-
return { inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0, costUSD: 0 };
|
|
4054
|
-
}
|
|
4055
|
-
function add(t, e) {
|
|
4056
|
-
t.inputTokens += e.input;
|
|
4057
|
-
t.outputTokens += e.output;
|
|
4058
|
-
t.cacheWriteTokens += e.cacheWrite5m + e.cacheWrite1h;
|
|
4059
|
-
t.cacheReadTokens += e.cacheRead;
|
|
4060
|
-
t.costUSD += e.costUSD;
|
|
4061
|
-
}
|
|
4062
|
-
function localDay(at) {
|
|
4695
|
+
function localDay$1(at) {
|
|
4063
4696
|
const d = new Date(at);
|
|
4064
4697
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
4065
4698
|
const day = String(d.getDate()).padStart(2, "0");
|
|
4066
4699
|
return `${d.getFullYear()}-${m}-${day}`;
|
|
4067
4700
|
}
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
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;
|
|
4072
|
-
}
|
|
4073
|
-
function computeProjection(points, now) {
|
|
4074
|
-
if (points.length === 0) return null;
|
|
4075
|
-
points.sort((a, b) => a.at - b.at);
|
|
4076
|
-
const blocks = [];
|
|
4077
|
-
let cur = null;
|
|
4078
|
-
for (const p of points) {
|
|
4079
|
-
if (!cur || p.at >= cur.start + BLOCK_MS || p.at - cur.lastAt >= BLOCK_MS) {
|
|
4080
|
-
cur = { start: Math.floor(p.at / HOUR_MS) * HOUR_MS, firstAt: p.at, lastAt: p.at, tokens: 0 };
|
|
4081
|
-
blocks.push(cur);
|
|
4082
|
-
}
|
|
4083
|
-
cur.lastAt = p.at;
|
|
4084
|
-
cur.tokens += p.tokens;
|
|
4701
|
+
class BudgetAlerts {
|
|
4702
|
+
constructor(getWin2) {
|
|
4703
|
+
this.getWin = getWin2;
|
|
4085
4704
|
}
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
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
|
+
);
|
|
4092
4720
|
}
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
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
|
+
}
|
|
4102
4740
|
}
|
|
4103
4741
|
}
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
} catch {
|
|
4118
|
-
return [];
|
|
4119
|
-
}
|
|
4120
|
-
const entries = [];
|
|
4121
|
-
for (const line of raw.split("\n")) {
|
|
4122
|
-
if (!line.includes('"assistant"') || !line.includes('"usage"')) continue;
|
|
4123
|
-
let obj;
|
|
4124
|
-
try {
|
|
4125
|
-
obj = JSON.parse(line);
|
|
4126
|
-
} catch {
|
|
4127
|
-
continue;
|
|
4128
|
-
}
|
|
4129
|
-
if (obj.type !== "assistant") continue;
|
|
4130
|
-
const message = obj.message;
|
|
4131
|
-
const usage = message?.usage;
|
|
4132
|
-
const model = typeof message?.model === "string" ? message.model : "";
|
|
4133
|
-
if (!usage || !model || model === "<synthetic>") continue;
|
|
4134
|
-
const num = (v) => typeof v === "number" && isFinite(v) ? v : 0;
|
|
4135
|
-
const cacheCreation = usage.cache_creation;
|
|
4136
|
-
const totalWrite = num(usage.cache_creation_input_tokens);
|
|
4137
|
-
const write1h = num(cacheCreation?.ephemeral_1h_input_tokens);
|
|
4138
|
-
const write5m = cacheCreation ? num(cacheCreation.ephemeral_5m_input_tokens) : totalWrite;
|
|
4139
|
-
const at = Date.parse(typeof obj.timestamp === "string" ? obj.timestamp : "") || 0;
|
|
4140
|
-
const msgId = typeof message?.id === "string" ? message.id : obj.uuid ?? "";
|
|
4141
|
-
const entry = {
|
|
4142
|
-
key: `${msgId}:${typeof obj.requestId === "string" ? obj.requestId : ""}`,
|
|
4143
|
-
day: localDay(at),
|
|
4144
|
-
at,
|
|
4145
|
-
model,
|
|
4146
|
-
input: num(usage.input_tokens),
|
|
4147
|
-
output: num(usage.output_tokens),
|
|
4148
|
-
cacheWrite5m: write5m,
|
|
4149
|
-
cacheWrite1h: write1h,
|
|
4150
|
-
cacheRead: num(usage.cache_read_input_tokens),
|
|
4151
|
-
costUSD: 0
|
|
4152
|
-
};
|
|
4153
|
-
const precomputed = num(obj.costUSD);
|
|
4154
|
-
entry.costUSD = precomputed > 0 ? precomputed : computeCost(entry);
|
|
4155
|
-
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();
|
|
4156
4755
|
}
|
|
4157
|
-
return entries;
|
|
4158
4756
|
}
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
const total = zeroTotals();
|
|
4166
|
-
const todayTotals = zeroTotals();
|
|
4167
|
-
const month = zeroTotals();
|
|
4168
|
-
const perModel = /* @__PURE__ */ new Map();
|
|
4169
|
-
const perProject = [];
|
|
4170
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4171
|
-
const liveFiles = /* @__PURE__ */ new Set();
|
|
4172
|
-
const points = [];
|
|
4173
|
-
let projectDirs = [];
|
|
4174
|
-
try {
|
|
4175
|
-
if (fs.existsSync(PROJECTS_DIR)) projectDirs = fs.readdirSync(PROJECTS_DIR);
|
|
4176
|
-
} catch {
|
|
4177
|
-
projectDirs = [];
|
|
4178
|
-
}
|
|
4179
|
-
for (const dir of projectDirs) {
|
|
4180
|
-
const dirPath = path.join(PROJECTS_DIR, dir);
|
|
4181
|
-
let files;
|
|
4182
|
-
try {
|
|
4183
|
-
files = fs.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
|
|
4184
|
-
} catch {
|
|
4185
|
-
continue;
|
|
4186
|
-
}
|
|
4187
|
-
const project = {
|
|
4188
|
-
dir,
|
|
4189
|
-
total: zeroTotals(),
|
|
4190
|
-
today: zeroTotals(),
|
|
4191
|
-
lastActivityAt: 0
|
|
4192
|
-
};
|
|
4193
|
-
for (const file of files) {
|
|
4194
|
-
const path$1 = path.join(dirPath, file);
|
|
4195
|
-
liveFiles.add(path$1);
|
|
4196
|
-
for (const entry of this.entriesFor(path$1)) {
|
|
4197
|
-
if (seen.has(entry.key)) continue;
|
|
4198
|
-
seen.add(entry.key);
|
|
4199
|
-
if (entry.at > 0) {
|
|
4200
|
-
points.push({
|
|
4201
|
-
at: entry.at,
|
|
4202
|
-
tokens: entry.input + entry.output + entry.cacheWrite5m + entry.cacheWrite1h + entry.cacheRead
|
|
4203
|
-
});
|
|
4204
|
-
}
|
|
4205
|
-
add(total, entry);
|
|
4206
|
-
add(project.total, entry);
|
|
4207
|
-
if (entry.day === today) {
|
|
4208
|
-
add(todayTotals, entry);
|
|
4209
|
-
add(project.today, entry);
|
|
4210
|
-
}
|
|
4211
|
-
if (entry.day.startsWith(monthPrefix)) add(month, entry);
|
|
4212
|
-
let m = perModel.get(entry.model);
|
|
4213
|
-
if (!m) perModel.set(entry.model, m = zeroTotals());
|
|
4214
|
-
add(m, entry);
|
|
4215
|
-
if (entry.at > project.lastActivityAt) project.lastActivityAt = entry.at;
|
|
4216
|
-
}
|
|
4217
|
-
}
|
|
4218
|
-
if (project.total.costUSD > 0 || project.total.outputTokens > 0) perProject.push(project);
|
|
4219
|
-
}
|
|
4220
|
-
for (const path2 of [...this.fileCache.keys()]) {
|
|
4221
|
-
if (!liveFiles.has(path2)) this.fileCache.delete(path2);
|
|
4222
|
-
}
|
|
4223
|
-
perProject.sort((a, b) => b.total.costUSD - a.total.costUSD);
|
|
4224
|
-
return {
|
|
4225
|
-
total,
|
|
4226
|
-
today: todayTotals,
|
|
4227
|
-
month,
|
|
4228
|
-
perProject,
|
|
4229
|
-
perModel: [...perModel.entries()].map(([model, totals]) => ({ model, totals })).sort((a, b) => b.totals.costUSD - a.totals.costUSD),
|
|
4230
|
-
projection: computeProjection(points, now),
|
|
4231
|
-
updatedAt: now
|
|
4232
|
-
};
|
|
4233
|
-
}
|
|
4234
|
-
entriesFor(path2) {
|
|
4235
|
-
let stat;
|
|
4236
|
-
try {
|
|
4237
|
-
stat = fs.statSync(path2);
|
|
4238
|
-
} catch {
|
|
4239
|
-
this.fileCache.delete(path2);
|
|
4240
|
-
return [];
|
|
4241
|
-
}
|
|
4242
|
-
const cached = this.fileCache.get(path2);
|
|
4243
|
-
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
4244
|
-
return cached.entries;
|
|
4245
|
-
}
|
|
4246
|
-
const entries = parseFile(path2);
|
|
4247
|
-
this.fileCache.set(path2, { mtimeMs: stat.mtimeMs, size: stat.size, entries });
|
|
4248
|
-
return entries;
|
|
4249
|
-
}
|
|
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)}`;
|
|
4250
4763
|
}
|
|
4251
4764
|
const CONDUCTOR_ATTACH_SCOPE = "conductor";
|
|
4252
4765
|
function tokenize(template) {
|
|
@@ -4256,7 +4769,7 @@ function tokenize(template) {
|
|
|
4256
4769
|
while (m = re.exec(template)) tokens.push(m[1] ?? m[2]);
|
|
4257
4770
|
return tokens;
|
|
4258
4771
|
}
|
|
4259
|
-
function registerIpc(sessions, fs2, persistence2, sentinels, features, autoExpand, conductor, factory, agents, tokenEff, getWin2) {
|
|
4772
|
+
function registerIpc(sessions, fs2, persistence2, sentinels, features, autoExpand, conductor, factory, agents, tokenEff, gamification, usage, getWin2) {
|
|
4260
4773
|
const rootOf = (id) => {
|
|
4261
4774
|
const config = sessions.getConfig(id);
|
|
4262
4775
|
if (!config) throw new Error(`Unknown session: ${id}`);
|
|
@@ -4314,6 +4827,7 @@ function registerIpc(sessions, fs2, persistence2, sentinels, features, autoExpan
|
|
|
4314
4827
|
(_e, sessionId, path2) => sessions.getGitFileDiff(sessionId, path2)
|
|
4315
4828
|
);
|
|
4316
4829
|
electron.ipcMain.handle("git:branches", (_e, sessionId) => sessions.listBranches(sessionId));
|
|
4830
|
+
electron.ipcMain.handle("git:branchDiff", (_e, sessionId) => sessions.getBranchDiff(sessionId));
|
|
4317
4831
|
electron.ipcMain.handle(
|
|
4318
4832
|
"checkpoint:create",
|
|
4319
4833
|
(_e, sessionId, label) => sessions.createCheckpoint(sessionId, label)
|
|
@@ -4322,6 +4836,10 @@ function registerIpc(sessions, fs2, persistence2, sentinels, features, autoExpan
|
|
|
4322
4836
|
"checkpoint:list",
|
|
4323
4837
|
(_e, sessionId) => sessions.listCheckpoints(sessionId)
|
|
4324
4838
|
);
|
|
4839
|
+
electron.ipcMain.handle(
|
|
4840
|
+
"checkpoint:diff",
|
|
4841
|
+
(_e, sessionId, id) => sessions.getCheckpointDiff(sessionId, id)
|
|
4842
|
+
);
|
|
4325
4843
|
electron.ipcMain.handle(
|
|
4326
4844
|
"checkpoint:restore",
|
|
4327
4845
|
(_e, sessionId, id) => sessions.restoreCheckpoint(sessionId, id)
|
|
@@ -4469,6 +4987,7 @@ function registerIpc(sessions, fs2, persistence2, sentinels, features, autoExpan
|
|
|
4469
4987
|
(_e, id, kind) => factory.createFromSuggestion(id, kind)
|
|
4470
4988
|
);
|
|
4471
4989
|
electron.ipcMain.handle("factory:dismissSuggestion", (_e, id) => factory.dismissSuggestion(id));
|
|
4990
|
+
electron.ipcMain.handle("gamification:get", () => gamification.snapshot());
|
|
4472
4991
|
electron.ipcMain.handle("agents:get", () => agents.snapshot());
|
|
4473
4992
|
electron.ipcMain.handle("agents:refresh", () => agents.refresh());
|
|
4474
4993
|
electron.ipcMain.handle("agents:read", (_e, filePath) => agents.readAgentFile(filePath));
|
|
@@ -4592,10 +5111,16 @@ function registerIpc(sessions, fs2, persistence2, sentinels, features, autoExpan
|
|
|
4592
5111
|
"tokenEff:detectTools",
|
|
4593
5112
|
(_e, refresh) => tokenEff.detectTools(refresh ?? false)
|
|
4594
5113
|
);
|
|
4595
|
-
const
|
|
4596
|
-
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
|
+
});
|
|
4597
5120
|
const usageLimits = new UsageLimitsService();
|
|
4598
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));
|
|
4599
5124
|
electron.ipcMain.handle(
|
|
4600
5125
|
"transcript:export",
|
|
4601
5126
|
async (_e, sessionId, fileName, content) => {
|
|
@@ -4656,7 +5181,15 @@ const DEFAULT_SETTINGS = {
|
|
|
4656
5181
|
watchdogUnansweredMinutes: 5,
|
|
4657
5182
|
tokenEfficiency: DEFAULT_TOKEN_EFFICIENCY,
|
|
4658
5183
|
tokenEfficiencyRepoOverrides: {},
|
|
4659
|
-
agentRegistryPath: "C:\\repos\\agent-factory\\registry\\registry.json"
|
|
5184
|
+
agentRegistryPath: "C:\\repos\\agent-factory\\registry\\registry.json",
|
|
5185
|
+
theme: "dark",
|
|
5186
|
+
accentColor: null,
|
|
5187
|
+
gamificationEnabled: true,
|
|
5188
|
+
gamificationReduceMotion: false,
|
|
5189
|
+
gamificationSound: false,
|
|
5190
|
+
dailyBudgetUSD: null,
|
|
5191
|
+
monthlyBudgetUSD: null,
|
|
5192
|
+
budgetAlertThreshold: 80
|
|
4660
5193
|
};
|
|
4661
5194
|
const DEFAULT_CATEGORIES = [
|
|
4662
5195
|
{
|
|
@@ -4826,9 +5359,11 @@ function gitHead$1(folder) {
|
|
|
4826
5359
|
});
|
|
4827
5360
|
}
|
|
4828
5361
|
class SentinelService {
|
|
4829
|
-
constructor(persistence2, getWin2) {
|
|
5362
|
+
constructor(persistence2, getWin2, emitGame = () => {
|
|
5363
|
+
}) {
|
|
4830
5364
|
this.persistence = persistence2;
|
|
4831
5365
|
this.getWin = getWin2;
|
|
5366
|
+
this.emitGame = emitGame;
|
|
4832
5367
|
}
|
|
4833
5368
|
timer = null;
|
|
4834
5369
|
/** Last observed HEAD per session id; baseline is set without firing. */
|
|
@@ -5008,6 +5543,7 @@ class SentinelService {
|
|
|
5008
5543
|
run.summary = summary;
|
|
5009
5544
|
run.findings = findings;
|
|
5010
5545
|
this.broadcast(run.sessionId);
|
|
5546
|
+
if (status !== "error") this.emitGame({ type: "sentinel.run" });
|
|
5011
5547
|
}
|
|
5012
5548
|
broadcast(sessionId) {
|
|
5013
5549
|
this.getWin()?.webContents.send("sentinel:runs", sessionId, this.listRuns(sessionId));
|
|
@@ -5258,6 +5794,8 @@ const ACTION_SHELL_READY_MS = 1200;
|
|
|
5258
5794
|
const CLAUDE_SUBMIT_DELAY_MS = 300;
|
|
5259
5795
|
const QUEUE_IDLE_DELAY_MS = 3e3;
|
|
5260
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;
|
|
5261
5799
|
const WATCHDOG_TICK_MS = 5e3;
|
|
5262
5800
|
const SCROLLBACK_DIVIDER = "\r\n\x1B[0m\x1B[2m── restored from previous session ──\x1B[0m\r\n\r\n";
|
|
5263
5801
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -5287,11 +5825,13 @@ const STATUS_PRIORITY = [
|
|
|
5287
5825
|
"exited"
|
|
5288
5826
|
];
|
|
5289
5827
|
class SessionManager {
|
|
5290
|
-
constructor(persistence2, fs2, tokenEff, getWin2) {
|
|
5828
|
+
constructor(persistence2, fs2, tokenEff, getWin2, emitGame = () => {
|
|
5829
|
+
}) {
|
|
5291
5830
|
this.persistence = persistence2;
|
|
5292
5831
|
this.fs = fs2;
|
|
5293
5832
|
this.tokenEff = tokenEff;
|
|
5294
5833
|
this.getWin = getWin2;
|
|
5834
|
+
this.emitGame = emitGame;
|
|
5295
5835
|
}
|
|
5296
5836
|
/** Keyed by terminal id, across all sessions. */
|
|
5297
5837
|
ptys = /* @__PURE__ */ new Map();
|
|
@@ -5304,6 +5844,10 @@ class SessionManager {
|
|
|
5304
5844
|
autoCompleteTimers = /* @__PURE__ */ new Map();
|
|
5305
5845
|
/** Worktree session ids with an auto-complete action in flight (fires once). */
|
|
5306
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();
|
|
5307
5851
|
/** On-disk tail of each terminal's output, replayed on app restart. */
|
|
5308
5852
|
scrollback = new ScrollbackStore();
|
|
5309
5853
|
/**
|
|
@@ -5316,6 +5860,8 @@ class SessionManager {
|
|
|
5316
5860
|
watchdog = /* @__PURE__ */ new Map();
|
|
5317
5861
|
/** The watchdog interval; null until startWatchdog() runs. */
|
|
5318
5862
|
watchdogTimer = null;
|
|
5863
|
+
/** Per-terminal last status, used only to award a turn on the working→done edge. */
|
|
5864
|
+
lastStatusForAward = /* @__PURE__ */ new Map();
|
|
5319
5865
|
get state() {
|
|
5320
5866
|
return this.persistence.state;
|
|
5321
5867
|
}
|
|
@@ -5327,12 +5873,12 @@ class SessionManager {
|
|
|
5327
5873
|
}
|
|
5328
5874
|
categoryOf(config) {
|
|
5329
5875
|
if (!config.categoryId) return null;
|
|
5330
|
-
return this.state.categories.find((
|
|
5876
|
+
return this.state.categories.find((c2) => c2.id === config.categoryId) ?? null;
|
|
5331
5877
|
}
|
|
5332
5878
|
/** Every MCP server name owned by any category — our materialization namespace. */
|
|
5333
5879
|
managedServerNames() {
|
|
5334
5880
|
const names = /* @__PURE__ */ new Set();
|
|
5335
|
-
for (const
|
|
5881
|
+
for (const c2 of this.state.categories) for (const s of c2.mcpServers) names.add(s.name);
|
|
5336
5882
|
return [...names];
|
|
5337
5883
|
}
|
|
5338
5884
|
/**
|
|
@@ -5366,14 +5912,15 @@ class SessionManager {
|
|
|
5366
5912
|
claudeArgs: [],
|
|
5367
5913
|
startMode: "continue"
|
|
5368
5914
|
};
|
|
5915
|
+
const terminals = opts?.terminals ?? [claudeTerminal];
|
|
5369
5916
|
const config = {
|
|
5370
5917
|
id: crypto.randomUUID(),
|
|
5371
5918
|
name: opts?.name ?? path.basename(folder) ?? folder,
|
|
5372
5919
|
folder,
|
|
5373
5920
|
color: opts?.color ?? null,
|
|
5374
5921
|
order: Math.max(0, ...this.state.sessions.map((s) => s.order + 1)),
|
|
5375
|
-
terminals
|
|
5376
|
-
activeTerminalId:
|
|
5922
|
+
terminals,
|
|
5923
|
+
activeTerminalId: terminals[0]?.id ?? null,
|
|
5377
5924
|
expandedPaths: [],
|
|
5378
5925
|
categoryId: opts?.categoryId ?? null
|
|
5379
5926
|
};
|
|
@@ -5383,6 +5930,7 @@ class SessionManager {
|
|
|
5383
5930
|
for (const terminal of config.terminals) this.spawnTerminal(config, terminal, "fresh");
|
|
5384
5931
|
this.fs.start(config.id, config.folder, []);
|
|
5385
5932
|
this.notifyChanged();
|
|
5933
|
+
this.emitGame({ type: "session.create" });
|
|
5386
5934
|
return this.toInfo(config);
|
|
5387
5935
|
}
|
|
5388
5936
|
close(id) {
|
|
@@ -5452,6 +6000,19 @@ class SessionManager {
|
|
|
5452
6000
|
if (!config) return { branches: [], current: null, defaultBranch: null };
|
|
5453
6001
|
return listBranches(config.folder);
|
|
5454
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
|
+
}
|
|
5455
6016
|
/**
|
|
5456
6017
|
* Initialize a git repository in a session's folder (so a non-repo session can
|
|
5457
6018
|
* host parallel tasks). Returns the resulting git facts; throws git's message
|
|
@@ -5472,7 +6033,9 @@ class SessionManager {
|
|
|
5472
6033
|
async createCheckpoint(sessionId, label) {
|
|
5473
6034
|
const config = this.getConfig(sessionId);
|
|
5474
6035
|
if (!config) throw new Error("Unknown session");
|
|
5475
|
-
|
|
6036
|
+
const checkpoint = await createCheckpoint(config.folder, label);
|
|
6037
|
+
this.emitGame({ type: "checkpoint.create" });
|
|
6038
|
+
return checkpoint;
|
|
5476
6039
|
}
|
|
5477
6040
|
/** Recent checkpoints for a session's repo, newest first. */
|
|
5478
6041
|
async listCheckpoints(sessionId) {
|
|
@@ -5480,6 +6043,12 @@ class SessionManager {
|
|
|
5480
6043
|
if (!config) return [];
|
|
5481
6044
|
return listCheckpoints(config.folder);
|
|
5482
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
|
+
}
|
|
5483
6052
|
/** Restore a session's working tree back to a checkpoint (guarded, reversible). */
|
|
5484
6053
|
async restoreCheckpoint(sessionId, id) {
|
|
5485
6054
|
const config = this.getConfig(sessionId);
|
|
@@ -5532,13 +6101,15 @@ class SessionManager {
|
|
|
5532
6101
|
console.error("copyWorktreeIncludes failed", err);
|
|
5533
6102
|
}
|
|
5534
6103
|
}
|
|
6104
|
+
const claudeArgs = [];
|
|
6105
|
+
if (opts.plan) claudeArgs.push("--permission-mode", "plan");
|
|
6106
|
+
if (opts.model) claudeArgs.push("--model", opts.model);
|
|
5535
6107
|
const claudeTerminal = {
|
|
5536
6108
|
id: crypto.randomUUID(),
|
|
5537
6109
|
kind: "claude",
|
|
5538
6110
|
title: "claude",
|
|
5539
6111
|
order: 0,
|
|
5540
|
-
|
|
5541
|
-
claudeArgs: opts.model ? ["--model", opts.model] : [],
|
|
6112
|
+
claudeArgs,
|
|
5542
6113
|
startMode: "fresh"
|
|
5543
6114
|
};
|
|
5544
6115
|
const config = {
|
|
@@ -5559,7 +6130,9 @@ class SessionManager {
|
|
|
5559
6130
|
// Default to direct merge; only persist a PR/auto choice when set, so
|
|
5560
6131
|
// existing tasks and the common case stay exactly as before.
|
|
5561
6132
|
...opts.completion && opts.completion !== "merge" ? { completion: opts.completion } : {},
|
|
5562
|
-
...opts.autoComplete ? { autoComplete: true } : {}
|
|
6133
|
+
...opts.autoComplete ? { autoComplete: true } : {},
|
|
6134
|
+
...opts.plan ? { plan: true } : {},
|
|
6135
|
+
...opts.plan && opts.autoAcceptPlan ? { autoAcceptPlan: true } : {}
|
|
5563
6136
|
}
|
|
5564
6137
|
};
|
|
5565
6138
|
this.state.sessions.push(config);
|
|
@@ -5580,6 +6153,7 @@ class SessionManager {
|
|
|
5580
6153
|
}, INITIAL_PROMPT_DELAY_MS);
|
|
5581
6154
|
}
|
|
5582
6155
|
this.notifyChanged();
|
|
6156
|
+
this.emitGame({ type: "worktree.create" });
|
|
5583
6157
|
return this.toInfo(config);
|
|
5584
6158
|
}
|
|
5585
6159
|
/** Live git facts about a worktree task (uncommitted files, commits ahead). */
|
|
@@ -5661,7 +6235,12 @@ ${commit.output}` };
|
|
|
5661
6235
|
}
|
|
5662
6236
|
const result = await mergeBranch(baseFolder, branch, baseBranch);
|
|
5663
6237
|
if (!result.ok) return { ...result, autoCommitted };
|
|
5664
|
-
|
|
6238
|
+
const merged = { ...await this.pushAfterMerge(result, baseFolder, baseBranch), autoCommitted };
|
|
6239
|
+
this.emitGame({ type: "worktree.merge", meta: { commits: ahead ?? 0 } });
|
|
6240
|
+
if (this.state.features.some((f) => f.taskSessionId === sessionId)) {
|
|
6241
|
+
this.emitGame({ type: "feature.merge" });
|
|
6242
|
+
}
|
|
6243
|
+
return merged;
|
|
5665
6244
|
}
|
|
5666
6245
|
/** True if `branch` is the dedicated expansion branch of any auto-expand config. */
|
|
5667
6246
|
isAutoExpandBranch(branch) {
|
|
@@ -5756,15 +6335,16 @@ ${commit.output}` };
|
|
|
5756
6335
|
const title = prTitle(config.name, branch);
|
|
5757
6336
|
const body = prBody(config.name, branch, baseBranch);
|
|
5758
6337
|
const result = await createPullRequest(config.folder, branch, baseBranch, title, body);
|
|
6338
|
+
if (result.ok) this.emitGame({ type: "worktree.pr" });
|
|
5759
6339
|
return { ...result, autoCommitted };
|
|
5760
6340
|
}
|
|
5761
6341
|
// ---------- auto-complete (auto-merge / auto-PR when claude finishes) --------
|
|
5762
6342
|
/**
|
|
5763
6343
|
* (Re)start the auto-complete idle countdown for a worktree task whose claude
|
|
5764
|
-
* just
|
|
5765
|
-
* boot-time
|
|
5766
|
-
* draining or an action is already in flight / done. The fire
|
|
5767
|
-
* 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.
|
|
5768
6348
|
*/
|
|
5769
6349
|
scheduleAutoComplete(sessionId) {
|
|
5770
6350
|
const config = this.getConfig(sessionId);
|
|
@@ -5788,6 +6368,28 @@ ${commit.output}` };
|
|
|
5788
6368
|
if (timer) clearTimeout(timer);
|
|
5789
6369
|
this.autoCompleteTimers.delete(sessionId);
|
|
5790
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
|
+
}
|
|
5791
6393
|
/**
|
|
5792
6394
|
* Run a task's chosen completion automatically. Re-checks the gates that may
|
|
5793
6395
|
* have changed during the countdown (still a worktree task, claude alive and
|
|
@@ -5803,7 +6405,8 @@ ${commit.output}` };
|
|
|
5803
6405
|
if (config.promptQueue?.length) return;
|
|
5804
6406
|
const terminal = this.claudeTargetTerminal(config);
|
|
5805
6407
|
const pty2 = terminal ? this.ptys.get(terminal.id) : null;
|
|
5806
|
-
|
|
6408
|
+
const settled = pty2?.detector.current === "done" || pty2?.detector.current === "idle";
|
|
6409
|
+
if (!pty2?.alive || !settled) return;
|
|
5807
6410
|
const dirty = await dirtyCount(config.folder) ?? 0;
|
|
5808
6411
|
const ahead = await aheadCount(wt.baseFolder, wt.baseBranch, wt.branch) ?? 0;
|
|
5809
6412
|
if (dirty === 0 && ahead === 0) return;
|
|
@@ -5921,6 +6524,7 @@ ${err.message}`
|
|
|
5921
6524
|
this.tokenEff.clearApplied(sessionId);
|
|
5922
6525
|
this.worktreeWorked.delete(sessionId);
|
|
5923
6526
|
this.autoCompleteInFlight.delete(sessionId);
|
|
6527
|
+
this.planAccepted.delete(sessionId);
|
|
5924
6528
|
this.state.sessions = this.state.sessions.filter((s) => s.id !== sessionId);
|
|
5925
6529
|
if (this.state.activeSessionId === sessionId) this.state.activeSessionId = null;
|
|
5926
6530
|
this.persistence.scheduleSave();
|
|
@@ -6040,6 +6644,7 @@ ${err.message}`
|
|
|
6040
6644
|
const action = this.state.actions.find((a) => a.id === actionId);
|
|
6041
6645
|
const command = action?.command.trim();
|
|
6042
6646
|
if (!config || !action || !command) return null;
|
|
6647
|
+
this.emitGame({ type: "action.run" });
|
|
6043
6648
|
if (action.shell === "claude") return this.runClaudeAction(config, command);
|
|
6044
6649
|
let terminal = config.terminals.find((t) => t.actionId === action.id);
|
|
6045
6650
|
let respawned = false;
|
|
@@ -6282,12 +6887,21 @@ ${err.message}`
|
|
|
6282
6887
|
if (!config) return;
|
|
6283
6888
|
const terminal = config.terminals.find((t) => t.id === terminalId);
|
|
6284
6889
|
if (!terminal || terminal.kind !== "claude") return;
|
|
6890
|
+
const prevForAward = this.lastStatusForAward.get(terminalId);
|
|
6891
|
+
this.lastStatusForAward.set(terminalId, status);
|
|
6892
|
+
if (status === "done" && prevForAward === "working") {
|
|
6893
|
+
this.emitGame({ type: "session.turn" });
|
|
6894
|
+
}
|
|
6285
6895
|
if (status === "done" || status === "idle") this.scheduleQueueDispatch(config.id);
|
|
6286
6896
|
if (config.worktree?.autoComplete) {
|
|
6287
6897
|
if (status === "working") this.worktreeWorked.add(config.id);
|
|
6288
|
-
if (status === "idle") this.scheduleAutoComplete(config.id);
|
|
6898
|
+
if (status === "done" || status === "idle") this.scheduleAutoComplete(config.id);
|
|
6289
6899
|
else this.clearAutoCompleteTimer(config.id);
|
|
6290
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
|
+
}
|
|
6291
6905
|
if (status !== "needs-attention") return;
|
|
6292
6906
|
const focused = win2?.isFocused() ?? false;
|
|
6293
6907
|
const isActive = this.state.activeSessionId === config.id && config.activeTerminalId === terminalId;
|
|
@@ -7224,6 +7838,217 @@ class TokenEfficiencyService {
|
|
|
7224
7838
|
}
|
|
7225
7839
|
}
|
|
7226
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
|
+
}
|
|
7227
8052
|
let win = null;
|
|
7228
8053
|
const getWin = () => win;
|
|
7229
8054
|
const persistence = new Persistence();
|
|
@@ -7286,18 +8111,31 @@ if (!gotLock) {
|
|
|
7286
8111
|
electron.app.whenReady().then(() => {
|
|
7287
8112
|
electron.app.setAppUserModelId("com.pedroferreira.maestro");
|
|
7288
8113
|
persistence.load();
|
|
8114
|
+
const gameBus = new events.EventEmitter();
|
|
8115
|
+
const emitGame = (e) => {
|
|
8116
|
+
try {
|
|
8117
|
+
gameBus.emit("game", e);
|
|
8118
|
+
} catch {
|
|
8119
|
+
}
|
|
8120
|
+
};
|
|
7289
8121
|
const fsService = new FsService(
|
|
7290
|
-
(sessionId,
|
|
8122
|
+
(sessionId, events2) => getWin()?.webContents.send("fs:events", sessionId, events2),
|
|
7291
8123
|
() => persistence.state.settings.ignoreNames
|
|
7292
8124
|
);
|
|
7293
8125
|
const tokenEff = new TokenEfficiencyService(persistence);
|
|
7294
|
-
const sessions = new SessionManager(persistence, fsService, tokenEff, getWin);
|
|
7295
|
-
const sentinels = new SentinelService(persistence, getWin);
|
|
7296
|
-
const features = new FeatureService(persistence, sessions);
|
|
7297
|
-
const autoExpand = new AutoExpandService(persistence, features, getWin);
|
|
8126
|
+
const sessions = new SessionManager(persistence, fsService, tokenEff, getWin, emitGame);
|
|
8127
|
+
const sentinels = new SentinelService(persistence, getWin, emitGame);
|
|
8128
|
+
const features = new FeatureService(persistence, sessions, emitGame);
|
|
8129
|
+
const autoExpand = new AutoExpandService(persistence, features, getWin, emitGame);
|
|
7298
8130
|
const conductor = new ConductorService(persistence, sessions, features, autoExpand, getWin);
|
|
7299
|
-
const factory = new FactoryService(getWin);
|
|
8131
|
+
const factory = new FactoryService(getWin, emitGame);
|
|
7300
8132
|
const agentRegistry = new AgentRegistryService(persistence, getWin);
|
|
8133
|
+
const usage = new UsageService();
|
|
8134
|
+
const gamification = new GamificationService(getWin, () => {
|
|
8135
|
+
const t = usage.snapshot().total;
|
|
8136
|
+
return t.inputTokens + t.outputTokens;
|
|
8137
|
+
});
|
|
8138
|
+
gameBus.on("game", (e) => gamification.award(e));
|
|
7301
8139
|
registerIpc(
|
|
7302
8140
|
sessions,
|
|
7303
8141
|
fsService,
|
|
@@ -7309,6 +8147,8 @@ if (!gotLock) {
|
|
|
7309
8147
|
factory,
|
|
7310
8148
|
agentRegistry,
|
|
7311
8149
|
tokenEff,
|
|
8150
|
+
gamification,
|
|
8151
|
+
usage,
|
|
7312
8152
|
getWin
|
|
7313
8153
|
);
|
|
7314
8154
|
createWindow();
|
|
@@ -7318,13 +8158,18 @@ if (!gotLock) {
|
|
|
7318
8158
|
autoExpand.start();
|
|
7319
8159
|
tokenEff.start();
|
|
7320
8160
|
factory.start();
|
|
7321
|
-
|
|
8161
|
+
gamification.start();
|
|
8162
|
+
conductor.onTurnComplete((messages) => {
|
|
8163
|
+
factory.considerConversation(messages);
|
|
8164
|
+
emitGame({ type: "conductor.turn" });
|
|
8165
|
+
});
|
|
7322
8166
|
electron.app.on("activate", () => {
|
|
7323
8167
|
if (electron.BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
7324
8168
|
});
|
|
7325
8169
|
electron.app.on("before-quit", () => {
|
|
7326
8170
|
tokenEff.dispose();
|
|
7327
8171
|
agentRegistry.dispose();
|
|
8172
|
+
gamification.dispose();
|
|
7328
8173
|
factory.dispose();
|
|
7329
8174
|
conductor.dispose();
|
|
7330
8175
|
autoExpand.dispose();
|