@kenkaiiii/ggcoder 5.6.3 → 5.7.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 +124 -2
- package/dist/app-sidecar.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 +7 -0
- package/dist/core/progress/rebuild.d.ts.map +1 -0
- package/dist/core/progress/rebuild.js +106 -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 +72 -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/package.json +4 -4
package/dist/app-sidecar.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import http from "node:http";
|
|
15
15
|
import fs from "node:fs/promises";
|
|
16
|
+
import { watch as fsWatch } from "node:fs";
|
|
16
17
|
import os from "node:os";
|
|
17
18
|
import path from "node:path";
|
|
18
19
|
import { randomUUID } from "node:crypto";
|
|
@@ -50,6 +51,11 @@ import { downscaleForPreview, validateVisionImage } from "./utils/image.js";
|
|
|
50
51
|
import { startServeMode } from "./modes/serve-mode.js";
|
|
51
52
|
import { loadTelegramConfig, saveTelegramConfig, verifyBotToken } from "./core/telegram-config.js";
|
|
52
53
|
import { loadServers, addServer, removeServer, getServer, parseMcpAddCommand, MCPClientManager, McpOAuthStore, } from "./core/mcp/index.js";
|
|
54
|
+
import { buildSnapshot, levelForXp, rankForLevel } from "./core/progress/ranks.js";
|
|
55
|
+
import { loadProgress, peekProgress, updateProgress } from "./core/progress/store.js";
|
|
56
|
+
import { awardPrompt, awardCommits } from "./core/progress/engine.js";
|
|
57
|
+
import { detectNewCommits, repoKey } from "./core/progress/git-xp.js";
|
|
58
|
+
import { rebuildFromSessions } from "./core/progress/rebuild.js";
|
|
53
59
|
const ALL_PROVIDERS = [
|
|
54
60
|
"anthropic",
|
|
55
61
|
"xiaomi",
|
|
@@ -495,6 +501,13 @@ async function main() {
|
|
|
495
501
|
// request to its window's session via the `x-gg-session` header (and the
|
|
496
502
|
// `?session=` query for the SSE /events stream).
|
|
497
503
|
const sessions = new Map();
|
|
504
|
+
// XP/rank progress — loaded once per daemon; awards fan out to every window.
|
|
505
|
+
// Each frame is tagged `origin: true` only for the session that earned the
|
|
506
|
+
// XP, so that window alone plays sounds/chips while the rest just re-render.
|
|
507
|
+
const progress = await createProgressManager(paths.agentDir, (snapshot, originId) => {
|
|
508
|
+
for (const ctx of sessions.values())
|
|
509
|
+
ctx.broadcast("progress", { ...snapshot, origin: ctx.id === originId });
|
|
510
|
+
});
|
|
498
511
|
/** Resolve the target session id: the `x-gg-session` header, else a
|
|
499
512
|
* `?session=` query param (used by the SSE /events connection). */
|
|
500
513
|
function sessionIdFromReq(req, url) {
|
|
@@ -538,7 +551,7 @@ async function main() {
|
|
|
538
551
|
const sessionPath = typeof body.sessionPath === "string" && body.sessionPath ? body.sessionPath : undefined;
|
|
539
552
|
const id = randomUUID();
|
|
540
553
|
try {
|
|
541
|
-
const ctx = await createSession({ auth, paths }, { id, cwd: sessionCwd, sessionPath });
|
|
554
|
+
const ctx = await createSession({ auth, paths, progress }, { id, cwd: sessionCwd, sessionPath });
|
|
542
555
|
sessions.set(id, ctx);
|
|
543
556
|
log("INFO", "app-sidecar", "session created", { id, cwd: sessionCwd });
|
|
544
557
|
daemonJson(res, 200, { sessionId: id });
|
|
@@ -563,6 +576,12 @@ async function main() {
|
|
|
563
576
|
daemonJson(res, 200, { ok: true });
|
|
564
577
|
return;
|
|
565
578
|
}
|
|
579
|
+
// Progress is daemon-level so the Home screen can paint before a project
|
|
580
|
+
// session exists; per-session callers still work through the same endpoint.
|
|
581
|
+
if (method === "GET" && url === "/progress") {
|
|
582
|
+
daemonJson(res, 200, progress.snapshot());
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
566
585
|
// ── Per-session delegation ───────────────────────────────────────────
|
|
567
586
|
const id = sessionIdFromReq(req, url);
|
|
568
587
|
const ctx = id ? sessions.get(id) : undefined;
|
|
@@ -641,6 +660,88 @@ function buildKenContext(buildSession, cwd, gitBranch, question, workflowCommand
|
|
|
641
660
|
injectedPrompts,
|
|
642
661
|
});
|
|
643
662
|
}
|
|
663
|
+
async function createProgressManager(agentDir, broadcastAll) {
|
|
664
|
+
// Boot: recovery chain main → backup → one-time session-history rebuild → empty.
|
|
665
|
+
let file = await loadProgress({ rebuild: () => rebuildFromSessions() });
|
|
666
|
+
// Don't re-celebrate an old levelUp event on boot.
|
|
667
|
+
let lastSeenNonce = file.lastEvent?.nonce ?? null;
|
|
668
|
+
log("INFO", "app-sidecar", "progress loaded", {
|
|
669
|
+
xp: String(file.xp),
|
|
670
|
+
level: String(levelForXp(file.xp)),
|
|
671
|
+
});
|
|
672
|
+
function snapshot() {
|
|
673
|
+
return buildSnapshot(file);
|
|
674
|
+
}
|
|
675
|
+
async function awardRun(cwd, runStartedAt, originId) {
|
|
676
|
+
try {
|
|
677
|
+
const now = Date.now();
|
|
678
|
+
const updated = await updateProgress(async (f) => {
|
|
679
|
+
const levelBefore = levelForXp(f.xp);
|
|
680
|
+
awardPrompt(f, now, cwd);
|
|
681
|
+
// Commit XP: probe repo root + HEAD, then score lastHead..HEAD bounded
|
|
682
|
+
// by the run window. First sight of a repo records HEAD, scores nothing.
|
|
683
|
+
const probe = await detectNewCommits(cwd, undefined, runStartedAt);
|
|
684
|
+
if (probe) {
|
|
685
|
+
const key = repoKey(probe.repoRoot);
|
|
686
|
+
const lastHead = f.repos[key]?.lastHead;
|
|
687
|
+
if (lastHead && lastHead !== probe.head) {
|
|
688
|
+
const detected = await detectNewCommits(cwd, lastHead, runStartedAt);
|
|
689
|
+
if (detected && detected.commits.length > 0) {
|
|
690
|
+
awardCommits(f, detected.commits, now, cwd);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
f.repos[key] = { lastHead: probe.head };
|
|
694
|
+
}
|
|
695
|
+
// One combined lastEvent per run so other windows celebrate exactly once.
|
|
696
|
+
const levelAfter = levelForXp(f.xp);
|
|
697
|
+
const levelUp = levelAfter > levelBefore
|
|
698
|
+
? { from: levelBefore, to: levelAfter, rankName: rankForLevel(levelAfter).name }
|
|
699
|
+
: null;
|
|
700
|
+
f.lastEvent = { nonce: randomUUID(), levelUp };
|
|
701
|
+
return { file: f, levelledUp: levelUp !== null };
|
|
702
|
+
});
|
|
703
|
+
file = updated;
|
|
704
|
+
lastSeenNonce = updated.lastEvent?.nonce ?? null;
|
|
705
|
+
broadcastAll(buildSnapshot(updated), originId);
|
|
706
|
+
}
|
|
707
|
+
catch (err) {
|
|
708
|
+
log("DEBUG", "app-sidecar", "progress award failed", {
|
|
709
|
+
message: err instanceof Error ? err.message : String(err),
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
// Watch ~/.gg (dir watch survives the atomic tmp+rename) for progress.json
|
|
714
|
+
// writes from other daemon processes; debounce, reload read-only, dedupe by nonce.
|
|
715
|
+
let watchDebounce = null;
|
|
716
|
+
try {
|
|
717
|
+
const watcher = fsWatch(agentDir, (_event, filename) => {
|
|
718
|
+
if (filename !== "progress.json")
|
|
719
|
+
return;
|
|
720
|
+
if (watchDebounce)
|
|
721
|
+
clearTimeout(watchDebounce);
|
|
722
|
+
watchDebounce = setTimeout(() => {
|
|
723
|
+
void (async () => {
|
|
724
|
+
const reloaded = await peekProgress();
|
|
725
|
+
if (!reloaded)
|
|
726
|
+
return;
|
|
727
|
+
const nonce = reloaded.lastEvent?.nonce ?? null;
|
|
728
|
+
if (nonce === lastSeenNonce)
|
|
729
|
+
return;
|
|
730
|
+
file = reloaded;
|
|
731
|
+
lastSeenNonce = nonce;
|
|
732
|
+
broadcastAll(buildSnapshot(reloaded));
|
|
733
|
+
})();
|
|
734
|
+
}, 150);
|
|
735
|
+
});
|
|
736
|
+
watcher.unref();
|
|
737
|
+
}
|
|
738
|
+
catch (err) {
|
|
739
|
+
log("DEBUG", "app-sidecar", "progress watch unavailable", {
|
|
740
|
+
message: err instanceof Error ? err.message : String(err),
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
return { snapshot, awardRun };
|
|
744
|
+
}
|
|
644
745
|
/**
|
|
645
746
|
* Build one in-process agent session: its AgentSession, SSE client set, event
|
|
646
747
|
* bridge, task runner, auth/login bridge, and the full HTTP route table exposed
|
|
@@ -649,7 +750,7 @@ function buildKenContext(buildSession, cwd, gitBranch, question, workflowCommand
|
|
|
649
750
|
* logger, PATH, shared auth file, and radio live at the daemon level.
|
|
650
751
|
*/
|
|
651
752
|
async function createSession(deps, opts) {
|
|
652
|
-
const { auth } = deps;
|
|
753
|
+
const { auth, progress } = deps;
|
|
653
754
|
const paths = deps.paths;
|
|
654
755
|
const cwd = opts.cwd;
|
|
655
756
|
// Base host for parsing request-URL query params (value is irrelevant to
|
|
@@ -812,6 +913,9 @@ async function createSession(deps, opts) {
|
|
|
812
913
|
session.eventBus.on("compaction_end", (d) => broadcast("compaction_end", d));
|
|
813
914
|
let running = false;
|
|
814
915
|
let titleGenerated = false;
|
|
916
|
+
// Bumped by /cancel — a run whose cancel generation changed mid-flight was
|
|
917
|
+
// canceled and earns no XP.
|
|
918
|
+
let cancelGeneration = 0;
|
|
815
919
|
// Autopilot (auto-review) toggle for THIS window's project. Loaded from
|
|
816
920
|
// gg-app.json on boot; flipped via POST /autopilot. When on, POST /prompt runs
|
|
817
921
|
// runAutopilotCycle after the user's turn settles — Ken auto-reviews the work
|
|
@@ -1020,15 +1124,28 @@ async function createSession(deps, opts) {
|
|
|
1020
1124
|
// run_start frame.
|
|
1021
1125
|
async function runAgent(label, run) {
|
|
1022
1126
|
running = true;
|
|
1127
|
+
// Progress (Ranks): completed, non-canceled runs with ≥1 assistant turn earn
|
|
1128
|
+
// XP — prompt + any commits authored during the run window.
|
|
1129
|
+
const runStartedAt = Date.now();
|
|
1130
|
+
const cancelGenAtStart = cancelGeneration;
|
|
1131
|
+
const assistantsBeforeRun = countAssistantMessages(session.getMessages());
|
|
1132
|
+
let runSucceeded = false;
|
|
1023
1133
|
broadcast("run_start", { text: label });
|
|
1024
1134
|
try {
|
|
1025
1135
|
await run();
|
|
1136
|
+
runSucceeded = true;
|
|
1026
1137
|
}
|
|
1027
1138
|
catch (err) {
|
|
1028
1139
|
broadcastError("error", "run failed", err);
|
|
1029
1140
|
}
|
|
1030
1141
|
finally {
|
|
1031
1142
|
running = false;
|
|
1143
|
+
if (runSucceeded &&
|
|
1144
|
+
cancelGeneration === cancelGenAtStart &&
|
|
1145
|
+
countAssistantMessages(session.getMessages()) > assistantsBeforeRun) {
|
|
1146
|
+
// Fire-and-forget — XP must never delay or break run teardown.
|
|
1147
|
+
void progress.awardRun(cwd, runStartedAt, opts.id);
|
|
1148
|
+
}
|
|
1032
1149
|
// A run may have switched branches (git checkout) or spawned/finished
|
|
1033
1150
|
// background tasks — refresh the footer extras once it settles.
|
|
1034
1151
|
gitBranch = await getGitBranch(cwd).catch(() => gitBranch);
|
|
@@ -1336,6 +1453,10 @@ async function createSession(deps, opts) {
|
|
|
1336
1453
|
});
|
|
1337
1454
|
return;
|
|
1338
1455
|
}
|
|
1456
|
+
if (method === "GET" && url === "/progress") {
|
|
1457
|
+
json(res, 200, progress.snapshot());
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1339
1460
|
if (method === "GET" && (url === "/events" || url.startsWith("/events?"))) {
|
|
1340
1461
|
res.writeHead(200, {
|
|
1341
1462
|
"content-type": "text/event-stream",
|
|
@@ -2146,6 +2267,7 @@ async function createSession(deps, opts) {
|
|
|
2146
2267
|
return;
|
|
2147
2268
|
}
|
|
2148
2269
|
if (method === "POST" && url === "/cancel") {
|
|
2270
|
+
cancelGeneration++;
|
|
2149
2271
|
abort.abort();
|
|
2150
2272
|
abort = new AbortController();
|
|
2151
2273
|
session.setSignal(abort.signal);
|