@prestyj/cli 5.3.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-sidecar.js +696 -59
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/core/agent-session-compaction.test.js +138 -0
- package/dist/core/agent-session-compaction.test.js.map +1 -1
- package/dist/core/agent-session.d.ts +71 -3
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +280 -47
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/autopilot-cycle.d.ts +69 -17
- package/dist/core/autopilot-cycle.d.ts.map +1 -1
- package/dist/core/autopilot-cycle.js +82 -13
- package/dist/core/autopilot-cycle.js.map +1 -1
- package/dist/core/autopilot-cycle.test.js +206 -10
- package/dist/core/autopilot-cycle.test.js.map +1 -1
- package/dist/core/autopilot-gate.d.ts +48 -22
- package/dist/core/autopilot-gate.d.ts.map +1 -1
- package/dist/core/autopilot-gate.js +110 -1
- package/dist/core/autopilot-gate.js.map +1 -1
- package/dist/core/autopilot-gate.test.js +139 -3
- package/dist/core/autopilot-gate.test.js.map +1 -1
- package/dist/core/autopilot-verdict.d.ts +6 -0
- package/dist/core/autopilot-verdict.d.ts.map +1 -1
- package/dist/core/autopilot-verdict.js +75 -1
- package/dist/core/autopilot-verdict.js.map +1 -1
- package/dist/core/autopilot-verdict.test.js +63 -0
- package/dist/core/autopilot-verdict.test.js.map +1 -1
- package/dist/core/bash-spawn-benchmark.d.ts +2 -0
- package/dist/core/bash-spawn-benchmark.d.ts.map +1 -0
- package/dist/core/bash-spawn-benchmark.js +139 -0
- package/dist/core/bash-spawn-benchmark.js.map +1 -0
- package/dist/core/compaction/compactor.test.js +1 -1
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/fast-apply-benchmark.d.ts +1 -1
- package/dist/core/fast-apply-benchmark.js +2 -2
- package/dist/core/fast-apply-benchmark.js.map +1 -1
- package/dist/core/hashline-edit-benchmark.d.ts +33 -1
- package/dist/core/hashline-edit-benchmark.d.ts.map +1 -1
- package/dist/core/hashline-edit-benchmark.js +193 -21
- package/dist/core/hashline-edit-benchmark.js.map +1 -1
- package/dist/core/hashline-edit-benchmark.test.js +51 -2
- package/dist/core/hashline-edit-benchmark.test.js.map +1 -1
- package/dist/core/mcp/deferred-catalog.d.ts +28 -0
- package/dist/core/mcp/deferred-catalog.d.ts.map +1 -0
- package/dist/core/mcp/deferred-catalog.js +72 -0
- package/dist/core/mcp/deferred-catalog.js.map +1 -0
- package/dist/core/mcp/resolve-stdio.d.ts +19 -3
- package/dist/core/mcp/resolve-stdio.d.ts.map +1 -1
- package/dist/core/mcp/resolve-stdio.js +136 -5
- package/dist/core/mcp/resolve-stdio.js.map +1 -1
- package/dist/core/mcp/resolve-stdio.test.js +82 -1
- package/dist/core/mcp/resolve-stdio.test.js.map +1 -1
- package/dist/core/nolan-context.d.ts +26 -2
- package/dist/core/nolan-context.d.ts.map +1 -1
- package/dist/core/nolan-context.js +46 -4
- package/dist/core/nolan-context.js.map +1 -1
- package/dist/core/nolan-context.test.js +53 -12
- package/dist/core/nolan-context.test.js.map +1 -1
- package/dist/core/nolan-prompt.d.ts +2 -15
- package/dist/core/nolan-prompt.d.ts.map +1 -1
- package/dist/core/nolan-prompt.js +96 -15
- package/dist/core/nolan-prompt.js.map +1 -1
- package/dist/core/nolan-prompt.test.js +102 -7
- package/dist/core/nolan-prompt.test.js.map +1 -1
- package/dist/core/persistent-shell.d.ts +27 -0
- package/dist/core/persistent-shell.d.ts.map +1 -0
- package/dist/core/persistent-shell.js +150 -0
- package/dist/core/persistent-shell.js.map +1 -0
- package/dist/core/persistent-shell.test.d.ts +2 -0
- package/dist/core/persistent-shell.test.d.ts.map +1 -0
- package/dist/core/persistent-shell.test.js +84 -0
- package/dist/core/persistent-shell.test.js.map +1 -0
- package/dist/core/process-manager-dev-server-repro.test.js +34 -6
- package/dist/core/process-manager-dev-server-repro.test.js.map +1 -1
- package/dist/core/progress/engine.d.ts +23 -0
- package/dist/core/progress/engine.d.ts.map +1 -0
- package/dist/core/progress/engine.js +131 -0
- package/dist/core/progress/engine.js.map +1 -0
- package/dist/core/progress/engine.test.d.ts +2 -0
- package/dist/core/progress/engine.test.d.ts.map +1 -0
- package/dist/core/progress/engine.test.js +136 -0
- package/dist/core/progress/engine.test.js.map +1 -0
- package/dist/core/progress/git-xp.d.ts +16 -0
- package/dist/core/progress/git-xp.d.ts.map +1 -0
- package/dist/core/progress/git-xp.js +106 -0
- package/dist/core/progress/git-xp.js.map +1 -0
- package/dist/core/progress/git-xp.test.d.ts +2 -0
- package/dist/core/progress/git-xp.test.d.ts.map +1 -0
- package/dist/core/progress/git-xp.test.js +88 -0
- package/dist/core/progress/git-xp.test.js.map +1 -0
- package/dist/core/progress/ranks.d.ts +21 -0
- package/dist/core/progress/ranks.d.ts.map +1 -0
- package/dist/core/progress/ranks.js +141 -0
- package/dist/core/progress/ranks.js.map +1 -0
- package/dist/core/progress/ranks.test.d.ts +2 -0
- package/dist/core/progress/ranks.test.d.ts.map +1 -0
- package/dist/core/progress/ranks.test.js +59 -0
- package/dist/core/progress/ranks.test.js.map +1 -0
- package/dist/core/progress/rebuild.d.ts +9 -0
- package/dist/core/progress/rebuild.d.ts.map +1 -0
- package/dist/core/progress/rebuild.js +119 -0
- package/dist/core/progress/rebuild.js.map +1 -0
- package/dist/core/progress/rebuild.test.d.ts +2 -0
- package/dist/core/progress/rebuild.test.d.ts.map +1 -0
- package/dist/core/progress/rebuild.test.js +88 -0
- package/dist/core/progress/rebuild.test.js.map +1 -0
- package/dist/core/progress/store.d.ts +35 -0
- package/dist/core/progress/store.d.ts.map +1 -0
- package/dist/core/progress/store.js +200 -0
- package/dist/core/progress/store.js.map +1 -0
- package/dist/core/progress/store.test.d.ts +2 -0
- package/dist/core/progress/store.test.d.ts.map +1 -0
- package/dist/core/progress/store.test.js +108 -0
- package/dist/core/progress/store.test.js.map +1 -0
- package/dist/core/progress/types.d.ts +108 -0
- package/dist/core/progress/types.d.ts.map +1 -0
- package/dist/core/progress/types.js +3 -0
- package/dist/core/progress/types.js.map +1 -0
- package/dist/core/project-discovery.d.ts.map +1 -1
- package/dist/core/project-discovery.js +40 -9
- package/dist/core/project-discovery.js.map +1 -1
- package/dist/core/project-discovery.test.d.ts +2 -0
- package/dist/core/project-discovery.test.d.ts.map +1 -0
- package/dist/core/project-discovery.test.js +104 -0
- package/dist/core/project-discovery.test.js.map +1 -0
- package/dist/core/session-history.d.ts +51 -0
- package/dist/core/session-history.d.ts.map +1 -0
- package/dist/core/session-history.js +156 -0
- package/dist/core/session-history.js.map +1 -0
- package/dist/core/session-history.test.d.ts +2 -0
- package/dist/core/session-history.test.d.ts.map +1 -0
- package/dist/core/session-history.test.js +101 -0
- package/dist/core/session-history.test.js.map +1 -0
- package/dist/core/session-manager.d.ts +34 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +67 -0
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-manager.test.js +140 -1
- package/dist/core/session-manager.test.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +4 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +12 -8
- package/dist/system-prompt.js.map +1 -1
- package/dist/system-prompt.test.js +15 -20
- package/dist/system-prompt.test.js.map +1 -1
- package/dist/tools/bash.d.ts +1 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +27 -2
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +9 -2
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +91 -10
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/edit.test.js +97 -0
- package/dist/tools/edit.test.js.map +1 -1
- package/dist/tools/plan-mode.test.js +25 -0
- package/dist/tools/plan-mode.test.js.map +1 -1
- package/dist/tools/prompt-hints.d.ts.map +1 -1
- package/dist/tools/prompt-hints.js +3 -0
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/subagent.d.ts +2 -0
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +213 -194
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/subagent.test.d.ts +2 -0
- package/dist/tools/subagent.test.d.ts.map +1 -0
- package/dist/tools/subagent.test.js +73 -0
- package/dist/tools/subagent.test.js.map +1 -0
- package/dist/tools/tool-search.d.ts +15 -0
- package/dist/tools/tool-search.d.ts.map +1 -0
- package/dist/tools/tool-search.js +35 -0
- package/dist/tools/tool-search.js.map +1 -0
- package/dist/ui/components/Footer.js +3 -3
- package/dist/ui/components/Footer.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +22 -9
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ProgressFile } from "./types.js";
|
|
2
|
+
export interface ProgressStoreOptions {
|
|
3
|
+
filePath?: string;
|
|
4
|
+
backupPath?: string;
|
|
5
|
+
/** Called when neither the main file nor the backup is usable. */
|
|
6
|
+
rebuild?: () => Promise<ProgressFile | null>;
|
|
7
|
+
}
|
|
8
|
+
export declare function signProgress(file: ProgressFile): string;
|
|
9
|
+
export declare function verifyProgress(file: ProgressFile): boolean;
|
|
10
|
+
declare function dayKey(now: Date): string;
|
|
11
|
+
export declare function createEmptyProgress(now?: Date): ProgressFile;
|
|
12
|
+
/**
|
|
13
|
+
* Read-only peek: main file → backup → null. Never writes — safe for fs.watch
|
|
14
|
+
* reloads that may race an in-flight atomic rename.
|
|
15
|
+
*/
|
|
16
|
+
export declare function peekProgress(opts?: ProgressStoreOptions): Promise<ProgressFile | null>;
|
|
17
|
+
/**
|
|
18
|
+
* Load progress with the recovery chain: main file → backup → rebuild → empty.
|
|
19
|
+
* Never silently zeroes a user if any recoverable state exists.
|
|
20
|
+
*/
|
|
21
|
+
export declare function loadProgress(opts?: ProgressStoreOptions): Promise<ProgressFile>;
|
|
22
|
+
/**
|
|
23
|
+
* Save progress: sign, atomic write under a file lock, and maintain the backup
|
|
24
|
+
* (written when the level increased — caller passes `levelledUp` — or at most once/day).
|
|
25
|
+
*/
|
|
26
|
+
export declare function saveProgress(file: ProgressFile, opts?: ProgressStoreOptions, levelledUp?: boolean): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Read-modify-write under the lock — the safe way for concurrent sidecars to award XP.
|
|
29
|
+
*/
|
|
30
|
+
export declare function updateProgress(mutate: (file: ProgressFile) => Promise<{
|
|
31
|
+
file: ProgressFile;
|
|
32
|
+
levelledUp: boolean;
|
|
33
|
+
}>, opts?: ProgressStoreOptions): Promise<ProgressFile>;
|
|
34
|
+
export { dayKey };
|
|
35
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/core/progress/store.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAK/C,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;CAC9C;AA2BD,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAEvD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAQ1D;AAED,iBAAS,MAAM,CAAC,GAAG,EAAE,IAAI,GAAG,MAAM,CAKjC;AAED,wBAAgB,mBAAmB,CAAC,GAAG,OAAa,GAAG,YAAY,CAgBlE;AAmCD;;;GAGG;AACH,wBAAsB,YAAY,CAAC,IAAI,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAG5F;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,IAAI,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CA4BrF;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,YAAY,EAClB,IAAI,CAAC,EAAE,oBAAoB,EAC3B,UAAU,UAAQ,GACjB,OAAO,CAAC,IAAI,CAAC,CAef;AAWD;;GAEG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,OAAO,CAAC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,CAAC,EACpF,IAAI,CAAC,EAAE,oBAAoB,GAC1B,OAAO,CAAC,YAAY,CAAC,CAyBvB;AAED,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Durable progress persistence: locked atomic writes, HMAC signing, backup + recovery chain.
|
|
2
|
+
//
|
|
3
|
+
// The HMAC key ships in the app bundle, so this is a tamper *deterrent*, not tamper-proof —
|
|
4
|
+
// real integrity would need server-side recomputation (leaderboard phase).
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
import { getAppPaths, withFileLock } from "@prestyj/core";
|
|
9
|
+
const HMAC_KEY = "ezcoder-progress-v1-9f2c4e7a1b8d3f6c";
|
|
10
|
+
const PATCH_ID_CAP = 500;
|
|
11
|
+
function resolvePaths(opts) {
|
|
12
|
+
const paths = getAppPaths();
|
|
13
|
+
return {
|
|
14
|
+
filePath: opts?.filePath ?? paths.progressFile,
|
|
15
|
+
backupPath: opts?.backupPath ?? paths.progressBackupFile,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Stable-key JSON of the file minus `sig`, for signing. */
|
|
19
|
+
function canonicalJson(file) {
|
|
20
|
+
const clone = { ...file };
|
|
21
|
+
delete clone.sig;
|
|
22
|
+
return stableStringify(clone);
|
|
23
|
+
}
|
|
24
|
+
function stableStringify(value) {
|
|
25
|
+
if (value === null || typeof value !== "object")
|
|
26
|
+
return JSON.stringify(value);
|
|
27
|
+
if (Array.isArray(value))
|
|
28
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
29
|
+
const keys = Object.keys(value).sort();
|
|
30
|
+
const parts = keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`);
|
|
31
|
+
return `{${parts.join(",")}}`;
|
|
32
|
+
}
|
|
33
|
+
export function signProgress(file) {
|
|
34
|
+
return crypto.createHmac("sha256", HMAC_KEY).update(canonicalJson(file)).digest("hex");
|
|
35
|
+
}
|
|
36
|
+
export function verifyProgress(file) {
|
|
37
|
+
if (typeof file.sig !== "string" || file.sig.length === 0)
|
|
38
|
+
return false;
|
|
39
|
+
const expected = signProgress(file);
|
|
40
|
+
try {
|
|
41
|
+
return crypto.timingSafeEqual(Buffer.from(file.sig, "hex"), Buffer.from(expected, "hex"));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function dayKey(now) {
|
|
48
|
+
const y = now.getFullYear();
|
|
49
|
+
const m = String(now.getMonth() + 1).padStart(2, "0");
|
|
50
|
+
const d = String(now.getDate()).padStart(2, "0");
|
|
51
|
+
return `${y}-${m}-${d}`;
|
|
52
|
+
}
|
|
53
|
+
export function createEmptyProgress(now = new Date()) {
|
|
54
|
+
const file = {
|
|
55
|
+
v: 1,
|
|
56
|
+
xp: 0,
|
|
57
|
+
createdAt: now.toISOString(),
|
|
58
|
+
totals: { prompts: 0, commits: 0, linesShipped: 0, projects: [] },
|
|
59
|
+
xpBySource: { prompts: 0, commits: 0, streakBonus: 0 },
|
|
60
|
+
streak: { current: 0, best: 0, lastActiveDay: "" },
|
|
61
|
+
rolling: { promptTimes: [], commitTimes: [], dayXp: 0, dayKey: dayKey(now) },
|
|
62
|
+
repos: {},
|
|
63
|
+
patchIds: [],
|
|
64
|
+
lastEvent: null,
|
|
65
|
+
sig: "",
|
|
66
|
+
};
|
|
67
|
+
file.sig = signProgress(file);
|
|
68
|
+
return file;
|
|
69
|
+
}
|
|
70
|
+
function isValidShape(file) {
|
|
71
|
+
if (typeof file !== "object" || file === null)
|
|
72
|
+
return false;
|
|
73
|
+
const f = file;
|
|
74
|
+
return (f.v === 1 &&
|
|
75
|
+
typeof f.xp === "number" &&
|
|
76
|
+
typeof f.createdAt === "string" &&
|
|
77
|
+
typeof f.totals === "object" &&
|
|
78
|
+
typeof f.streak === "object" &&
|
|
79
|
+
typeof f.rolling === "object" &&
|
|
80
|
+
typeof f.sig === "string");
|
|
81
|
+
}
|
|
82
|
+
async function readFileIfValid(filePath) {
|
|
83
|
+
try {
|
|
84
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
85
|
+
const parsed = JSON.parse(raw);
|
|
86
|
+
if (!isValidShape(parsed))
|
|
87
|
+
return null;
|
|
88
|
+
if (!verifyProgress(parsed))
|
|
89
|
+
return null;
|
|
90
|
+
return parsed;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function atomicWrite(filePath, contents) {
|
|
97
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
98
|
+
const tmp = `${filePath}.tmp`;
|
|
99
|
+
await fs.writeFile(tmp, contents, "utf-8");
|
|
100
|
+
await fs.rename(tmp, filePath);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Read-only peek: main file → backup → null. Never writes — safe for fs.watch
|
|
104
|
+
* reloads that may race an in-flight atomic rename.
|
|
105
|
+
*/
|
|
106
|
+
export async function peekProgress(opts) {
|
|
107
|
+
const { filePath, backupPath } = resolvePaths(opts);
|
|
108
|
+
return (await readFileIfValid(filePath)) ?? (await readFileIfValid(backupPath));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Load progress with the recovery chain: main file → backup → rebuild → empty.
|
|
112
|
+
* Never silently zeroes a user if any recoverable state exists.
|
|
113
|
+
*/
|
|
114
|
+
export async function loadProgress(opts) {
|
|
115
|
+
const { filePath, backupPath } = resolvePaths(opts);
|
|
116
|
+
const main = await readFileIfValid(filePath);
|
|
117
|
+
if (main)
|
|
118
|
+
return main;
|
|
119
|
+
const backup = await readFileIfValid(backupPath);
|
|
120
|
+
if (backup) {
|
|
121
|
+
// Restore the main file from backup.
|
|
122
|
+
await saveProgress(backup, opts).catch(() => { });
|
|
123
|
+
return backup;
|
|
124
|
+
}
|
|
125
|
+
if (opts?.rebuild) {
|
|
126
|
+
try {
|
|
127
|
+
const rebuilt = await opts.rebuild();
|
|
128
|
+
if (rebuilt) {
|
|
129
|
+
rebuilt.sig = signProgress(rebuilt);
|
|
130
|
+
await saveProgress(rebuilt, opts).catch(() => { });
|
|
131
|
+
return rebuilt;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// fall through to empty
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const empty = createEmptyProgress();
|
|
139
|
+
await saveProgress(empty, opts).catch(() => { });
|
|
140
|
+
return empty;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Save progress: sign, atomic write under a file lock, and maintain the backup
|
|
144
|
+
* (written when the level increased — caller passes `levelledUp` — or at most once/day).
|
|
145
|
+
*/
|
|
146
|
+
export async function saveProgress(file, opts, levelledUp = false) {
|
|
147
|
+
const { filePath, backupPath } = resolvePaths(opts);
|
|
148
|
+
// Keep the patch-id ring buffer bounded.
|
|
149
|
+
if (file.patchIds.length > PATCH_ID_CAP) {
|
|
150
|
+
file.patchIds = file.patchIds.slice(-PATCH_ID_CAP);
|
|
151
|
+
}
|
|
152
|
+
file.sig = signProgress(file);
|
|
153
|
+
const contents = JSON.stringify(file, null, 2);
|
|
154
|
+
await withFileLock(filePath, async () => {
|
|
155
|
+
await atomicWrite(filePath, contents);
|
|
156
|
+
if (levelledUp || (await backupIsStale(backupPath))) {
|
|
157
|
+
await atomicWrite(backupPath, contents).catch(() => { });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
async function backupIsStale(backupPath) {
|
|
162
|
+
try {
|
|
163
|
+
const stat = await fs.stat(backupPath);
|
|
164
|
+
return Date.now() - stat.mtimeMs > 24 * 60 * 60 * 1000;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return true; // missing → write it
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Read-modify-write under the lock — the safe way for concurrent sidecars to award XP.
|
|
172
|
+
*/
|
|
173
|
+
export async function updateProgress(mutate, opts) {
|
|
174
|
+
const { filePath, backupPath } = resolvePaths(opts);
|
|
175
|
+
return withFileLock(filePath, async () => {
|
|
176
|
+
// Do the recovery read inside the SAME lock used by saveProgress, without
|
|
177
|
+
// calling loadProgress/saveProgress recursively. That keeps multi-sidecar
|
|
178
|
+
// read-modify-write atomic instead of racing a plain save on another lock.
|
|
179
|
+
let current = (await readFileIfValid(filePath)) ?? (await readFileIfValid(backupPath));
|
|
180
|
+
if (!current && opts?.rebuild) {
|
|
181
|
+
current = await opts.rebuild().catch(() => null);
|
|
182
|
+
if (current)
|
|
183
|
+
current.sig = signProgress(current);
|
|
184
|
+
}
|
|
185
|
+
current ??= createEmptyProgress();
|
|
186
|
+
const { file, levelledUp } = await mutate(current);
|
|
187
|
+
if (file.patchIds.length > PATCH_ID_CAP) {
|
|
188
|
+
file.patchIds = file.patchIds.slice(-PATCH_ID_CAP);
|
|
189
|
+
}
|
|
190
|
+
file.sig = signProgress(file);
|
|
191
|
+
const contents = JSON.stringify(file, null, 2);
|
|
192
|
+
await atomicWrite(filePath, contents);
|
|
193
|
+
if (levelledUp || (await backupIsStale(backupPath))) {
|
|
194
|
+
await atomicWrite(backupPath, contents).catch(() => { });
|
|
195
|
+
}
|
|
196
|
+
return file;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
export { dayKey };
|
|
200
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/core/progress/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.test.d.ts","sourceRoot":"","sources":["../../../src/core/progress/store.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { createEmptyProgress, loadProgress, saveProgress, signProgress, updateProgress, verifyProgress, } from "./store.js";
|
|
6
|
+
let dir;
|
|
7
|
+
let opts;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
dir = await fs.mkdtemp(path.join(os.tmpdir(), "gg-progress-"));
|
|
10
|
+
opts = {
|
|
11
|
+
filePath: path.join(dir, "progress.json"),
|
|
12
|
+
backupPath: path.join(dir, "progress.backup.json"),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
describe("HMAC signing", () => {
|
|
19
|
+
it("round-trips sign + verify", () => {
|
|
20
|
+
const file = createEmptyProgress();
|
|
21
|
+
expect(verifyProgress(file)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
it("rejects tampered XP", () => {
|
|
24
|
+
const file = createEmptyProgress();
|
|
25
|
+
file.xp = 999_999;
|
|
26
|
+
expect(verifyProgress(file)).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
it("rejects a missing sig", () => {
|
|
29
|
+
const file = createEmptyProgress();
|
|
30
|
+
file.sig = "";
|
|
31
|
+
expect(verifyProgress(file)).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe("save + load", () => {
|
|
35
|
+
it("round-trips through disk", async () => {
|
|
36
|
+
const file = createEmptyProgress();
|
|
37
|
+
file.xp = 1234;
|
|
38
|
+
await saveProgress(file, opts);
|
|
39
|
+
const loaded = await loadProgress(opts);
|
|
40
|
+
expect(loaded.xp).toBe(1234);
|
|
41
|
+
});
|
|
42
|
+
it("writes a backup on first save", async () => {
|
|
43
|
+
await saveProgress(createEmptyProgress(), opts);
|
|
44
|
+
const backup = JSON.parse(await fs.readFile(opts.backupPath, "utf-8"));
|
|
45
|
+
expect(verifyProgress(backup)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
it("restores from backup when main file is deleted", async () => {
|
|
48
|
+
const file = createEmptyProgress();
|
|
49
|
+
file.xp = 777;
|
|
50
|
+
await saveProgress(file, opts);
|
|
51
|
+
await fs.unlink(opts.filePath);
|
|
52
|
+
const loaded = await loadProgress(opts);
|
|
53
|
+
expect(loaded.xp).toBe(777);
|
|
54
|
+
// Main file restored too.
|
|
55
|
+
const raw = JSON.parse(await fs.readFile(opts.filePath, "utf-8"));
|
|
56
|
+
expect(raw.xp).toBe(777);
|
|
57
|
+
});
|
|
58
|
+
it("restores from backup when main file is hand-edited (bad HMAC)", async () => {
|
|
59
|
+
const file = createEmptyProgress();
|
|
60
|
+
file.xp = 500;
|
|
61
|
+
await saveProgress(file, opts);
|
|
62
|
+
const tampered = JSON.parse(await fs.readFile(opts.filePath, "utf-8"));
|
|
63
|
+
tampered.xp = 999_999;
|
|
64
|
+
await fs.writeFile(opts.filePath, JSON.stringify(tampered));
|
|
65
|
+
const loaded = await loadProgress(opts);
|
|
66
|
+
expect(loaded.xp).toBe(500);
|
|
67
|
+
});
|
|
68
|
+
it("rebuilds when both main and backup are gone", async () => {
|
|
69
|
+
const rebuilt = createEmptyProgress();
|
|
70
|
+
rebuilt.xp = 4242;
|
|
71
|
+
rebuilt.sig = signProgress(rebuilt);
|
|
72
|
+
const loaded = await loadProgress({ ...opts, rebuild: async () => rebuilt });
|
|
73
|
+
expect(loaded.xp).toBe(4242);
|
|
74
|
+
});
|
|
75
|
+
it("falls back to empty when nothing is recoverable", async () => {
|
|
76
|
+
const loaded = await loadProgress(opts);
|
|
77
|
+
expect(loaded.xp).toBe(0);
|
|
78
|
+
expect(loaded.v).toBe(1);
|
|
79
|
+
});
|
|
80
|
+
it("survives corrupt JSON in the main file", async () => {
|
|
81
|
+
await fs.mkdir(dir, { recursive: true });
|
|
82
|
+
await fs.writeFile(opts.filePath, "{not json!!");
|
|
83
|
+
const loaded = await loadProgress(opts);
|
|
84
|
+
expect(loaded.xp).toBe(0);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe("updateProgress", () => {
|
|
88
|
+
it("applies a read-modify-write and re-signs", async () => {
|
|
89
|
+
await saveProgress(createEmptyProgress(), opts);
|
|
90
|
+
const updated = await updateProgress(async (f) => {
|
|
91
|
+
f.xp += 100;
|
|
92
|
+
return { file: f, levelledUp: false };
|
|
93
|
+
}, opts);
|
|
94
|
+
expect(updated.xp).toBe(100);
|
|
95
|
+
expect(verifyProgress(updated)).toBe(true);
|
|
96
|
+
const loaded = await loadProgress(opts);
|
|
97
|
+
expect(loaded.xp).toBe(100);
|
|
98
|
+
});
|
|
99
|
+
it("caps the patch-id ring buffer at 500", async () => {
|
|
100
|
+
const updated = await updateProgress(async (f) => {
|
|
101
|
+
f.patchIds = Array.from({ length: 600 }, (_, i) => `pid-${i}`);
|
|
102
|
+
return { file: f, levelledUp: false };
|
|
103
|
+
}, opts);
|
|
104
|
+
expect(updated.patchIds).toHaveLength(500);
|
|
105
|
+
expect(updated.patchIds[0]).toBe("pid-100");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
//# sourceMappingURL=store.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.test.js","sourceRoot":"","sources":["../../../src/core/progress/store.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** Rank-up event embedded in the file so watcher-side sidecars can re-broadcast it once. */
|
|
2
|
+
export interface LevelUpEvent {
|
|
3
|
+
from: number;
|
|
4
|
+
to: number;
|
|
5
|
+
rankName: string;
|
|
6
|
+
}
|
|
7
|
+
/** Last award event marker — nonce lets other windows dedupe celebrations. */
|
|
8
|
+
export interface ProgressLastEvent {
|
|
9
|
+
nonce: string;
|
|
10
|
+
levelUp: LevelUpEvent | null;
|
|
11
|
+
}
|
|
12
|
+
/** Durable on-disk progress file (~/.ezcoder/progress.json), versioned + HMAC-signed. */
|
|
13
|
+
export interface ProgressFile {
|
|
14
|
+
v: 1;
|
|
15
|
+
xp: number;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
totals: {
|
|
18
|
+
prompts: number;
|
|
19
|
+
commits: number;
|
|
20
|
+
linesShipped: number;
|
|
21
|
+
/** Hashed project cwds the user has earned XP in. */
|
|
22
|
+
projects: string[];
|
|
23
|
+
};
|
|
24
|
+
xpBySource: {
|
|
25
|
+
prompts: number;
|
|
26
|
+
commits: number;
|
|
27
|
+
streakBonus: number;
|
|
28
|
+
};
|
|
29
|
+
streak: {
|
|
30
|
+
current: number;
|
|
31
|
+
best: number;
|
|
32
|
+
/** Local calendar day (YYYY-MM-DD) of the last XP event. */
|
|
33
|
+
lastActiveDay: string;
|
|
34
|
+
};
|
|
35
|
+
rolling: {
|
|
36
|
+
/** Epoch-ms timestamps of prompt awards in the last hour. */
|
|
37
|
+
promptTimes: number[];
|
|
38
|
+
/** Epoch-ms timestamps of XP-earning commits in the last hour. */
|
|
39
|
+
commitTimes: number[];
|
|
40
|
+
/** XP earned so far today (for the daily soft cap). */
|
|
41
|
+
dayXp: number;
|
|
42
|
+
/** Local calendar day (YYYY-MM-DD) dayXp belongs to. */
|
|
43
|
+
dayKey: string;
|
|
44
|
+
};
|
|
45
|
+
/** Per-repo last seen HEAD, keyed by repo-root hash. */
|
|
46
|
+
repos: Record<string, {
|
|
47
|
+
lastHead: string;
|
|
48
|
+
}>;
|
|
49
|
+
/** Ring buffer of git patch-ids already scored (cap 500). */
|
|
50
|
+
patchIds: string[];
|
|
51
|
+
lastEvent: ProgressLastEvent | null;
|
|
52
|
+
/** HMAC-SHA256 of canonical JSON minus this field. */
|
|
53
|
+
sig: string;
|
|
54
|
+
}
|
|
55
|
+
/** One rung of the 50-rank ladder, as sent to the webview. */
|
|
56
|
+
export interface RankLadderEntry {
|
|
57
|
+
level: number;
|
|
58
|
+
name: string;
|
|
59
|
+
tier: number;
|
|
60
|
+
tierName: string;
|
|
61
|
+
effectId: string;
|
|
62
|
+
/** Cumulative XP required to reach this level. */
|
|
63
|
+
xpRequired: number;
|
|
64
|
+
}
|
|
65
|
+
/** What the sidecar broadcasts/serves — the webview renders this verbatim. */
|
|
66
|
+
export interface ProgressSnapshot {
|
|
67
|
+
level: number;
|
|
68
|
+
rankName: string;
|
|
69
|
+
tier: number;
|
|
70
|
+
tierName: string;
|
|
71
|
+
tierGlyph: string;
|
|
72
|
+
effectId: string;
|
|
73
|
+
xp: number;
|
|
74
|
+
xpIntoLevel: number;
|
|
75
|
+
xpForLevel: number;
|
|
76
|
+
/** 0–100 percent toward the next level. */
|
|
77
|
+
percent: number;
|
|
78
|
+
streak: {
|
|
79
|
+
current: number;
|
|
80
|
+
best: number;
|
|
81
|
+
};
|
|
82
|
+
totals: {
|
|
83
|
+
prompts: number;
|
|
84
|
+
commits: number;
|
|
85
|
+
linesShipped: number;
|
|
86
|
+
projects: number;
|
|
87
|
+
};
|
|
88
|
+
xpBySource: {
|
|
89
|
+
prompts: number;
|
|
90
|
+
commits: number;
|
|
91
|
+
streakBonus: number;
|
|
92
|
+
};
|
|
93
|
+
memberSince: string;
|
|
94
|
+
ladder: RankLadderEntry[];
|
|
95
|
+
levelUp: LevelUpEvent | null;
|
|
96
|
+
/** Nonce of the award event this snapshot was produced by (for celebration dedupe). */
|
|
97
|
+
eventNonce: string | null;
|
|
98
|
+
/** True only on the SSE frame sent to the window whose run earned the XP.
|
|
99
|
+
* Gates window-local feedback (sounds, XP chips); absent on GET /progress. */
|
|
100
|
+
origin?: boolean;
|
|
101
|
+
}
|
|
102
|
+
/** A commit that passed detection filters and is ready to be scored. */
|
|
103
|
+
export interface ScoredCommit {
|
|
104
|
+
sha: string;
|
|
105
|
+
patchId: string;
|
|
106
|
+
linesChanged: number;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/progress/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/progress/types.ts"],"names":[],"mappings":"AAAA,6FAA6F"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-discovery.d.ts","sourceRoot":"","sources":["../../src/core/project-discovery.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,aAAa,GAAG,OAAO,CAAC;AAEhE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gEAAgE;IAChE,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA6BrE;
|
|
1
|
+
{"version":3,"file":"project-discovery.d.ts","sourceRoot":"","sources":["../../src/core/project-discovery.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,aAAa,GAAG,OAAO,CAAC;AAEhE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gEAAgE;IAChE,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA6BrE;AAsUD,MAAM,WAAW,aAAa;IAC5B,kBAAkB;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAczF"}
|
|
@@ -50,8 +50,12 @@ function mergeSources(a, b) {
|
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
52
|
* Scan ~/.ezcoder/sessions/. Each session directory's name is the encoded cwd
|
|
53
|
-
* (slashes → underscores)
|
|
54
|
-
*
|
|
53
|
+
* (slashes → underscores), but that encoding is lossy: any real path segment
|
|
54
|
+
* containing a literal underscore round-trips wrong (e.g. `my_app` decodes to
|
|
55
|
+
* `.../my/app`, which doesn't exist, so the project silently vanished from the
|
|
56
|
+
* picker). So — like Claude Code discovery — we read the real cwd out of the
|
|
57
|
+
* session header (`{"type":"session",...,"cwd":"/abs"}`) and only fall back to
|
|
58
|
+
* decoding the directory name when no header carries a cwd.
|
|
55
59
|
*/
|
|
56
60
|
async function discoverGgcoderProjects() {
|
|
57
61
|
const sessionsDir = getAppPaths().sessionsDir;
|
|
@@ -68,15 +72,17 @@ async function discoverGgcoderProjects() {
|
|
|
68
72
|
const mtime = await maxJsonlMtime(dir);
|
|
69
73
|
if (mtime === null)
|
|
70
74
|
continue;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
const rawCwd = (await readFirstFromJsonlDir(dir, ezcoderCwdExtractor)) ?? fallbackUnderscoreDecode(entry);
|
|
76
|
+
if (!rawCwd)
|
|
77
|
+
continue;
|
|
78
|
+
// Normalize traversal segments (e.g. an agent launched with cwd
|
|
79
|
+
// `.../src-tauri/../..`) so the basename isn't a stray "..".
|
|
80
|
+
const cwd = path.resolve(rawCwd);
|
|
81
|
+
if (!(await isDirectory(cwd)))
|
|
76
82
|
continue;
|
|
77
83
|
results.push({
|
|
78
|
-
name: path.basename(
|
|
79
|
-
path:
|
|
84
|
+
name: path.basename(cwd),
|
|
85
|
+
path: cwd,
|
|
80
86
|
lastActiveMs: mtime,
|
|
81
87
|
lastActiveDisplay: formatRelativeTime(mtime),
|
|
82
88
|
sources: ["ezcoder"],
|
|
@@ -84,6 +90,15 @@ async function discoverGgcoderProjects() {
|
|
|
84
90
|
}
|
|
85
91
|
return results;
|
|
86
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Best-effort decode of a ezcoder session directory name back to a cwd, used
|
|
95
|
+
* only when the session files carry no `cwd` header. Lossy by design (literal
|
|
96
|
+
* underscores are indistinguishable from separators); the caller still verifies
|
|
97
|
+
* the result is an existing directory.
|
|
98
|
+
*/
|
|
99
|
+
function fallbackUnderscoreDecode(entry) {
|
|
100
|
+
return "/" + entry.replace(/_/g, "/");
|
|
101
|
+
}
|
|
87
102
|
/**
|
|
88
103
|
* Scan ~/.claude/projects/. Claude Code's directory encoding replaces every
|
|
89
104
|
* "/" with "-", which is genuinely ambiguous — a real dash in a path component
|
|
@@ -225,6 +240,22 @@ const claudeCwdExtractor = (line) => {
|
|
|
225
240
|
}
|
|
226
241
|
return null;
|
|
227
242
|
};
|
|
243
|
+
// ezcoder session files open with a `{"type":"session",...,"cwd":"/abs"}` header
|
|
244
|
+
// that stores the real cwd verbatim. Prefer it over decoding the directory name,
|
|
245
|
+
// whose slash→underscore encoding is lossy for paths containing literal
|
|
246
|
+
// underscores (e.g. `my_app` would wrongly decode to `.../my/app`).
|
|
247
|
+
const ezcoderCwdExtractor = (line) => {
|
|
248
|
+
try {
|
|
249
|
+
const parsed = JSON.parse(line);
|
|
250
|
+
if (parsed.type === "session" && typeof parsed.cwd === "string" && parsed.cwd.startsWith("/")) {
|
|
251
|
+
return parsed.cwd;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// skip malformed
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
};
|
|
228
259
|
const CODEX_CWD_RE = /<cwd>([^<]+)<\/cwd>/;
|
|
229
260
|
const codexCwdExtractor = (line) => {
|
|
230
261
|
// Current format (openai/codex protocol.rs, late-2025+): RolloutLine wraps
|