vibe-roast 0.98.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 (164) hide show
  1. package/README.md +418 -0
  2. package/assests/README.md +43 -0
  3. package/assests/badges/.gitkeep +0 -0
  4. package/assests/badges/01-builder/builder-badge.png +0 -0
  5. package/assests/badges/01-builder/builder-badge.svg +4 -0
  6. package/assests/badges/02-debugger/debugger-badge.png +0 -0
  7. package/assests/badges/02-debugger/debugger-badge.svg +4 -0
  8. package/assests/badges/03-architect/architect-badge.png +0 -0
  9. package/assests/badges/03-architect/architect-badge.svg +4 -0
  10. package/assests/badges/04-codex-believer/codex-believer-badge.png +0 -0
  11. package/assests/badges/04-codex-believer/codex-believer-badge.svg +4 -0
  12. package/assests/badges/05-prompt-priest/prompt-priest-badge.png +0 -0
  13. package/assests/badges/05-prompt-priest/prompt-priest-badge.svg +4 -0
  14. package/assests/badges/06-tab-hoarder/tab-hoarder-badge.png +0 -0
  15. package/assests/badges/06-tab-hoarder/tab-hoarder-badge.svg +4 -0
  16. package/assests/badges/07-context-maxxer/context-maxxer-badge.png +0 -0
  17. package/assests/badges/07-context-maxxer/context-maxxer-badge.svg +4 -0
  18. package/assests/badges/08-yolo-shipper/yolo-shipper-badge.png +0 -0
  19. package/assests/badges/08-yolo-shipper/yolo-shipper-badge.svg +4 -0
  20. package/assests/banners/.gitkeep +0 -0
  21. package/assests/banners/home-hero.png +0 -0
  22. package/assests/banners/home-hero.svg +3 -0
  23. package/assests/banners/result-header.png +0 -0
  24. package/assests/banners/result-header.svg +3 -0
  25. package/assests/banners/share-strip.png +0 -0
  26. package/assests/banners/share-strip.svg +3 -0
  27. package/assests/characters/01-builder/builder-card.sbti-archive.svg +5 -0
  28. package/assests/characters/01-builder/builder-card.svg +3 -0
  29. package/assests/characters/01-builder/builder-character.sbti-archive.svg +3 -0
  30. package/assests/characters/01-builder/builder-character.svg +3 -0
  31. package/assests/characters/01-builder/builder-figure.png +0 -0
  32. package/assests/characters/01-builder/builder-mascot.png +0 -0
  33. package/assests/characters/02-debugger/debugger-card.sbti-archive.svg +5 -0
  34. package/assests/characters/02-debugger/debugger-card.svg +3 -0
  35. package/assests/characters/02-debugger/debugger-character.sbti-archive.svg +3 -0
  36. package/assests/characters/02-debugger/debugger-character.svg +3 -0
  37. package/assests/characters/02-debugger/debugger-figure.png +0 -0
  38. package/assests/characters/02-debugger/debugger-mascot.png +0 -0
  39. package/assests/characters/03-architect/architect-card.sbti-archive.svg +5 -0
  40. package/assests/characters/03-architect/architect-card.svg +3 -0
  41. package/assests/characters/03-architect/architect-character.sbti-archive.svg +3 -0
  42. package/assests/characters/03-architect/architect-character.svg +3 -0
  43. package/assests/characters/03-architect/architect-figure.png +0 -0
  44. package/assests/characters/03-architect/architect-mascot.png +0 -0
  45. package/assests/characters/04-codex-believer/codex-believer-card.sbti-archive.svg +5 -0
  46. package/assests/characters/04-codex-believer/codex-believer-card.svg +3 -0
  47. package/assests/characters/04-codex-believer/codex-believer-character.sbti-archive.svg +3 -0
  48. package/assests/characters/04-codex-believer/codex-believer-character.svg +3 -0
  49. package/assests/characters/04-codex-believer/codex-believer-figure.png +0 -0
  50. package/assests/characters/04-codex-believer/codex-believer-mascot.png +0 -0
  51. package/assests/characters/05-prompt-priest/prompt-priest-card.sbti-archive.svg +5 -0
  52. package/assests/characters/05-prompt-priest/prompt-priest-card.svg +3 -0
  53. package/assests/characters/05-prompt-priest/prompt-priest-character.sbti-archive.svg +3 -0
  54. package/assests/characters/05-prompt-priest/prompt-priest-character.svg +3 -0
  55. package/assests/characters/05-prompt-priest/prompt-priest-figure.png +0 -0
  56. package/assests/characters/05-prompt-priest/prompt-priest-mascot.png +0 -0
  57. package/assests/characters/06-tab-hoarder/tab-hoarder-card.sbti-archive.svg +5 -0
  58. package/assests/characters/06-tab-hoarder/tab-hoarder-card.svg +3 -0
  59. package/assests/characters/06-tab-hoarder/tab-hoarder-character.sbti-archive.svg +3 -0
  60. package/assests/characters/06-tab-hoarder/tab-hoarder-character.svg +3 -0
  61. package/assests/characters/06-tab-hoarder/tab-hoarder-figure.png +0 -0
  62. package/assests/characters/06-tab-hoarder/tab-hoarder-mascot.png +0 -0
  63. package/assests/characters/07-context-maxxer/context-maxxer-card.sbti-archive.svg +5 -0
  64. package/assests/characters/07-context-maxxer/context-maxxer-card.svg +3 -0
  65. package/assests/characters/07-context-maxxer/context-maxxer-character.sbti-archive.svg +3 -0
  66. package/assests/characters/07-context-maxxer/context-maxxer-character.svg +3 -0
  67. package/assests/characters/07-context-maxxer/context-maxxer-figure.png +0 -0
  68. package/assests/characters/07-context-maxxer/context-maxxer-mascot.png +0 -0
  69. package/assests/characters/08-yolo-shipper/yolo-shipper-card.sbti-archive.svg +5 -0
  70. package/assests/characters/08-yolo-shipper/yolo-shipper-card.svg +3 -0
  71. package/assests/characters/08-yolo-shipper/yolo-shipper-character.sbti-archive.svg +3 -0
  72. package/assests/characters/08-yolo-shipper/yolo-shipper-character.svg +3 -0
  73. package/assests/characters/08-yolo-shipper/yolo-shipper-figure.png +0 -0
  74. package/assests/characters/08-yolo-shipper/yolo-shipper-mascot.png +0 -0
  75. package/assests/characters-vibe-types/aosf-strategy-shipper/strategy-shipper-figure.png +0 -0
  76. package/assests/characters-vibe-types/aosx-agent-believer/agent-believer-figure.png +0 -0
  77. package/assests/characters-vibe-types/aovf-systems-architect/systems-architect-figure.png +0 -0
  78. package/assests/characters-vibe-types/aovx-context-cartographer/context-cartographer-figure.png +0 -0
  79. package/assests/characters-vibe-types/apsf-diagram-sprinter/diagram-sprinter-figure.png +0 -0
  80. package/assests/characters-vibe-types/apsx-infinite-planner/infinite-planner-figure.png +0 -0
  81. package/assests/characters-vibe-types/apvf-architect/architect-figure.png +0 -0
  82. package/assests/characters-vibe-types/apvx-prompt-priest/prompt-priest-figure.png +0 -0
  83. package/assests/characters-vibe-types/mosf-yolo-shipper/yolo-shipper-figure.png +0 -0
  84. package/assests/characters-vibe-types/mosx-agent-commander/agent-commander-figure.png +0 -0
  85. package/assests/characters-vibe-types/movf-agent-engineer/agent-engineer-figure.png +0 -0
  86. package/assests/characters-vibe-types/movx-systems-wrangler/systems-wrangler-figure.png +0 -0
  87. package/assests/characters-vibe-types/mpsf-builder/builder-figure.png +0 -0
  88. package/assests/characters-vibe-types/mpsx-tab-hoarder/tab-hoarder-figure.png +0 -0
  89. package/assests/characters-vibe-types/mpvf-debugger/debugger-figure.png +0 -0
  90. package/assests/characters-vibe-types/mpvx-context-maxxer/context-maxxer-figure.png +0 -0
  91. package/assests/live-report.html +1403 -0
  92. package/assests/notes/live-inspect-summary.json +3487 -0
  93. package/assests/scripts/compose-banners.py +148 -0
  94. package/assests/scripts/live-report-boot.js +2 -0
  95. package/assests/scripts/profiles.json +104 -0
  96. package/assests/scripts/qa-faces.mjs +72 -0
  97. package/assests/scripts/roast-extras.js +619 -0
  98. package/assests/scripts/score-engine.js +62 -0
  99. package/assests/scripts/split-badges.py +49 -0
  100. package/assests/scripts/split-reference.py +38 -0
  101. package/assests/scripts/sync-banners.mjs +45 -0
  102. package/assests/scripts/sync-cards.mjs +50 -0
  103. package/assests/scripts/sync-raster.mjs +118 -0
  104. package/assests/scripts/vibe-types.json +22 -0
  105. package/assests/source/design-system.md +50 -0
  106. package/assests/source/prompts/01-builder.md +19 -0
  107. package/assests/source/prompts/02-debugger.md +17 -0
  108. package/assests/source/prompts/03-architect.md +16 -0
  109. package/assests/source/prompts/04-codex-believer.md +13 -0
  110. package/assests/source/prompts/05-prompt-priest.md +13 -0
  111. package/assests/source/prompts/06-tab-hoarder.md +13 -0
  112. package/assests/source/prompts/07-context-maxxer.md +13 -0
  113. package/assests/source/prompts/08-yolo-shipper.md +13 -0
  114. package/assests/source/references/sbti-style-reference-board.png +0 -0
  115. package/assests/source/references/vibe-badges-v6-board.png +0 -0
  116. package/assests/source/references/vibe-mascots-v4-reference.png +0 -0
  117. package/assests/source/references/vibe-profiles-v5-board-a.png +0 -0
  118. package/assests/source/references/vibe-profiles-v5-board-b.png +0 -0
  119. package/assests/source/vibe-types-visual-brief.md +38 -0
  120. package/bin/hook.js +121 -0
  121. package/bin/vibe-roast.js +98 -0
  122. package/bin/vibe-wrapper.js +4 -0
  123. package/dashboard/dist/assets/index-DlBycTcO.css +1 -0
  124. package/dashboard/dist/assets/index-t8m64Hm9.js +168 -0
  125. package/dashboard/dist/index.html +32 -0
  126. package/package.json +58 -0
  127. package/src/extract/environment.js +160 -0
  128. package/src/extract/phrase-stats.js +684 -0
  129. package/src/extract/prompt-analysis.js +368 -0
  130. package/src/extract/text.js +30 -0
  131. package/src/hooks/install.js +184 -0
  132. package/src/inspect.js +261 -0
  133. package/src/lib/activity-metrics.js +238 -0
  134. package/src/lib/agent-score.js +187 -0
  135. package/src/lib/ai-roast.js +446 -0
  136. package/src/lib/dates.js +43 -0
  137. package/src/lib/hashtags.js +150 -0
  138. package/src/lib/jsonl.js +48 -0
  139. package/src/lib/load-env.js +59 -0
  140. package/src/lib/roast-evidence.js +186 -0
  141. package/src/lib/roast-snapshot.js +58 -0
  142. package/src/server.js +644 -0
  143. package/src/sources/aider.js +150 -0
  144. package/src/sources/amazonq.js +113 -0
  145. package/src/sources/antigravity.js +147 -0
  146. package/src/sources/claude.js +262 -0
  147. package/src/sources/cline.js +11 -0
  148. package/src/sources/codex.js +222 -0
  149. package/src/sources/common.js +133 -0
  150. package/src/sources/continue.js +67 -0
  151. package/src/sources/copilot.js +170 -0
  152. package/src/sources/cursor.js +238 -0
  153. package/src/sources/gemini.js +75 -0
  154. package/src/sources/index.js +61 -0
  155. package/src/sources/roo.js +11 -0
  156. package/src/sources/token-tracker.js +171 -0
  157. package/src/sources/vibe-tracker.js +79 -0
  158. package/src/sources/vscode-tasks.js +142 -0
  159. package/src/sources/windsurf.js +109 -0
  160. package/worker/README.md +96 -0
  161. package/worker/package-lock.json +1547 -0
  162. package/worker/package.json +20 -0
  163. package/worker/src/index.mjs +694 -0
  164. package/worker/wrangler.jsonc +40 -0
@@ -0,0 +1,142 @@
1
+ const path = require("node:path");
2
+
3
+ const { walkFiles } = require("../lib/jsonl");
4
+ const { isInRange } = require("../lib/dates");
5
+ const {
6
+ emptyTotals,
7
+ emptyReport,
8
+ addTotals,
9
+ usageFromObject,
10
+ cleanText,
11
+ toIsoTimestamp,
12
+ readJsonFile,
13
+ resolveVscodeGlobalStorage,
14
+ } = require("./common");
15
+
16
+ async function inspectVscodeTaskStore({
17
+ source,
18
+ extensionId,
19
+ root,
20
+ range,
21
+ home,
22
+ platform,
23
+ env,
24
+ appNames = ["Code", "Code - Insiders", "Cursor", "Windsurf"],
25
+ } = {}) {
26
+ const roots = root
27
+ ? [root]
28
+ : appNames.map((appName) =>
29
+ resolveVscodeGlobalStorage({ home, platform, env, appName, parts: [extensionId, "tasks"] }),
30
+ );
31
+
32
+ const prompts = [];
33
+ const tokenTotals = emptyTotals();
34
+ let filesScanned = 0;
35
+ const notes = [];
36
+ let usedRoot = roots[0] || null;
37
+
38
+ for (const tasksRoot of roots) {
39
+ const files = await walkFiles(
40
+ tasksRoot,
41
+ (_filePath, name) =>
42
+ name === "ui_messages.json" ||
43
+ name === "api_conversation_history.json" ||
44
+ name.endsWith(".json"),
45
+ );
46
+ if (!files.length) continue;
47
+ usedRoot = tasksRoot;
48
+ for (const file of files) {
49
+ const base = path.basename(file);
50
+ if (
51
+ base !== "ui_messages.json" &&
52
+ base !== "api_conversation_history.json" &&
53
+ !/messages?/i.test(base)
54
+ ) {
55
+ continue;
56
+ }
57
+ filesScanned += 1;
58
+ const data = await readJsonFile(file);
59
+ const rows = normalizeMessageRows(data);
60
+ for (const row of rows) {
61
+ const text = extractTaskUserText(row);
62
+ if (!text) continue;
63
+ const timestamp = toIsoTimestamp(row.ts || row.timestamp || row.time || row.createdAt || row.date);
64
+ if (!isInRange(timestamp, range)) continue;
65
+ prompts.push({
66
+ source,
67
+ timestamp,
68
+ session_file: file,
69
+ text,
70
+ });
71
+ addTotals(tokenTotals, usageFromObject(row));
72
+ }
73
+ }
74
+ if (filesScanned > 0) break;
75
+ }
76
+
77
+ if (!filesScanned && !root) {
78
+ notes.push(
79
+ `No ${source} task transcripts found under VS Code/Cursor globalStorage (${extensionId}/tasks).`,
80
+ );
81
+ }
82
+
83
+ return {
84
+ ...(emptyReport(source, usedRoot, notes)),
85
+ files_scanned: filesScanned,
86
+ prompt_count: prompts.length,
87
+ token_totals: tokenTotals,
88
+ prompts,
89
+ };
90
+ }
91
+
92
+ function normalizeMessageRows(data) {
93
+ if (Array.isArray(data)) return data;
94
+ if (!data || typeof data !== "object") return [];
95
+ if (Array.isArray(data.messages)) return data.messages;
96
+ if (Array.isArray(data.clineMessages)) return data.clineMessages;
97
+ if (Array.isArray(data.history)) return data.history;
98
+ return [];
99
+ }
100
+
101
+ function extractTaskUserText(row) {
102
+ if (!row || typeof row !== "object") return "";
103
+ const type = String(row.type || row.role || row.say || row.ask || "").toLowerCase();
104
+ const say = String(row.say || "").toLowerCase();
105
+ const ask = String(row.ask || "").toLowerCase();
106
+ const role = String(row.role || "").toLowerCase();
107
+
108
+ const looksUser =
109
+ role === "user" ||
110
+ type === "user" ||
111
+ type === "human" ||
112
+ say === "user_feedback" ||
113
+ say === "text" && role !== "assistant" ||
114
+ ask === "followup" ||
115
+ ask === "command" ||
116
+ (type === "ask" && !/tool|error|completion/i.test(ask));
117
+
118
+ if (!looksUser) {
119
+ // api_conversation_history style
120
+ if (role !== "user" && type !== "user") return "";
121
+ }
122
+
123
+ const candidates = [
124
+ row.text,
125
+ row.content,
126
+ row.message,
127
+ row.userContent,
128
+ row.ask === "followup" ? row.text : null,
129
+ Array.isArray(row.content) ? row.content : null,
130
+ ];
131
+ for (const candidate of candidates) {
132
+ const text = cleanText(candidate);
133
+ if (text) return text;
134
+ }
135
+ return "";
136
+ }
137
+
138
+ module.exports = {
139
+ inspectVscodeTaskStore,
140
+ extractTaskUserText,
141
+ normalizeMessageRows,
142
+ };
@@ -0,0 +1,109 @@
1
+ const os = require("node:os");
2
+ const path = require("node:path");
3
+
4
+ const { walkFiles, readJsonl } = require("../lib/jsonl");
5
+ const { isInRange } = require("../lib/dates");
6
+ const {
7
+ emptyTotals,
8
+ emptyReport,
9
+ addTotals,
10
+ usageFromObject,
11
+ cleanText,
12
+ toIsoTimestamp,
13
+ readJsonFile,
14
+ } = require("./common");
15
+
16
+ async function inspectWindsurf({ root, range, home = os.homedir() } = {}) {
17
+ const windsurfRoot = root || path.join(home, ".codeium", "windsurf");
18
+ const files = await walkFiles(
19
+ windsurfRoot,
20
+ (_filePath, name) =>
21
+ name.endsWith(".json") ||
22
+ name.endsWith(".jsonl") ||
23
+ name.endsWith(".md") ||
24
+ name.endsWith(".pb"),
25
+ );
26
+
27
+ const prompts = [];
28
+ const tokenTotals = emptyTotals();
29
+ let readableFiles = 0;
30
+ let encryptedPb = 0;
31
+
32
+ for (const file of files) {
33
+ if (file.endsWith(".pb")) {
34
+ encryptedPb += 1;
35
+ continue;
36
+ }
37
+ readableFiles += 1;
38
+ if (file.endsWith(".jsonl")) {
39
+ await readJsonl(file, (obj) => {
40
+ const text = extractWindsurfUserText(obj);
41
+ if (!text) return;
42
+ const timestamp = toIsoTimestamp(obj.timestamp || obj.time || obj.createdAt);
43
+ if (!isInRange(timestamp, range)) return;
44
+ prompts.push({
45
+ source: "windsurf",
46
+ timestamp,
47
+ session_file: file,
48
+ text,
49
+ });
50
+ addTotals(tokenTotals, usageFromObject(obj));
51
+ });
52
+ continue;
53
+ }
54
+
55
+ if (file.endsWith(".md")) {
56
+ // Exported Cascade markdown — treat lines after "## User" / "### User" as prompts.
57
+ // Keep lightweight: only explicit user headings.
58
+ continue;
59
+ }
60
+
61
+ const data = await readJsonFile(file);
62
+ const rows = Array.isArray(data)
63
+ ? data
64
+ : Array.isArray(data?.messages)
65
+ ? data.messages
66
+ : Array.isArray(data?.trajectory)
67
+ ? data.trajectory
68
+ : Array.isArray(data?.steps)
69
+ ? data.steps
70
+ : [];
71
+ for (const row of rows) {
72
+ const text = extractWindsurfUserText(row);
73
+ if (!text) continue;
74
+ const timestamp = toIsoTimestamp(row.timestamp || row.time || row.createdAt || data?.createdAt);
75
+ if (!isInRange(timestamp, range)) continue;
76
+ prompts.push({
77
+ source: "windsurf",
78
+ timestamp,
79
+ session_file: file,
80
+ text,
81
+ });
82
+ addTotals(tokenTotals, usageFromObject(row));
83
+ }
84
+ }
85
+
86
+ const notes = [];
87
+ if (encryptedPb > 0 && prompts.length === 0) {
88
+ notes.push(
89
+ `Found ${encryptedPb} encrypted Cascade .pb trajectory file(s); plaintext JSON/JSONL exports are required for prompt extraction.`,
90
+ );
91
+ }
92
+
93
+ return {
94
+ ...(emptyReport("windsurf", windsurfRoot, notes)),
95
+ files_scanned: readableFiles + encryptedPb,
96
+ prompt_count: prompts.length,
97
+ token_totals: tokenTotals,
98
+ prompts,
99
+ };
100
+ }
101
+
102
+ function extractWindsurfUserText(row) {
103
+ if (!row || typeof row !== "object") return "";
104
+ const role = String(row.role || row.type || row.author || row.kind || "").toLowerCase();
105
+ if (role && !/(user|human|prompt|input)/.test(role)) return "";
106
+ return cleanText(row.text ?? row.content ?? row.message ?? row.prompt ?? "");
107
+ }
108
+
109
+ module.exports = { inspectWindsurf, extractWindsurfUserText };
@@ -0,0 +1,96 @@
1
+ # Vibe Roast Auth · Cloudflare Worker
2
+
3
+ This Worker is the production OAuth broker and hosted free-roast gateway for
4
+ the public npm client. It keeps GitHub secrets off user machines, stores OAuth
5
+ flows and one-time tickets in a SQLite-backed Durable Object, and invokes
6
+ Cloudflare Workers AI through a server-only binding.
7
+
8
+ ## Configure
9
+
10
+ Authenticate Wrangler:
11
+
12
+ ```bash
13
+ npm run worker:login
14
+ ```
15
+
16
+ Set the two server-only secrets:
17
+
18
+ ```bash
19
+ npm run worker:secret:github
20
+ npm run worker:secret:session
21
+ ```
22
+
23
+ - `GITHUB_CLIENT_SECRET`: the GitHub App client secret.
24
+ - `AUTH_BROKER_SESSION_SECRET`: at least 32 random bytes; for example,
25
+ `openssl rand -base64 48`.
26
+
27
+ The public GitHub App client ID lives in `wrangler.jsonc`. Override it there if
28
+ the app registration changes.
29
+
30
+ ## Deploy
31
+
32
+ ```bash
33
+ npm run worker:deploy
34
+ ```
35
+
36
+ Wrangler binds the Worker to the configured Cloudflare Custom Domain:
37
+
38
+ ```text
39
+ https://auth.pinktalk.online
40
+ ```
41
+
42
+ Configure the GitHub App callback with this exact URL:
43
+
44
+ ```text
45
+ https://auth.pinktalk.online/oauth/github/callback
46
+ ```
47
+
48
+ The public npm app uses `https://auth.pinktalk.online` automatically. Users do
49
+ not need to configure `VIBE_WRAPPER_AUTH_BROKER_URL`; that variable is only an
50
+ override for local development or a self-hosted broker.
51
+
52
+ `AUTH_BROKER_PUBLIC_URL` pins OAuth redirects to the Custom Domain even when
53
+ the `workers.dev` preview URL is also enabled. It is not confidential.
54
+
55
+ ## Hosted free AI
56
+
57
+ `POST /v1/chat/completions` is an OpenAI-compatible internal endpoint used by
58
+ the localhost npm server. It requires the signed broker session returned after
59
+ GitHub OAuth; it is not an anonymous public inference endpoint.
60
+
61
+ - Model: `@cf/qwen/qwen3-30b-a3b-fp8`
62
+ - Default quota: 9 model calls per GitHub account per UTC day
63
+ - Configuration: `FREE_AI_DAILY_CALLS` in `wrangler.jsonc`
64
+ - Limits: at most 8 messages, 50,000 characters of prompt text, and 1,800
65
+ output tokens per call
66
+
67
+ The local roast writer spends one model call per roast. It validates the
68
+ bilingual JSON/schema but does not run a post-generation grounding/style audit,
69
+ editor pass, or repair call. Raw prompts and local paths are never sent; only
70
+ the aggregate `roast_evidence` packet is used.
71
+
72
+ The localhost client also sends a compact behavioral snapshot header. The
73
+ Durable Object binds the generated roast to the authenticated GitHub identity
74
+ and reuses it without consuming quota while the profile remains materially
75
+ equivalent. Token growth and minor score movement do not invalidate the cache;
76
+ type changes, large axis/radar/category shifts, a substantially different
77
+ concept set, or a changed top Agent do. The cache contains aggregate snapshot
78
+ fields and generated copy, never raw prompts.
79
+
80
+ ## Local development
81
+
82
+ Create an ignored `worker/.dev.vars`:
83
+
84
+ ```dotenv
85
+ GITHUB_CLIENT_SECRET=...
86
+ AUTH_BROKER_SESSION_SECRET=...
87
+ AUTH_BROKER_PUBLIC_URL=http://localhost:8787
88
+ ```
89
+
90
+ Then run:
91
+
92
+ ```bash
93
+ npm run worker:dev
94
+ ```
95
+
96
+ The GitHub App must include the matching local callback URL while testing.