@opslane/claude-code-game 0.1.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 (201) hide show
  1. package/dist/cli.d.ts +2 -0
  2. package/dist/cli.js +59 -0
  3. package/dist/cli.js.map +1 -0
  4. package/dist/routes/auth.d.ts +1 -0
  5. package/dist/routes/auth.js +123 -0
  6. package/dist/routes/auth.js.map +1 -0
  7. package/dist/routes/levels.d.ts +44 -0
  8. package/dist/routes/levels.js +78 -0
  9. package/dist/routes/levels.js.map +1 -0
  10. package/dist/routes/sessions.d.ts +17 -0
  11. package/dist/routes/sessions.js +303 -0
  12. package/dist/routes/sessions.js.map +1 -0
  13. package/dist/server.d.ts +2 -0
  14. package/dist/server.js +58 -0
  15. package/dist/server.js.map +1 -0
  16. package/dist/terminal.d.ts +6 -0
  17. package/dist/terminal.js +23 -0
  18. package/dist/terminal.js.map +1 -0
  19. package/dist/verification.d.ts +31 -0
  20. package/dist/verification.js +239 -0
  21. package/dist/verification.js.map +1 -0
  22. package/frontend/assets/index-CNVEnbfs.css +1 -0
  23. package/frontend/assets/index-D70xl9zu.js +27 -0
  24. package/frontend/index.html +14 -0
  25. package/frontend/vite.svg +1 -0
  26. package/keys/v1.pem +9 -0
  27. package/levels/01-context-is-everything/exercise/README.md +152 -0
  28. package/levels/01-context-is-everything/exercise/data/expenses.db +0 -0
  29. package/levels/01-context-is-everything/exercise/database.py +171 -0
  30. package/levels/01-context-is-everything/exercise/docs/FIRECRAWL_QUICKSTART.md +212 -0
  31. package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_01.json +2306 -0
  32. package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_02.json +2394 -0
  33. package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_03.json +2251 -0
  34. package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_04.json +1987 -0
  35. package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_05.json +2229 -0
  36. package/levels/01-context-is-everything/exercise/main.py +97 -0
  37. package/levels/01-context-is-everything/exercise/models.py +141 -0
  38. package/levels/01-context-is-everything/exercise/pyproject.toml +52 -0
  39. package/levels/01-context-is-everything/exercise/reports.py +138 -0
  40. package/levels/01-context-is-everything/exercise/seed_data.py +91 -0
  41. package/levels/01-context-is-everything/exercise/tests/__init__.py +1 -0
  42. package/levels/01-context-is-everything/exercise/tests/conftest.py +69 -0
  43. package/levels/01-context-is-everything/exercise/tests/test_database.py +244 -0
  44. package/levels/01-context-is-everything/exercise/tests/test_models.py +240 -0
  45. package/levels/01-context-is-everything/exercise/tests/test_reports.py +190 -0
  46. package/levels/01-context-is-everything/exercise/utils.py +163 -0
  47. package/levels/01-context-is-everything/lesson.yaml +82 -0
  48. package/levels/02-claude-md/exercise/README.md +152 -0
  49. package/levels/02-claude-md/exercise/data/expenses.db +0 -0
  50. package/levels/02-claude-md/exercise/database.py +171 -0
  51. package/levels/02-claude-md/exercise/main.py +97 -0
  52. package/levels/02-claude-md/exercise/models.py +141 -0
  53. package/levels/02-claude-md/exercise/pyproject.toml +52 -0
  54. package/levels/02-claude-md/exercise/reports.py +138 -0
  55. package/levels/02-claude-md/exercise/seed_data.py +91 -0
  56. package/levels/02-claude-md/exercise/tests/__init__.py +1 -0
  57. package/levels/02-claude-md/exercise/tests/conftest.py +69 -0
  58. package/levels/02-claude-md/exercise/tests/test_database.py +244 -0
  59. package/levels/02-claude-md/exercise/tests/test_models.py +240 -0
  60. package/levels/02-claude-md/exercise/tests/test_reports.py +190 -0
  61. package/levels/02-claude-md/exercise/utils.py +163 -0
  62. package/levels/02-claude-md/lesson.yaml +60 -0
  63. package/levels/03-read-edit-verify/exercise/CLAUDE.md +15 -0
  64. package/levels/03-read-edit-verify/exercise/README.md +152 -0
  65. package/levels/03-read-edit-verify/exercise/data/expenses.db +0 -0
  66. package/levels/03-read-edit-verify/exercise/database.py +171 -0
  67. package/levels/03-read-edit-verify/exercise/main.py +97 -0
  68. package/levels/03-read-edit-verify/exercise/models.py +141 -0
  69. package/levels/03-read-edit-verify/exercise/pyproject.toml +52 -0
  70. package/levels/03-read-edit-verify/exercise/reports.py +138 -0
  71. package/levels/03-read-edit-verify/exercise/seed_data.py +91 -0
  72. package/levels/03-read-edit-verify/exercise/tests/__init__.py +1 -0
  73. package/levels/03-read-edit-verify/exercise/tests/conftest.py +69 -0
  74. package/levels/03-read-edit-verify/exercise/tests/test_database.py +244 -0
  75. package/levels/03-read-edit-verify/exercise/tests/test_models.py +240 -0
  76. package/levels/03-read-edit-verify/exercise/tests/test_reports.py +190 -0
  77. package/levels/03-read-edit-verify/exercise/utils.py +163 -0
  78. package/levels/03-read-edit-verify/lesson.yaml +60 -0
  79. package/levels/04-planning-mode/exercise/README.md +152 -0
  80. package/levels/04-planning-mode/exercise/data/expenses.db +0 -0
  81. package/levels/04-planning-mode/exercise/database.py +171 -0
  82. package/levels/04-planning-mode/exercise/main.py +97 -0
  83. package/levels/04-planning-mode/exercise/models.py +116 -0
  84. package/levels/04-planning-mode/exercise/pyproject.toml +52 -0
  85. package/levels/04-planning-mode/exercise/reports.py +138 -0
  86. package/levels/04-planning-mode/exercise/seed_data.py +91 -0
  87. package/levels/04-planning-mode/exercise/tests/__init__.py +1 -0
  88. package/levels/04-planning-mode/exercise/tests/conftest.py +69 -0
  89. package/levels/04-planning-mode/exercise/tests/test_database.py +244 -0
  90. package/levels/04-planning-mode/exercise/tests/test_expenses.db +0 -0
  91. package/levels/04-planning-mode/exercise/tests/test_models.py +240 -0
  92. package/levels/04-planning-mode/exercise/tests/test_reports.py +190 -0
  93. package/levels/04-planning-mode/exercise/utils.py +163 -0
  94. package/levels/04-planning-mode/lesson.yaml +53 -0
  95. package/levels/05-spec-driven/exercise/README.md +152 -0
  96. package/levels/05-spec-driven/exercise/data/expenses.db +0 -0
  97. package/levels/05-spec-driven/exercise/database.py +171 -0
  98. package/levels/05-spec-driven/exercise/main.py +97 -0
  99. package/levels/05-spec-driven/exercise/models.py +116 -0
  100. package/levels/05-spec-driven/exercise/pyproject.toml +52 -0
  101. package/levels/05-spec-driven/exercise/reports.py +138 -0
  102. package/levels/05-spec-driven/exercise/seed_data.py +91 -0
  103. package/levels/05-spec-driven/exercise/tests/__init__.py +1 -0
  104. package/levels/05-spec-driven/exercise/tests/conftest.py +69 -0
  105. package/levels/05-spec-driven/exercise/tests/test_database.py +244 -0
  106. package/levels/05-spec-driven/exercise/tests/test_expenses.db +0 -0
  107. package/levels/05-spec-driven/exercise/tests/test_models.py +240 -0
  108. package/levels/05-spec-driven/exercise/tests/test_reports.py +190 -0
  109. package/levels/05-spec-driven/exercise/utils.py +163 -0
  110. package/levels/05-spec-driven/lesson.yaml +53 -0
  111. package/levels/06-sub-agents/exercise/README.md +152 -0
  112. package/levels/06-sub-agents/exercise/data/expenses.db +0 -0
  113. package/levels/06-sub-agents/exercise/database.py +171 -0
  114. package/levels/06-sub-agents/exercise/main.py +97 -0
  115. package/levels/06-sub-agents/exercise/models.py +116 -0
  116. package/levels/06-sub-agents/exercise/pyproject.toml +52 -0
  117. package/levels/06-sub-agents/exercise/reports.py +63 -0
  118. package/levels/06-sub-agents/exercise/seed_data.py +91 -0
  119. package/levels/06-sub-agents/exercise/tests/__init__.py +1 -0
  120. package/levels/06-sub-agents/exercise/tests/conftest.py +69 -0
  121. package/levels/06-sub-agents/exercise/tests/test_database.py +244 -0
  122. package/levels/06-sub-agents/exercise/tests/test_models.py +240 -0
  123. package/levels/06-sub-agents/exercise/tests/test_reports.py +190 -0
  124. package/levels/06-sub-agents/exercise/utils.py +163 -0
  125. package/levels/06-sub-agents/lesson.yaml +49 -0
  126. package/levels/07-skills/exercise/README.md +152 -0
  127. package/levels/07-skills/exercise/data/expenses.db +0 -0
  128. package/levels/07-skills/exercise/database.py +171 -0
  129. package/levels/07-skills/exercise/main.py +97 -0
  130. package/levels/07-skills/exercise/models.py +116 -0
  131. package/levels/07-skills/exercise/pyproject.toml +52 -0
  132. package/levels/07-skills/exercise/reports.py +63 -0
  133. package/levels/07-skills/exercise/seed_data.py +91 -0
  134. package/levels/07-skills/exercise/tests/__init__.py +1 -0
  135. package/levels/07-skills/exercise/tests/conftest.py +69 -0
  136. package/levels/07-skills/exercise/tests/test_database.py +244 -0
  137. package/levels/07-skills/exercise/tests/test_models.py +240 -0
  138. package/levels/07-skills/exercise/tests/test_reports.py +190 -0
  139. package/levels/07-skills/exercise/utils.py +163 -0
  140. package/levels/07-skills/lesson.yaml +49 -0
  141. package/levels/08-mcp-servers/exercise/README.md +152 -0
  142. package/levels/08-mcp-servers/exercise/data/expenses.db +0 -0
  143. package/levels/08-mcp-servers/exercise/database.py +171 -0
  144. package/levels/08-mcp-servers/exercise/main.py +97 -0
  145. package/levels/08-mcp-servers/exercise/models.py +116 -0
  146. package/levels/08-mcp-servers/exercise/pyproject.toml +52 -0
  147. package/levels/08-mcp-servers/exercise/reports.py +63 -0
  148. package/levels/08-mcp-servers/exercise/seed_data.py +91 -0
  149. package/levels/08-mcp-servers/exercise/tests/__init__.py +1 -0
  150. package/levels/08-mcp-servers/exercise/tests/conftest.py +69 -0
  151. package/levels/08-mcp-servers/exercise/tests/test_database.py +244 -0
  152. package/levels/08-mcp-servers/exercise/tests/test_models.py +240 -0
  153. package/levels/08-mcp-servers/exercise/tests/test_reports.py +190 -0
  154. package/levels/08-mcp-servers/exercise/utils.py +163 -0
  155. package/levels/08-mcp-servers/lesson.yaml +59 -0
  156. package/levels/09-plugins/exercise/README.md +152 -0
  157. package/levels/09-plugins/exercise/data/expenses.db +0 -0
  158. package/levels/09-plugins/exercise/database.py +171 -0
  159. package/levels/09-plugins/exercise/main.py +97 -0
  160. package/levels/09-plugins/exercise/models.py +116 -0
  161. package/levels/09-plugins/exercise/pyproject.toml +52 -0
  162. package/levels/09-plugins/exercise/reports.py +63 -0
  163. package/levels/09-plugins/exercise/seed_data.py +91 -0
  164. package/levels/09-plugins/exercise/tests/__init__.py +1 -0
  165. package/levels/09-plugins/exercise/tests/conftest.py +69 -0
  166. package/levels/09-plugins/exercise/tests/test_database.py +244 -0
  167. package/levels/09-plugins/exercise/tests/test_models.py +240 -0
  168. package/levels/09-plugins/exercise/tests/test_reports.py +190 -0
  169. package/levels/09-plugins/exercise/utils.py +163 -0
  170. package/levels/09-plugins/lesson.yaml +51 -0
  171. package/levels/10-hooks/exercise/README.md +152 -0
  172. package/levels/10-hooks/exercise/data/expenses.db +0 -0
  173. package/levels/10-hooks/exercise/database.py +171 -0
  174. package/levels/10-hooks/exercise/main.py +97 -0
  175. package/levels/10-hooks/exercise/models.py +116 -0
  176. package/levels/10-hooks/exercise/pyproject.toml +52 -0
  177. package/levels/10-hooks/exercise/reports.py +63 -0
  178. package/levels/10-hooks/exercise/seed_data.py +91 -0
  179. package/levels/10-hooks/exercise/tests/__init__.py +1 -0
  180. package/levels/10-hooks/exercise/tests/conftest.py +69 -0
  181. package/levels/10-hooks/exercise/tests/test_database.py +244 -0
  182. package/levels/10-hooks/exercise/tests/test_models.py +240 -0
  183. package/levels/10-hooks/exercise/tests/test_reports.py +190 -0
  184. package/levels/10-hooks/exercise/utils.py +163 -0
  185. package/levels/10-hooks/lesson.yaml +58 -0
  186. package/levels/11-worktrees/exercise/README.md +152 -0
  187. package/levels/11-worktrees/exercise/data/expenses.db +0 -0
  188. package/levels/11-worktrees/exercise/database.py +171 -0
  189. package/levels/11-worktrees/exercise/main.py +97 -0
  190. package/levels/11-worktrees/exercise/models.py +116 -0
  191. package/levels/11-worktrees/exercise/pyproject.toml +52 -0
  192. package/levels/11-worktrees/exercise/reports.py +63 -0
  193. package/levels/11-worktrees/exercise/seed_data.py +91 -0
  194. package/levels/11-worktrees/exercise/tests/__init__.py +1 -0
  195. package/levels/11-worktrees/exercise/tests/conftest.py +69 -0
  196. package/levels/11-worktrees/exercise/tests/test_database.py +244 -0
  197. package/levels/11-worktrees/exercise/tests/test_models.py +240 -0
  198. package/levels/11-worktrees/exercise/tests/test_reports.py +190 -0
  199. package/levels/11-worktrees/exercise/utils.py +163 -0
  200. package/levels/11-worktrees/lesson.yaml +68 -0
  201. package/package.json +38 -0
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ import { startServer } from "./server.js";
3
+ import open from "open";
4
+ import fs from "fs";
5
+ const args = process.argv.slice(2);
6
+ const noOpen = args.includes("--no-open");
7
+ const portFlag = args.indexOf("--port");
8
+ const preferredPort = portFlag !== -1 ? parseInt(args[portFlag + 1], 10) : 3000;
9
+ // Detect WSL — auto-open doesn't work there
10
+ function isWSL() {
11
+ try {
12
+ return fs.readFileSync("/proc/version", "utf-8").toLowerCase().includes("microsoft");
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ async function tryStart(port) {
19
+ try {
20
+ return await startServer(port);
21
+ }
22
+ catch (err) {
23
+ if (err.code === "EADDRINUSE") {
24
+ throw err;
25
+ }
26
+ throw err;
27
+ }
28
+ }
29
+ async function main() {
30
+ console.log("Starting Claude Code Game...");
31
+ let actualPort;
32
+ try {
33
+ // Try preferred port first
34
+ actualPort = await tryStart(preferredPort);
35
+ }
36
+ catch (err) {
37
+ if (err.code === "EADDRINUSE") {
38
+ console.log(`Port ${preferredPort} is in use, finding an available port...`);
39
+ // Let the OS assign a free port
40
+ actualPort = await tryStart(0);
41
+ }
42
+ else {
43
+ throw err;
44
+ }
45
+ }
46
+ const url = `http://127.0.0.1:${actualPort}`;
47
+ console.log(`Running at ${url}`);
48
+ if (!noOpen && !isWSL()) {
49
+ await open(url);
50
+ }
51
+ else if (isWSL()) {
52
+ console.log("WSL detected — open the URL above in your browser manually.");
53
+ }
54
+ }
55
+ main().catch((err) => {
56
+ console.error("Failed to start:", err);
57
+ process.exit(1);
58
+ });
59
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;AACxC,MAAM,aAAa,GAAG,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAEhF,4CAA4C;AAC5C,SAAS,KAAK;IACZ,IAAI,CAAC;QACH,OAAO,EAAE,CAAC,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IACvF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,KAAK,UAAU,QAAQ,CAAC,IAAY;IAClC,IAAI,CAAC;QACH,OAAO,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YAC9B,MAAM,GAAG,CAAC;QACZ,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;IAE5C,IAAI,UAAkB,CAAC;IACvB,IAAI,CAAC;QACH,2BAA2B;QAC3B,UAAU,GAAG,MAAM,QAAQ,CAAC,aAAa,CAAC,CAAC;IAC7C,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YAC9B,OAAO,CAAC,GAAG,CAAC,QAAQ,aAAa,0CAA0C,CAAC,CAAC;YAC7E,gCAAgC;YAChC,UAAU,GAAG,MAAM,QAAQ,CAAC,CAAC,CAAC,CAAC;QACjC,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED,MAAM,GAAG,GAAG,oBAAoB,UAAU,EAAE,CAAC;IAC7C,OAAO,CAAC,GAAG,CAAC,cAAc,GAAG,EAAE,CAAC,CAAC;IAEjC,IAAI,CAAC,MAAM,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC;QACxB,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;IAClB,CAAC;SAAM,IAAI,KAAK,EAAE,EAAE,CAAC;QACnB,OAAO,CAAC,GAAG,CAAC,6DAA6D,CAAC,CAAC;IAC7E,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC;IACvC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export declare const authRouter: import("express-serve-static-core").Router;
@@ -0,0 +1,123 @@
1
+ import { Router } from "express";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import os from "os";
5
+ import jwt from "jsonwebtoken";
6
+ import { fileURLToPath } from "url";
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const DATA_DIR = path.join(os.homedir(), ".claude-code-game");
9
+ const AUTH_FILE = path.join(DATA_DIR, "auth.json");
10
+ const KEYS_DIR = path.resolve(__dirname, "../../keys");
11
+ // Worker URL — defaults to deployed Cloudflare Worker, overridable via env
12
+ const WORKER_URL = process.env.WORKER_URL || "https://claude-code-game-api.abhishekray07.workers.dev";
13
+ export const authRouter = Router();
14
+ function readAuthFile() {
15
+ try {
16
+ if (!fs.existsSync(AUTH_FILE))
17
+ return null;
18
+ const data = JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8"));
19
+ return data;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ function writeAuthFile(data) {
26
+ fs.mkdirSync(DATA_DIR, { recursive: true });
27
+ // Write with 0600 permissions on Unix
28
+ fs.writeFileSync(AUTH_FILE, JSON.stringify(data), { mode: 0o600 });
29
+ }
30
+ function verifyToken(token) {
31
+ try {
32
+ // Try all public keys in keys/ directory
33
+ if (!fs.existsSync(KEYS_DIR))
34
+ return { valid: false };
35
+ const keyFiles = fs.readdirSync(KEYS_DIR).filter(f => f.endsWith(".pem"));
36
+ for (const keyFile of keyFiles) {
37
+ try {
38
+ const publicKey = fs.readFileSync(path.join(KEYS_DIR, keyFile), "utf-8");
39
+ const payload = jwt.verify(token, publicKey, {
40
+ algorithms: ["RS256"],
41
+ issuer: "claude-code-game-worker",
42
+ audience: "claude-code-game-local",
43
+ });
44
+ return { valid: true, payload };
45
+ }
46
+ catch {
47
+ continue;
48
+ }
49
+ }
50
+ return { valid: false };
51
+ }
52
+ catch {
53
+ return { valid: false };
54
+ }
55
+ }
56
+ // GET /api/auth/status — check if user has valid local token
57
+ authRouter.get("/api/auth/status", (_req, res) => {
58
+ const auth = readAuthFile();
59
+ if (!auth) {
60
+ res.json({ authenticated: false });
61
+ return;
62
+ }
63
+ const { valid, payload } = verifyToken(auth.token);
64
+ if (!valid) {
65
+ res.json({ authenticated: false });
66
+ return;
67
+ }
68
+ res.json({ authenticated: true, token: auth.token, email: payload.sub, name: payload.name });
69
+ });
70
+ // POST /api/auth/request — proxy to Worker
71
+ authRouter.post("/api/auth/request", async (req, res) => {
72
+ try {
73
+ const workerRes = await fetch(`${WORKER_URL}/verify/request`, {
74
+ method: "POST",
75
+ headers: { "Content-Type": "application/json" },
76
+ body: JSON.stringify(req.body),
77
+ });
78
+ const data = await workerRes.json();
79
+ res.status(workerRes.status).json(data);
80
+ }
81
+ catch (err) {
82
+ res.status(503).json({ error: "Auth service unavailable. Please try again later." });
83
+ }
84
+ });
85
+ // POST /api/auth/confirm — proxy to Worker, store token locally
86
+ authRouter.post("/api/auth/confirm", async (req, res) => {
87
+ try {
88
+ const workerRes = await fetch(`${WORKER_URL}/verify/confirm`, {
89
+ method: "POST",
90
+ headers: { "Content-Type": "application/json" },
91
+ body: JSON.stringify(req.body),
92
+ });
93
+ const data = await workerRes.json();
94
+ if (workerRes.ok && data.token) {
95
+ writeAuthFile({ token: data.token, email: data.email, name: data.name });
96
+ }
97
+ res.status(workerRes.status).json(data);
98
+ }
99
+ catch (err) {
100
+ res.status(503).json({ error: "Auth service unavailable. Please try again later." });
101
+ }
102
+ });
103
+ // POST /api/auth/logout — delete local auth file
104
+ authRouter.post("/api/auth/logout", (_req, res) => {
105
+ try {
106
+ if (fs.existsSync(AUTH_FILE))
107
+ fs.unlinkSync(AUTH_FILE);
108
+ }
109
+ catch { }
110
+ res.json({ ok: true });
111
+ });
112
+ // GET /api/leaderboard — proxy to Worker
113
+ authRouter.get("/api/leaderboard", async (_req, res) => {
114
+ try {
115
+ const workerRes = await fetch(`${WORKER_URL}/leaderboard`);
116
+ const data = await workerRes.json();
117
+ res.status(workerRes.status).json(data);
118
+ }
119
+ catch {
120
+ res.json([]); // Return empty if Worker is unreachable
121
+ }
122
+ });
123
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ export declare const levelsRouter: import("express-serve-static-core").Router;
2
+ export interface VerificationRule {
3
+ type: string;
4
+ tool_name?: string;
5
+ min_count?: number;
6
+ path?: string;
7
+ pattern?: string;
8
+ command?: string;
9
+ expected_output?: string;
10
+ description?: string;
11
+ }
12
+ export interface Hint {
13
+ after_minutes: number;
14
+ text: string;
15
+ }
16
+ export interface Level {
17
+ id: string;
18
+ number: number;
19
+ title: string;
20
+ module: string;
21
+ intro: string;
22
+ video?: {
23
+ url: string;
24
+ duration_seconds: number;
25
+ };
26
+ exercise?: {
27
+ intro: string;
28
+ objective: string;
29
+ };
30
+ verification: VerificationRule[];
31
+ hints: Hint[];
32
+ success: string;
33
+ limits: {
34
+ max_duration_minutes: number;
35
+ max_claude_messages: number;
36
+ };
37
+ }
38
+ export declare function loadLevelByNumber(num: number): Level | null;
39
+ export declare function listLevels(): Array<{
40
+ id: string;
41
+ number: number;
42
+ title: string;
43
+ module: string;
44
+ }>;
@@ -0,0 +1,78 @@
1
+ import { Router } from "express";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import YAML from "yaml";
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const LEVELS_DIR = path.resolve(__dirname, "../../levels");
8
+ export const levelsRouter = Router();
9
+ export function loadLevelByNumber(num) {
10
+ const prefix = String(num).padStart(2, "0");
11
+ const entries = fs.readdirSync(LEVELS_DIR, { withFileTypes: true });
12
+ for (const entry of entries) {
13
+ if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
14
+ const lessonPath = path.join(LEVELS_DIR, entry.name, "lesson.yaml");
15
+ if (fs.existsSync(lessonPath)) {
16
+ const raw = YAML.parse(fs.readFileSync(lessonPath, "utf-8"));
17
+ return parseLevel(raw);
18
+ }
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+ export function listLevels() {
24
+ const levels = [];
25
+ const entries = fs.readdirSync(LEVELS_DIR, { withFileTypes: true });
26
+ for (const entry of entries) {
27
+ if (entry.isDirectory() && /^\d{2}-/.test(entry.name)) {
28
+ const lessonPath = path.join(LEVELS_DIR, entry.name, "lesson.yaml");
29
+ if (fs.existsSync(lessonPath)) {
30
+ const raw = YAML.parse(fs.readFileSync(lessonPath, "utf-8"));
31
+ levels.push({ id: raw.id, number: raw.number, title: raw.title, module: raw.module });
32
+ }
33
+ }
34
+ }
35
+ return levels.sort((a, b) => a.number - b.number);
36
+ }
37
+ function parseLevel(data) {
38
+ return {
39
+ id: data.id,
40
+ number: data.number,
41
+ title: data.title,
42
+ module: data.module,
43
+ intro: data.intro,
44
+ video: data.video ? { url: data.video.url, duration_seconds: data.video.duration_seconds } : undefined,
45
+ exercise: data.exercise ? { intro: data.exercise.intro, objective: data.exercise.objective } : undefined,
46
+ verification: (data.verification || []).map((r) => ({
47
+ type: r.type,
48
+ tool_name: r.tool_name,
49
+ min_count: r.min_count,
50
+ path: r.path,
51
+ pattern: r.pattern,
52
+ command: r.command,
53
+ expected_output: r.expected_output,
54
+ description: r.description,
55
+ })),
56
+ hints: (data.hints || []).map((h) => ({ after_minutes: h.after_minutes, text: h.text })),
57
+ success: data.success,
58
+ limits: {
59
+ max_duration_minutes: data.limits?.max_duration_minutes ?? 15,
60
+ max_claude_messages: data.limits?.max_claude_messages ?? 20,
61
+ },
62
+ };
63
+ }
64
+ // GET /api/levels
65
+ levelsRouter.get("/api/levels", (_req, res) => {
66
+ res.json(listLevels());
67
+ });
68
+ // GET /api/levels/:number
69
+ levelsRouter.get("/api/levels/:number", (req, res) => {
70
+ const num = parseInt(req.params.number, 10);
71
+ const level = loadLevelByNumber(num);
72
+ if (!level) {
73
+ res.status(404).json({ detail: `Level ${num} not found` });
74
+ return;
75
+ }
76
+ res.json(level);
77
+ });
78
+ //# sourceMappingURL=levels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"levels.js","sourceRoot":"","sources":["../../src/routes/levels.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { Server } from "http";
2
+ import { Level } from "./levels.js";
3
+ import type { IPty } from "node-pty-prebuilt-multiarch";
4
+ interface Session {
5
+ sessionId: string;
6
+ wsToken: string;
7
+ pty: IPty;
8
+ level: Level;
9
+ levelNumber: number;
10
+ workspaceDir: string;
11
+ completed: boolean;
12
+ lastActivity: number;
13
+ }
14
+ export declare const sessionsRouter: import("express-serve-static-core").Router;
15
+ export declare function getSession(sessionId: string): Session | undefined;
16
+ export declare function setupWebSocket(server: Server): void;
17
+ export {};
@@ -0,0 +1,303 @@
1
+ import { Router } from "express";
2
+ import { WebSocketServer, WebSocket } from "ws";
3
+ import { v4 as uuidv4 } from "uuid";
4
+ import crypto from "crypto";
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import os from "os";
8
+ import { spawnTerminal } from "../terminal.js";
9
+ import { loadLevelByNumber } from "./levels.js";
10
+ import { VerificationEngine } from "../verification.js";
11
+ const DATA_DIR = path.join(os.homedir(), ".claude-code-game");
12
+ const WORKSPACES_DIR = path.join(DATA_DIR, "workspaces");
13
+ const LEVELS_DIR = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../levels");
14
+ const sessions = new Map();
15
+ export const sessionsRouter = Router();
16
+ function safePath(root, relative) {
17
+ const resolved = path.resolve(root, relative);
18
+ if (!resolved.startsWith(path.resolve(root) + path.sep) && resolved !== path.resolve(root)) {
19
+ throw new Error(`Path traversal detected: ${relative}`);
20
+ }
21
+ return resolved;
22
+ }
23
+ function getExerciseDir(levelNumber) {
24
+ const prefix = String(levelNumber).padStart(2, "0");
25
+ const entries = fs.readdirSync(LEVELS_DIR, { withFileTypes: true });
26
+ for (const entry of entries) {
27
+ if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
28
+ const exerciseDir = path.join(LEVELS_DIR, entry.name, "exercise");
29
+ if (fs.existsSync(exerciseDir))
30
+ return exerciseDir;
31
+ }
32
+ }
33
+ return null;
34
+ }
35
+ function copyExerciseFiles(exerciseDir, workspaceDir) {
36
+ const entries = fs.readdirSync(exerciseDir, { withFileTypes: true });
37
+ for (const entry of entries) {
38
+ if (entry.isSymbolicLink())
39
+ continue;
40
+ const src = path.join(exerciseDir, entry.name);
41
+ const dest = path.join(workspaceDir, entry.name);
42
+ if (entry.isDirectory()) {
43
+ fs.cpSync(src, dest, { recursive: true });
44
+ }
45
+ else {
46
+ fs.copyFileSync(src, dest);
47
+ }
48
+ }
49
+ }
50
+ // Report completion to cloud (fire and forget)
51
+ function reportCompletion(levelNumber) {
52
+ try {
53
+ const authPath = path.join(os.homedir(), ".claude-code-game", "auth.json");
54
+ if (!fs.existsSync(authPath))
55
+ return;
56
+ const auth = JSON.parse(fs.readFileSync(authPath, "utf-8"));
57
+ if (!auth.token)
58
+ return;
59
+ const workerUrl = process.env.WORKER_URL || "https://claude-code-game-api.abhishekray07.workers.dev";
60
+ fetch(`${workerUrl}/events`, {
61
+ method: "POST",
62
+ headers: {
63
+ "Content-Type": "application/json",
64
+ "Authorization": `Bearer ${auth.token}`,
65
+ },
66
+ body: JSON.stringify({ level_number: levelNumber }),
67
+ }).catch(() => { }); // fire and forget
68
+ }
69
+ catch { }
70
+ }
71
+ const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
72
+ setInterval(() => {
73
+ const now = Date.now();
74
+ for (const [id, session] of sessions) {
75
+ if (now - session.lastActivity > IDLE_TIMEOUT_MS) {
76
+ session.pty.kill();
77
+ fs.rmSync(session.workspaceDir, { recursive: true, force: true });
78
+ sessions.delete(id);
79
+ }
80
+ }
81
+ }, 60_000);
82
+ process.on("exit", () => {
83
+ for (const [, session] of sessions) {
84
+ try {
85
+ session.pty.kill();
86
+ }
87
+ catch { }
88
+ }
89
+ });
90
+ // POST /api/sessions
91
+ sessionsRouter.post("/api/sessions", (req, res) => {
92
+ const { level_number = 1 } = req.body;
93
+ const level = loadLevelByNumber(level_number);
94
+ if (!level) {
95
+ res.status(404).json({ detail: `Level ${level_number} not found` });
96
+ return;
97
+ }
98
+ const sessionId = uuidv4().slice(0, 8);
99
+ const wsToken = crypto.randomBytes(16).toString("hex");
100
+ const workspaceDir = path.join(WORKSPACES_DIR, sessionId);
101
+ fs.mkdirSync(workspaceDir, { recursive: true });
102
+ const exerciseDir = getExerciseDir(level_number);
103
+ if (exerciseDir) {
104
+ copyExerciseFiles(exerciseDir, workspaceDir);
105
+ }
106
+ let ptyProcess;
107
+ try {
108
+ ptyProcess = spawnTerminal(workspaceDir);
109
+ }
110
+ catch (err) {
111
+ fs.rmSync(workspaceDir, { recursive: true, force: true });
112
+ res.status(500).json({ detail: `Failed to spawn terminal: ${err.message}` });
113
+ return;
114
+ }
115
+ sessions.set(sessionId, {
116
+ sessionId,
117
+ wsToken,
118
+ pty: ptyProcess,
119
+ level,
120
+ levelNumber: level_number,
121
+ workspaceDir,
122
+ completed: false,
123
+ lastActivity: Date.now(),
124
+ });
125
+ res.json({
126
+ session_id: sessionId,
127
+ ws_token: wsToken,
128
+ status: "ready",
129
+ level: {
130
+ number: level.number,
131
+ title: level.title,
132
+ module: level.module,
133
+ intro: level.intro,
134
+ video: level.video ?? null,
135
+ exercise: level.exercise ?? null,
136
+ },
137
+ });
138
+ });
139
+ // DELETE /api/sessions/:sessionId
140
+ sessionsRouter.delete("/api/sessions/:sessionId", (req, res) => {
141
+ const sessionId = req.params.sessionId;
142
+ const session = sessions.get(sessionId);
143
+ if (session) {
144
+ session.pty.kill();
145
+ fs.rmSync(session.workspaceDir, { recursive: true, force: true });
146
+ sessions.delete(sessionId);
147
+ }
148
+ res.json({ session_id: sessionId, status: "stopped" });
149
+ });
150
+ // PATCH /api/sessions/:sessionId/level
151
+ sessionsRouter.patch("/api/sessions/:sessionId/level", (req, res) => {
152
+ const session = sessions.get(req.params.sessionId);
153
+ if (!session) {
154
+ res.status(404).json({ detail: "Session not found" });
155
+ return;
156
+ }
157
+ const { level_number } = req.body;
158
+ const level = loadLevelByNumber(level_number);
159
+ if (!level) {
160
+ res.status(404).json({ detail: `Level ${level_number} not found` });
161
+ return;
162
+ }
163
+ session.pty.kill();
164
+ fs.rmSync(session.workspaceDir, { recursive: true, force: true });
165
+ fs.mkdirSync(session.workspaceDir, { recursive: true });
166
+ const exerciseDir = getExerciseDir(level_number);
167
+ if (exerciseDir) {
168
+ copyExerciseFiles(exerciseDir, session.workspaceDir);
169
+ }
170
+ session.pty = spawnTerminal(session.workspaceDir);
171
+ session.level = level;
172
+ session.levelNumber = level_number;
173
+ session.completed = false;
174
+ session.lastActivity = Date.now();
175
+ res.json({
176
+ level: {
177
+ number: level.number,
178
+ title: level.title,
179
+ module: level.module,
180
+ intro: level.intro,
181
+ video: level.video ?? null,
182
+ exercise: level.exercise ?? null,
183
+ },
184
+ });
185
+ });
186
+ // GET /api/sessions/:sessionId/progress
187
+ sessionsRouter.get("/api/sessions/:sessionId/progress", async (req, res) => {
188
+ const sessionId = req.params.sessionId;
189
+ const session = sessions.get(sessionId);
190
+ if (!session) {
191
+ res.status(404).json({ detail: "Session not found" });
192
+ return;
193
+ }
194
+ session.lastActivity = Date.now();
195
+ const engine = new VerificationEngine(session.workspaceDir);
196
+ const progress = await engine.getProgress(session.level);
197
+ if (progress.completed) {
198
+ session.completed = true;
199
+ reportCompletion(session.levelNumber);
200
+ }
201
+ res.json({
202
+ session_id: session.sessionId,
203
+ level_number: session.levelNumber,
204
+ completed: session.completed,
205
+ progress,
206
+ });
207
+ });
208
+ // GET /api/sessions/:sessionId/status
209
+ sessionsRouter.get("/api/sessions/:sessionId/status", async (req, res) => {
210
+ const sessionId = req.params.sessionId;
211
+ const session = sessions.get(sessionId);
212
+ if (!session) {
213
+ res.status(404).json({ detail: "Session not found" });
214
+ return;
215
+ }
216
+ session.lastActivity = Date.now();
217
+ if (!session.completed) {
218
+ const engine = new VerificationEngine(session.workspaceDir);
219
+ const progress = await engine.getProgress(session.level);
220
+ if (progress.completed) {
221
+ session.completed = true;
222
+ reportCompletion(session.levelNumber);
223
+ }
224
+ }
225
+ res.json({ completed: session.completed });
226
+ });
227
+ export function getSession(sessionId) {
228
+ return sessions.get(sessionId);
229
+ }
230
+ // WebSocket setup
231
+ export function setupWebSocket(server) {
232
+ const wss = new WebSocketServer({ noServer: true });
233
+ server.on("upgrade", (request, socket, head) => {
234
+ const url = new URL(request.url || "", `http://${request.headers.host}`);
235
+ const match = url.pathname.match(/^\/ws\/terminal\/(.+)$/);
236
+ if (!match) {
237
+ socket.destroy();
238
+ return;
239
+ }
240
+ const sessionId = match[1];
241
+ const token = url.searchParams.get("token");
242
+ const session = sessions.get(sessionId);
243
+ if (!session || session.wsToken !== token) {
244
+ socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
245
+ socket.destroy();
246
+ return;
247
+ }
248
+ const origin = request.headers.origin || "";
249
+ if (origin && !origin.match(/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/)) {
250
+ socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
251
+ socket.destroy();
252
+ return;
253
+ }
254
+ wss.handleUpgrade(request, socket, head, (ws) => {
255
+ wss.emit("connection", ws, request, sessionId);
256
+ });
257
+ });
258
+ wss.on("connection", (ws, _req, sessionId) => {
259
+ const session = sessions.get(sessionId);
260
+ if (!session) {
261
+ ws.close(1008, "Session not found");
262
+ return;
263
+ }
264
+ session.lastActivity = Date.now();
265
+ const intro = session.level.intro;
266
+ ws.send("\x1b[2J\x1b[H");
267
+ ws.send("\r\n");
268
+ for (const line of intro.split("\n")) {
269
+ ws.send(line + "\r\n");
270
+ }
271
+ ws.send("\r\n");
272
+ ws.send("\x1b[90m" + "\u2500".repeat(60) + "\x1b[0m\r\n");
273
+ ws.send("\r\n");
274
+ session.pty.onData((data) => {
275
+ if (ws.readyState === WebSocket.OPEN) {
276
+ ws.send(data);
277
+ }
278
+ });
279
+ // Trigger a fresh prompt — the original was emitted before WS connected
280
+ session.pty.write("\n");
281
+ ws.on("message", (data) => {
282
+ const text = typeof data === "string" ? data : data.toString("utf-8");
283
+ session.lastActivity = Date.now();
284
+ if (text.startsWith("{")) {
285
+ try {
286
+ const msg = JSON.parse(text);
287
+ if (typeof msg.cols === "number" && typeof msg.rows === "number") {
288
+ session.pty.resize(msg.cols, msg.rows);
289
+ return;
290
+ }
291
+ }
292
+ catch {
293
+ // Not valid JSON, treat as PTY input
294
+ }
295
+ }
296
+ session.pty.write(text);
297
+ });
298
+ ws.on("close", () => {
299
+ // Session stays alive — user might reconnect
300
+ });
301
+ });
302
+ }
303
+ //# sourceMappingURL=sessions.js.map