@ak--47/dungeon-master 1.5.3 → 1.5.4

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.
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: create-project
3
+ description: Use when an existing dungeon needs a real Mixpanel project provisioned before sending data — creates the project, sets timezone UTC, mints a scoped service account, adds the dungeon's group keys, uploads business context (AI context), and writes the resulting credentials back into the dungeon so it "just runs". Follows create-dungeon / write-hooks / verify-dungeon.
4
+ argument-hint: [dungeon path, e.g. dungeons/user/shopstream/shopstream.js]
5
+ model: claude-opus-4-6
6
+ effort: max
7
+ ---
8
+
9
+ # Create a Mixpanel Project for a Dungeon
10
+
11
+ Provision a fresh Mixpanel project for an existing dungeon and wire its credentials back in.
12
+
13
+ **Dungeon file:** `$ARGUMENTS`
14
+
15
+ This is the step after a dungeon is authored, reviewed, and tweaked. It turns a
16
+ local dungeon into one you can actually send to Mixpanel by creating the project
17
+ and stamping `credentials` back into the file.
18
+
19
+ ## What it does
20
+
21
+ All work runs through the orchestrator `provision.mjs` (this skill's directory),
22
+ which calls the [power-tools API](https://mixpanel-power-tools-api-lmozz6xkha-uc.a.run.app)
23
+ in order:
24
+
25
+ 1. **createProject** — name derived from the dungeon's `OVERVIEW` (`NAME:` line), region `US`, timezone `UTC` (set as a follow-up by the endpoint).
26
+ 2. **mintServiceAccount** — `admin`, expires `+30 days`, scoped to the new project. This is what the dungeon uses to **send** data.
27
+ 3. **addGroupKey** — one per `groupKeys` entry in the dungeon (`property_name` + a titleized `display_name`). Skipped if the dungeon has no group keys.
28
+ 4. **setBusinessContext** — markdown built from the dungeon's `OVERVIEW` + `HOOK STORIES` comment blocks (via the package's `extractComments`) plus an events/funnels/props/group-keys summary, capped at 50k chars.
29
+ 5. **write-back** — replaces the dungeon's `credentials: { … }` block with `{ token, projectId, serviceAccount, serviceSecret, region }`.
30
+
31
+ **Always creates a fresh project.** Re-running mints a new project and overwrites
32
+ `credentials`. Auth for all four calls is the OAuth `BEARER_TOKEN`; the minted
33
+ service account is only written into the dungeon for later data sends.
34
+
35
+ ## Prerequisites
36
+
37
+ `.env` at the repo root must contain (both are gitignored):
38
+
39
+ ```
40
+ BEARER_TOKEN=<oauth token> # creates projects / mints SAs / adds group keys
41
+ ORG_ID=<organization id>
42
+ ```
43
+
44
+ If either is missing the orchestrator exits with a clear message — tell the user
45
+ to add them to `.env`.
46
+
47
+ User dungeons (`dungeons/user/`, `dungeons/customers/`, `dungeons/capstone/`) are
48
+ gitignored, so writing plaintext credentials into them is expected and safe.
49
+
50
+ ## Steps
51
+
52
+ ### 1. Show the plan (dry run)
53
+
54
+ ```bash
55
+ node .claude/skills/create-project/provision.mjs <dungeon-path> --dry-run
56
+ ```
57
+
58
+ This makes **no** API calls. It prints the derived project name, group-key
59
+ mapping, service-account name/expiry, and a business-context preview. Show this
60
+ to the user.
61
+
62
+ ### 2. Confirm
63
+
64
+ Creating a real project + service account is outward-facing and not easily
65
+ undone. Confirm with the user before the live run (one line is enough).
66
+
67
+ ### 3. Provision (live)
68
+
69
+ ```bash
70
+ node .claude/skills/create-project/provision.mjs <dungeon-path>
71
+ ```
72
+
73
+ On success it writes `credentials` back into the dungeon and prints a non-secret
74
+ summary (project URL + id, SA username/expiry, group keys added/skipped, context
75
+ size). Token and secret are written into the dungeon, not printed.
76
+
77
+ ### 4. Report
78
+
79
+ Relay the project URL and confirm credentials were written. Point the user at the
80
+ run command:
81
+
82
+ ```bash
83
+ node scripts/run-dungeon.mjs <dungeon-path>
84
+ ```
85
+
86
+ ## Error handling
87
+
88
+ - **Missing `BEARER_TOKEN` / `ORG_ID`** — orchestrator exits; have the user fix `.env`.
89
+ - **`createProject` fails** — nothing is provisioned; surface the power-tools error (`{ error }` or `{ errors:[{param,message}] }`) verbatim and stop.
90
+ - **A later step fails** (mint / group keys / context / write-back) — the orchestrator continues, writes back whatever succeeded, and lists the failed step under `⚠ warnings`. Relay those warnings; the user may re-run or fix manually.
91
+
92
+ ## Notes
93
+
94
+ - Region is always `US`; timezone is always `UTC` (matches the dungeon's UTC time window so Mixpanel day-bucketing aligns).
95
+ - Group keys come straight from the dungeon's `groupKeys` (`[["parent_account", N, []], …]`) — author them there (via `create-dungeon`) before running this skill.
96
+ - The orchestrator reuses the package's own exports (`loadFromFile`, `extractComments`) — no separate parser to keep in sync.
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * create-project orchestrator.
5
+ *
6
+ * Given an existing dungeon file, provisions a fresh Mixpanel project via the
7
+ * power-tools API and writes the resulting credentials back into the dungeon so
8
+ * it can "just run":
9
+ *
10
+ * 1. createProject (sets timezone UTC as a follow-up)
11
+ * 2. mintServiceAccount (admin, +30d) — scoped to the new project
12
+ * 3. addGroupKey (only if the dungeon declares groupKeys)
13
+ * 4. setBusinessContext (OVERVIEW + HOOK STORIES + schema summary)
14
+ * 5. write `credentials: { token, projectId, serviceAccount, serviceSecret, region }`
15
+ * back into the dungeon (gitignored user dungeons — plaintext is fine)
16
+ *
17
+ * Auth: all calls use the OAuth BEARER_TOKEN from .env. The minted service
18
+ * account is for the dungeon to SEND data later, not for setup.
19
+ *
20
+ * Usage:
21
+ * node .claude/skills/create-project/provision.mjs <dungeon-path> [--dry-run]
22
+ *
23
+ * Env (.env at repo root):
24
+ * BEARER_TOKEN=<oauth token>
25
+ * ORG_ID=<organization id>
26
+ */
27
+
28
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
29
+ import path, { dirname, resolve } from 'path';
30
+ import { fileURLToPath } from 'url';
31
+ import dotenv from 'dotenv';
32
+ import { loadFromFile, extractComments } from '../../../index.js';
33
+
34
+ const __dirname = dirname(fileURLToPath(import.meta.url));
35
+ const REPO_ROOT = resolve(__dirname, '../../../');
36
+
37
+ dotenv.config({ path: resolve(REPO_ROOT, '.env') });
38
+
39
+ const BASE = 'https://mixpanel-power-tools-api-lmozz6xkha-uc.a.run.app';
40
+ const CLIENT_ID = 'dungeon-master';
41
+ const REGION = 'US';
42
+ const SA_TTL_DAYS = 30;
43
+
44
+ // ── args ──────────────────────────────────────────────────────────────────
45
+ const args = process.argv.slice(2);
46
+ const dryRun = args.includes('--dry-run');
47
+ const dungeonArg = args.find((a) => !a.startsWith('--'));
48
+
49
+ if (!dungeonArg) {
50
+ fail('Usage: node .claude/skills/create-project/provision.mjs <dungeon-path> [--dry-run]');
51
+ }
52
+
53
+ const dungeonPath = resolve(process.cwd(), dungeonArg);
54
+ if (!existsSync(dungeonPath)) fail(`dungeon file not found: ${dungeonPath}`);
55
+
56
+ // ── main ──────────────────────────────────────────────────────────────────
57
+ const config = await loadFromFile(dungeonPath);
58
+ const comments = extractComments(dungeonPath);
59
+
60
+ const name = deriveName(comments, dungeonPath);
61
+ const groupKeys = Array.isArray(config.groupKeys)
62
+ ? config.groupKeys.filter(Boolean).map(([prop]) => ({ property_name: prop, display_name: titleize(prop) }))
63
+ : [];
64
+ const saName = `${slug(name)}-dungeon-sa`.slice(0, 64);
65
+ const content = buildContext(name, config, comments, groupKeys);
66
+
67
+ if (dryRun) {
68
+ printPlan();
69
+ process.exit(0);
70
+ }
71
+
72
+ // Live run needs auth.
73
+ const { BEARER_TOKEN, ORG_ID } = process.env;
74
+ if (!BEARER_TOKEN) fail('BEARER_TOKEN missing from .env (OAuth token required to create projects).');
75
+ if (!ORG_ID) fail('ORG_ID missing from .env (organization id required to create projects).');
76
+
77
+ const warnings = [];
78
+
79
+ // 1. create project (timezone set to UTC as a follow-up by the endpoint)
80
+ let project;
81
+ try {
82
+ project = await post('/crud/createProject', { org_id: ORG_ID, name, timezone: 'UTC' });
83
+ } catch (err) {
84
+ fail(`createProject failed — nothing provisioned.\n ${err.message}`);
85
+ }
86
+ const projectId = String(project.id);
87
+ const projectToken = project.token;
88
+
89
+ // 2. mint service account (admin, +30d)
90
+ let sa = null;
91
+ const expires = isoInDays(SA_TTL_DAYS);
92
+ try {
93
+ sa = await post('/crud/mintServiceAccount', {
94
+ org_id: ORG_ID,
95
+ project_id: projectId,
96
+ name: saName,
97
+ role: 'admin',
98
+ expires,
99
+ });
100
+ } catch (err) {
101
+ warnings.push(`mintServiceAccount failed: ${err.message}`);
102
+ }
103
+
104
+ // 3. group keys (only if declared)
105
+ let groupKeyResult = null;
106
+ if (groupKeys.length) {
107
+ try {
108
+ groupKeyResult = await post('/crud/addGroupKey', { project_id: projectId, group_keys: groupKeys });
109
+ } catch (err) {
110
+ warnings.push(`addGroupKey failed: ${err.message}`);
111
+ }
112
+ }
113
+
114
+ // 4. business context
115
+ try {
116
+ await post('/crud/setBusinessContext', { project_id: projectId, content });
117
+ } catch (err) {
118
+ warnings.push(`setBusinessContext failed: ${err.message}`);
119
+ }
120
+
121
+ // 5. write credentials back into the dungeon
122
+ const creds = {
123
+ token: projectToken,
124
+ projectId,
125
+ serviceAccount: sa?.username || '',
126
+ serviceSecret: sa?.secret || '',
127
+ region: REGION,
128
+ };
129
+ let wroteBack = true;
130
+ try {
131
+ writeBackCredentials(dungeonPath, creds);
132
+ } catch (err) {
133
+ wroteBack = false;
134
+ warnings.push(`credentials write-back failed: ${err.message}`);
135
+ }
136
+
137
+ printSummary();
138
+
139
+ // ── helpers ─────────────────────────────────────────────────────────────────
140
+
141
+ async function post(pathname, body) {
142
+ const res = await fetch(BASE + pathname, {
143
+ method: 'POST',
144
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.BEARER_TOKEN}` },
145
+ body: JSON.stringify({ client_id: CLIENT_ID, region: REGION, ...body }),
146
+ });
147
+ const text = await res.text();
148
+ let json;
149
+ try {
150
+ json = JSON.parse(text);
151
+ } catch {
152
+ json = { raw: text };
153
+ }
154
+ if (!res.ok) {
155
+ const detail = Array.isArray(json.errors)
156
+ ? json.errors.map((e) => `${e.param}: ${e.message}`).join('; ')
157
+ : json.error || text || `HTTP ${res.status}`;
158
+ throw new Error(`${res.status} ${detail}`);
159
+ }
160
+ return json;
161
+ }
162
+
163
+ function deriveName(comments, p) {
164
+ const m = (comments.overview || '').match(/^NAME:\s*(.+)$/m);
165
+ if (m) return m[1].trim();
166
+ return path.basename(p).replace(/\.(js|mjs|json)$/i, '');
167
+ }
168
+
169
+ function titleize(s) {
170
+ return String(s)
171
+ .split(/[_\s-]+/)
172
+ .map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w))
173
+ .join(' ');
174
+ }
175
+
176
+ function slug(s) {
177
+ return String(s)
178
+ .toLowerCase()
179
+ .replace(/[^a-z0-9]+/g, '-')
180
+ .replace(/^-+|-+$/g, '');
181
+ }
182
+
183
+ function isoInDays(days) {
184
+ const ms = Date.now() + days * 24 * 60 * 60 * 1000;
185
+ return new Date(ms).toISOString();
186
+ }
187
+
188
+ function buildContext(name, config, comments, groupKeys) {
189
+ const parts = [`# ${name}`, ''];
190
+ if (comments.overview) parts.push(comments.overview, '');
191
+ if (comments.hookStories) parts.push('## Engineered Behaviors', '', comments.hookStories, '');
192
+
193
+ parts.push('## Schema', '');
194
+ const events = config.events || [];
195
+ parts.push(`### Events (${events.length})`);
196
+ for (const e of events) {
197
+ const props = e.properties ? Object.keys(e.properties).join(', ') : '';
198
+ const weight = e.weight != null ? ` (weight ${e.weight})` : '';
199
+ parts.push(`- ${e.event}${weight}${props ? ` — ${props}` : ''}`);
200
+ }
201
+ parts.push('');
202
+
203
+ const funnels = config.funnels || [];
204
+ if (funnels.length) {
205
+ parts.push(`### Funnels (${funnels.length})`);
206
+ for (const f of funnels) {
207
+ const seq = (f.sequence || []).join(' → ');
208
+ const rate = f.conversionRate != null ? ` (${f.conversionRate}%)` : '';
209
+ parts.push(`- ${f.name || '(unnamed)'}: ${seq}${rate}`);
210
+ }
211
+ parts.push('');
212
+ }
213
+
214
+ if (groupKeys.length) {
215
+ parts.push('### Group keys', ...groupKeys.map((g) => `- ${g.property_name} (${g.display_name})`), '');
216
+ }
217
+
218
+ let md = parts.join('\n');
219
+ if (md.length > 50000) md = md.slice(0, 49900) + '\n\n…(truncated to 50,000 chars)';
220
+ return md;
221
+ }
222
+
223
+ function writeBackCredentials(p, creds) {
224
+ let src = readFileSync(p, 'utf-8');
225
+ const block =
226
+ `credentials: { token: ${q(creds.token)}, projectId: ${q(creds.projectId)}, ` +
227
+ `serviceAccount: ${q(creds.serviceAccount)}, serviceSecret: ${q(creds.serviceSecret)}, region: ${q(creds.region)} }`;
228
+
229
+ const existing = /credentials\s*:\s*\{[\s\S]*?\}/;
230
+ if (existing.test(src)) {
231
+ src = src.replace(existing, block);
232
+ } else {
233
+ const opener = /(const\s+\w+\s*=\s*\{)/;
234
+ if (opener.test(src)) {
235
+ src = src.replace(opener, `$1\n\t${block},`);
236
+ } else {
237
+ throw new Error('no `credentials` block or `const X = {` opener found — add a credentials block manually.');
238
+ }
239
+ }
240
+ writeFileSync(p, src, 'utf-8');
241
+ }
242
+
243
+ function q(v) {
244
+ return JSON.stringify(String(v ?? ''));
245
+ }
246
+
247
+ function printPlan() {
248
+ const preview = content.length > 600 ? content.slice(0, 600) + ' …' : content;
249
+ console.log('── create-project plan (dry run) ────────────────────────────');
250
+ console.log(`dungeon: ${path.relative(process.cwd(), dungeonPath)}`);
251
+ console.log(`project name: ${name}`);
252
+ console.log(`region: ${REGION} timezone: UTC`);
253
+ console.log(`service acct: ${saName} (role admin, expires +${SA_TTL_DAYS}d)`);
254
+ console.log(`group keys: ${groupKeys.length ? groupKeys.map((g) => `${g.property_name} → "${g.display_name}"`).join(', ') : '(none)'}`);
255
+ console.log(`business ctx: ${content.length} chars`);
256
+ console.log('');
257
+ console.log('would POST: createProject → mintServiceAccount' + (groupKeys.length ? ' → addGroupKey' : '') + ' → setBusinessContext');
258
+ console.log('then write credentials back into the dungeon.');
259
+ console.log('');
260
+ console.log('── business context preview ─────────────────────────────────');
261
+ console.log(preview);
262
+ }
263
+
264
+ function printSummary() {
265
+ console.log('── create-project: provisioned ──────────────────────────────');
266
+ console.log(`project: ${name} (id ${projectId})`);
267
+ if (project.url) console.log(`url: ${project.url}`);
268
+ console.log(`region: ${REGION} timezone: UTC`);
269
+ console.log(`service acct: ${sa ? `${sa.username} (role ${sa.role}, expires ${sa.expires})` : '(FAILED — see warnings)'}`);
270
+ if (groupKeys.length) {
271
+ const added = groupKeyResult?.added?.map((g) => g.property_name).join(', ') || '(none)';
272
+ const skipped = groupKeyResult?.skipped?.join(', ') || '(none)';
273
+ console.log(`group keys: added [${added}] skipped [${skipped}]`);
274
+ }
275
+ console.log(`business ctx: ${content.length} chars uploaded`);
276
+ console.log(`credentials: ${wroteBack ? 'written back into dungeon ✓' : 'NOT written (see warnings)'}`);
277
+ if (warnings.length) {
278
+ console.log('');
279
+ console.log('⚠ warnings:');
280
+ for (const w of warnings) console.log(` - ${w}`);
281
+ }
282
+ console.log('');
283
+ console.log(`next: node scripts/run-dungeon.mjs ${path.relative(process.cwd(), dungeonPath)}`);
284
+ }
285
+
286
+ function fail(msg) {
287
+ console.error(`✖ ${msg}`);
288
+ process.exit(1);
289
+ }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  All notable changes to `@ak--47/dungeon-master`.
4
4
 
5
+ ## 1.5.4 — 2026-06-04
6
+
7
+ Patch. Import-phase progress now reaches `onProgress` consumers.
8
+
9
+ ### Changed
10
+
11
+ - **Bumped `mixpanel-import` to `^3.3.2`.** It now fires `progressCallback`
12
+ independently of `verbose` / `showProgress`. Previously the import callbacks
13
+ wired up in `mixpanel-sender.js` never fired in non-verbose runs because the
14
+ importer only invoked them when its stdout progress bar was enabled.
15
+
16
+ ### Fixed
17
+
18
+ - **Import progress reaches `onProgress`** (requires `mixpanel-import >= 3.3.2`).
19
+ Every import call (events, user profiles, ad spend, group profiles, group
20
+ events, SCD) already passed a `progressCallback`; with the dependency bump
21
+ those now emit `{ phase: "import", recordType, processed, total, eps,
22
+ bytesProcessed }` to the consumer's `onProgress` during the import phase.
23
+ `showProgress: !!verbose` is unchanged — it still gates only the importer's
24
+ stdout bar, so non-verbose runs stay quiet while the callback fires.
25
+
26
+ ### Why
27
+
28
+ Consumers (e.g. DM4) already handle `update.phase === 'import'` to render an
29
+ import progress bar; the callbacks simply weren't firing. This is a dependency
30
+ bump plus a regression test — no DM API change. Consumers pick it up via their
31
+ normal upgrade flow with no code change.
32
+
5
33
  ## 1.5.3 — 2026-06-04
6
34
 
7
35
  Adds two JSON/source interop helpers to the public API. No breaking changes —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ak--47/dungeon-master",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
4
4
  "description": "generate fancy datasets",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -86,7 +86,7 @@
86
86
  "dotenv": "^16.4.5",
87
87
  "hyparquet-writer": "^0.6.1",
88
88
  "mixpanel": "^0.18.0",
89
- "mixpanel-import": "^3.3.1",
89
+ "mixpanel-import": "^3.3.2",
90
90
  "p-limit": "^3.1.0",
91
91
  "pino": "^9.0.0",
92
92
  "pino-pretty": "^11.0.0",