@kenkaiiii/ggcoder 5.6.2 → 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.
Files changed (50) hide show
  1. package/dist/app-sidecar.js +124 -2
  2. package/dist/app-sidecar.js.map +1 -1
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +26 -13
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/progress/engine.d.ts +23 -0
  7. package/dist/core/progress/engine.d.ts.map +1 -0
  8. package/dist/core/progress/engine.js +131 -0
  9. package/dist/core/progress/engine.js.map +1 -0
  10. package/dist/core/progress/engine.test.d.ts +2 -0
  11. package/dist/core/progress/engine.test.d.ts.map +1 -0
  12. package/dist/core/progress/engine.test.js +136 -0
  13. package/dist/core/progress/engine.test.js.map +1 -0
  14. package/dist/core/progress/git-xp.d.ts +16 -0
  15. package/dist/core/progress/git-xp.d.ts.map +1 -0
  16. package/dist/core/progress/git-xp.js +106 -0
  17. package/dist/core/progress/git-xp.js.map +1 -0
  18. package/dist/core/progress/git-xp.test.d.ts +2 -0
  19. package/dist/core/progress/git-xp.test.d.ts.map +1 -0
  20. package/dist/core/progress/git-xp.test.js +88 -0
  21. package/dist/core/progress/git-xp.test.js.map +1 -0
  22. package/dist/core/progress/ranks.d.ts +21 -0
  23. package/dist/core/progress/ranks.d.ts.map +1 -0
  24. package/dist/core/progress/ranks.js +141 -0
  25. package/dist/core/progress/ranks.js.map +1 -0
  26. package/dist/core/progress/ranks.test.d.ts +2 -0
  27. package/dist/core/progress/ranks.test.d.ts.map +1 -0
  28. package/dist/core/progress/ranks.test.js +59 -0
  29. package/dist/core/progress/ranks.test.js.map +1 -0
  30. package/dist/core/progress/rebuild.d.ts +7 -0
  31. package/dist/core/progress/rebuild.d.ts.map +1 -0
  32. package/dist/core/progress/rebuild.js +106 -0
  33. package/dist/core/progress/rebuild.js.map +1 -0
  34. package/dist/core/progress/rebuild.test.d.ts +2 -0
  35. package/dist/core/progress/rebuild.test.d.ts.map +1 -0
  36. package/dist/core/progress/rebuild.test.js +72 -0
  37. package/dist/core/progress/rebuild.test.js.map +1 -0
  38. package/dist/core/progress/store.d.ts +35 -0
  39. package/dist/core/progress/store.d.ts.map +1 -0
  40. package/dist/core/progress/store.js +200 -0
  41. package/dist/core/progress/store.js.map +1 -0
  42. package/dist/core/progress/store.test.d.ts +2 -0
  43. package/dist/core/progress/store.test.d.ts.map +1 -0
  44. package/dist/core/progress/store.test.js +108 -0
  45. package/dist/core/progress/store.test.js.map +1 -0
  46. package/dist/core/progress/types.d.ts +108 -0
  47. package/dist/core/progress/types.d.ts.map +1 -0
  48. package/dist/core/progress/types.js +3 -0
  49. package/dist/core/progress/types.js.map +1 -0
  50. package/package.json +4 -4
@@ -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);