portals-mcp 1.2.2 → 1.2.3-beta.1

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 (67) hide show
  1. package/dist/api.js +19 -0
  2. package/dist/context-assembler.js +199 -0
  3. package/dist/docs.js +14 -42
  4. package/dist/feedback-prompts.js +5 -91
  5. package/dist/index.js +1 -1
  6. package/dist/intelligence-bridge.js +30 -11
  7. package/dist/lookup.js +0 -4
  8. package/dist/resources/ai/bootstrap.json +13 -21
  9. package/dist/resources/guide/rules-and-conventions.md +1 -1
  10. package/dist/resources/guide/workflow-steps.md +15 -0
  11. package/dist/resources/index/intents.json +38 -28
  12. package/dist/resources/index/knowledge-map.json +28 -34
  13. package/dist/resources/logic/expression-guide.md +2 -18
  14. package/dist/resources/logic/js-function-reference.md +1 -1
  15. package/dist/resources/python/lib/__pycache__/portals_effects.cpython-314.pyc +0 -0
  16. package/dist/resources/python/lib/__pycache__/portals_ops.cpython-314.pyc +0 -0
  17. package/dist/resources/python/lib/__pycache__/portals_utils.cpython-314.pyc +0 -0
  18. package/dist/resources/python/tools/validate_room.py +56 -0
  19. package/dist/resources/recipes/board-game-pawn/implementation.md +12 -0
  20. package/dist/resources/recipes/cutscene-camera/implementation.md +20 -0
  21. package/dist/resources/recipes/dice-roll/implementation.md +12 -0
  22. package/dist/resources/recipes/keypad/implementation.md +12 -0
  23. package/dist/resources/recipes/leaderboard-scoring/implementation.md +12 -0
  24. package/dist/resources/recipes/logic-board/implementation.md +13 -1
  25. package/dist/resources/recipes/manifest.json +42 -258
  26. package/dist/resources/recipes/position-heartbeat/implementation.md +12 -0
  27. package/dist/resources/recipes/side-scroller/implementation.md +12 -0
  28. package/dist/resources/ref/pitfalls.json +1 -1
  29. package/dist/resources/ref/systems/function-effector-js.json +54 -0
  30. package/dist/resources/ref/systems/function-effector.json +1 -11
  31. package/dist/resources/ref/systems/multiplayer.json +57 -0
  32. package/dist/resources/ref/systems/string-variables.json +40 -0
  33. package/dist/resources/ref/systems/use-effector.json +73 -0
  34. package/dist/resources/reference/gotchas.md +121 -0
  35. package/dist/resources/reference/interactions.md +1 -1
  36. package/dist/resources/usage-rules.md +20 -49
  37. package/dist/resources/workflows/function-effects-reference.md +2 -18
  38. package/dist/resources/workflows/modular-build-workflow.md +240 -0
  39. package/dist/session-tracker.js +200 -0
  40. package/dist/tools.js +643 -765
  41. package/dist/ws-bridge.js +29 -6
  42. package/package.json +1 -1
  43. package/dist/resources/ai/workflow-map.json +0 -6
  44. package/dist/resources/guide/glb-workflow.md +0 -35
  45. package/dist/resources/recipes/board-game-pawn/overview.json +0 -13
  46. package/dist/resources/recipes/cutscene-camera/overview.json +0 -13
  47. package/dist/resources/recipes/dice-roll/overview.json +0 -13
  48. package/dist/resources/recipes/keypad/overview.json +0 -13
  49. package/dist/resources/recipes/leaderboard-scoring/overview.json +0 -13
  50. package/dist/resources/recipes/logic-board/overview.json +0 -13
  51. package/dist/resources/recipes/position-heartbeat/overview.json +0 -13
  52. package/dist/resources/recipes/side-scroller/overview.json +0 -13
  53. package/dist/resources/scene/composition.md +0 -153
  54. package/dist/resources/scene/density-targets.md +0 -130
  55. package/dist/resources/scene/glb-catalog-system.md +0 -364
  56. package/dist/resources/scene/glb-workflow.md +0 -358
  57. package/dist/resources/scene/modular-kits.md +0 -238
  58. package/dist/resources/scene/placement-strategies.md +0 -343
  59. package/dist/resources/templates/game-design-doc.md +0 -158
  60. package/dist/resources/templates/simple_demo.json +0 -143
  61. package/dist/resources/workflows/asset-pipeline.md +0 -31
  62. package/dist/resources/workflows/builder.md +0 -18
  63. package/dist/resources/workflows/component-template.md +0 -362
  64. package/dist/resources/workflows/game-designer-workflow.md +0 -191
  65. package/dist/resources/workflows/game-designer.md +0 -14
  66. package/dist/resources/workflows/game-logic-board.md +0 -235
  67. package/dist/resources/workflows/validate-room.md +0 -37
package/dist/api.js CHANGED
@@ -192,6 +192,25 @@ export async function sendUserMessage(content, roomId) {
192
192
  });
193
193
  await parseJsonResponse(res, "Failed to send user message");
194
194
  }
195
+ export async function uploadAudio(roomId, fileData, fileType) {
196
+ const res = await fetch(`${API_BASE}/api/v2/utils/generate-audio-upload-url`, {
197
+ method: "POST",
198
+ body: JSON.stringify({ fileType, roomId }),
199
+ headers: {
200
+ "Content-Type": "application/json",
201
+ "x-api-key": getAccessKey(),
202
+ },
203
+ });
204
+ const json = await parseJsonResponse(res, "Failed to generate audio upload URL");
205
+ const putRes = await fetch(json.signedUploadURL, {
206
+ method: "PUT",
207
+ headers: { "Content-Type": fileType },
208
+ body: fileData,
209
+ });
210
+ if (!putRes.ok)
211
+ throw new Error(`Failed to upload audio: ${putRes.statusText}`);
212
+ return { assetURL: json.assetURL, objectKey: json.objectKey };
213
+ }
195
214
  export async function downloadRoomData(roomId) {
196
215
  const res = await fetch(`${API_BASE}/api/v2/mcp/download-room-data`, {
197
216
  method: "GET",
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Context assembler — powers the get_context tool.
3
+ *
4
+ * Uses the existing intent-matching engine from context-recommend.ts
5
+ * to identify relevant resources, then reads them from disk and returns
6
+ * the content inline (instead of returning URIs for the agent to read).
7
+ */
8
+ import { readFile } from "node:fs/promises";
9
+ import { existsSync } from "node:fs";
10
+ import { resolve } from "node:path";
11
+ import { recommendContextForTask } from "./context-recommend.js";
12
+ import { resolveDocsRoot, docsRootCandidates } from "./docs.js";
13
+ import { searchBundledRecipes } from "./recipe-search.js";
14
+ import { tokenizeQuery } from "./utils.js";
15
+ // Map intent IDs to pitfall categories
16
+ const INTENT_TO_PITFALL_CATEGORIES = {
17
+ "js-scripting": ["js", "javascript", "functioneffectorjs", "variables", "strings"],
18
+ "function-effects": ["ncalc", "functioneffector", "expressions", "variables", "quests", "tasks"],
19
+ "gameplay-build": ["quests", "tasks", "variables", "interactions"],
20
+ "scene-item": ["items", "fields", "naming"],
21
+ "scene-design": ["items", "placement", "coordinates"],
22
+ "create-room": ["room", "settings"],
23
+ "settings-update": ["settings"],
24
+ "asset-pipeline": ["assets", "glb", "upload"],
25
+ };
26
+ async function loadPitfalls() {
27
+ const docsRoot = resolveDocsRoot();
28
+ if (!docsRoot)
29
+ return [];
30
+ const pitfallsPath = resolve(docsRoot, "ref", "pitfalls.json");
31
+ if (!existsSync(pitfallsPath))
32
+ return [];
33
+ try {
34
+ const raw = JSON.parse(await readFile(pitfallsPath, "utf-8"));
35
+ // pitfalls.json has a "pitfalls" array
36
+ if (Array.isArray(raw.pitfalls))
37
+ return raw.pitfalls;
38
+ if (Array.isArray(raw))
39
+ return raw;
40
+ return [];
41
+ }
42
+ catch {
43
+ return [];
44
+ }
45
+ }
46
+ function filterPitfalls(pitfalls, intents) {
47
+ const relevantCategories = new Set();
48
+ for (const intent of intents) {
49
+ const categories = INTENT_TO_PITFALL_CATEGORIES[intent];
50
+ if (categories) {
51
+ for (const cat of categories)
52
+ relevantCategories.add(cat);
53
+ }
54
+ }
55
+ // If no category mapping, return top pitfalls as general guidance
56
+ if (relevantCategories.size === 0) {
57
+ return pitfalls.slice(0, 5).map(pitfallToGotcha);
58
+ }
59
+ const filtered = pitfalls.filter((p) => {
60
+ if (!p.categories || p.categories.length === 0)
61
+ return true; // uncategorized = always relevant
62
+ return p.categories.some((c) => relevantCategories.has(c.toLowerCase()));
63
+ });
64
+ return filtered.map(pitfallToGotcha);
65
+ }
66
+ function pitfallToGotcha(p) {
67
+ const gotcha = { rule: p.rule, detail: p.detail };
68
+ if (p.wrong)
69
+ gotcha.wrong = p.wrong;
70
+ if (p.right)
71
+ gotcha.right = p.right;
72
+ return gotcha;
73
+ }
74
+ // ── Resource reading ──
75
+ async function readResourceContent(uri) {
76
+ const docsRoot = resolveDocsRoot();
77
+ if (!docsRoot)
78
+ return null;
79
+ // docs://ref/systems/function-effector → ref/systems/function-effector
80
+ const stripped = uri.replace(/^docs:\/\//, "");
81
+ // Try common extensions
82
+ const extensions = [".json", ".md", ".txt", ".py"];
83
+ const roots = docsRootCandidates();
84
+ for (const root of roots) {
85
+ if (!existsSync(root))
86
+ continue;
87
+ for (const ext of extensions) {
88
+ const fullPath = resolve(root, stripped + ext);
89
+ if (existsSync(fullPath)) {
90
+ try {
91
+ return await readFile(fullPath, "utf-8");
92
+ }
93
+ catch {
94
+ continue;
95
+ }
96
+ }
97
+ }
98
+ // Try without extension (file might already have one in the URI)
99
+ const directPath = resolve(root, stripped);
100
+ if (existsSync(directPath)) {
101
+ try {
102
+ return await readFile(directPath, "utf-8");
103
+ }
104
+ catch {
105
+ continue;
106
+ }
107
+ }
108
+ }
109
+ return null;
110
+ }
111
+ // ── Main assembly ──
112
+ export async function assembleContext(query, options) {
113
+ // 1. Score intents using existing engine
114
+ const recommendOpts = { maxIntents: 3 };
115
+ if (options?.stage)
116
+ recommendOpts.stage = options.stage;
117
+ const recommendation = recommendContextForTask(query, recommendOpts);
118
+ const matchedIntents = recommendation.selected_intents.map((i) => i.intent);
119
+ // 2. Collect resource URIs to read (required + recommended, skip deep_dive)
120
+ const resourcesToRead = [
121
+ ...recommendation.resource_phases.required,
122
+ ...recommendation.resource_phases.recommended,
123
+ ];
124
+ // Deduplicate URIs
125
+ const uniqueUris = [...new Set(resourcesToRead.map((r) => r.uri))];
126
+ // 3. Read resource content in parallel
127
+ const contentParts = [];
128
+ const readPromises = uniqueUris.map(async (uri) => {
129
+ const content = await readResourceContent(uri);
130
+ if (content) {
131
+ return { uri, content };
132
+ }
133
+ return null;
134
+ });
135
+ const results = await Promise.all(readPromises);
136
+ for (const result of results) {
137
+ if (result) {
138
+ contentParts.push(result.content);
139
+ }
140
+ }
141
+ // 4. Filter pitfalls to relevant gotchas
142
+ const allPitfalls = await loadPitfalls();
143
+ const gotchas = filterPitfalls(allPitfalls, matchedIntents);
144
+ // 5. Search recipes
145
+ const tokens = tokenizeQuery(query);
146
+ let recipes = [];
147
+ try {
148
+ const recipeResults = await searchBundledRecipes(query, { top: 5 });
149
+ recipes = recipeResults.matches.map((r) => ({
150
+ name: r.name,
151
+ summary: r.summary,
152
+ patterns: r.patterns,
153
+ uri: r.resource_uri,
154
+ }));
155
+ }
156
+ catch {
157
+ // Non-critical
158
+ }
159
+ // 6. Build task summary
160
+ const taskSummary = buildTaskSummary(matchedIntents, query);
161
+ // 7. Collect deep-dive URIs (not read inline, available for agents that want more)
162
+ const deepDiveUris = recommendation.resource_phases.deep_dive.map((r) => r.uri);
163
+ // 8. Collect suggested tools
164
+ const suggestedTools = recommendation.tool_sequence.map((t) => t.tool);
165
+ return {
166
+ task_summary: taskSummary,
167
+ matched_intents: matchedIntents,
168
+ knowledge: contentParts.join("\n\n---\n\n"),
169
+ gotchas,
170
+ relevant_recipes: recipes,
171
+ deep_dive_uris: deepDiveUris,
172
+ suggested_tools: suggestedTools,
173
+ };
174
+ }
175
+ function buildTaskSummary(intents, query) {
176
+ const descriptions = {
177
+ "setup-auth": "Setting up authentication and connection",
178
+ "create-room": "Creating a new room",
179
+ "scene-item": "Working with scene items and placement",
180
+ "gameplay-build": "Building gameplay mechanics",
181
+ "function-effects": "Implementing logic with NCalc expressions",
182
+ "js-scripting": "Building with JavaScript/FunctionEffectorJS",
183
+ "logic-board": "Creating a logic board / flowchart",
184
+ "local-scripting": "Using Python tools for room generation",
185
+ "validation-publish": "Validating and publishing",
186
+ "asset-pipeline": "Uploading and managing assets",
187
+ "settings-update": "Updating room settings",
188
+ "game-design-full": "Designing and building a complete game",
189
+ "scene-design": "Designing scene composition and layout",
190
+ "room-targeting": "Working with an existing room",
191
+ };
192
+ const parts = intents
193
+ .map((i) => descriptions[i])
194
+ .filter(Boolean);
195
+ if (parts.length === 0) {
196
+ return `Task: ${query}`;
197
+ }
198
+ return parts.join(" + ");
199
+ }
package/dist/docs.js CHANGED
@@ -2,7 +2,6 @@ import { existsSync, readdirSync } from "node:fs";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { markResourceRead } from "./context-handshake.js";
6
5
  import { isWithinRoot } from "./utils.js";
7
6
  const SUPPORTED_RESOURCE_EXTENSIONS = [".md", ".json", ".py", ".txt"];
8
7
  const STATIC_DOC_RESOURCES = [
@@ -108,22 +107,6 @@ const STATIC_DOC_RESOURCES = [
108
107
  mimeType: "text/markdown",
109
108
  },
110
109
  // ── Workflows ────────────────────────────────────────────────────
111
- {
112
- name: "docs_workflow_builder",
113
- uri: "docs://workflows/builder",
114
- title: "Builder Workflow",
115
- description: "Build and iteration workflow for approved game designs.",
116
- relativePath: "workflows/builder.md",
117
- mimeType: "text/markdown",
118
- },
119
- {
120
- name: "docs_workflow_game_designer",
121
- uri: "docs://workflows/game-designer",
122
- title: "Game Designer Workflow",
123
- description: "Discovery, design doc creation, and approval flow.",
124
- relativePath: "workflows/game-designer.md",
125
- mimeType: "text/markdown",
126
- },
127
110
  {
128
111
  name: "docs_workflow_quality_review",
129
112
  uri: "docs://workflows/quality-review",
@@ -148,12 +131,10 @@ const HELP_TOPICS = [
148
131
  title: "System Overview",
149
132
  summary: "Start here for AI-native contracts and minimal context loading.",
150
133
  steps: [
151
- "Read docs://ai/bootstrap first.",
152
- "Read docs://ai/contracts/tool-output-envelope to parse all tool outputs safely.",
153
- "Use recommend_context for task-specific resource selection.",
154
- "Read context_handshake.required_resources before mutating tools.",
155
- "Use context_handshake.token as contextToken on mutating tool calls.",
134
+ "Call get_context with a description of what you want to build — it returns inline specs, gotchas, and recipes.",
135
+ "Use lookup(term) for quick spec lookups on specific items, triggers, or effects.",
156
136
  "Use docs://usage-rules when wiring read/write workflows.",
137
+ "Read docs://ai/contracts/tool-output-envelope to parse all tool outputs safely.",
157
138
  ],
158
139
  resources: [
159
140
  "docs://ai/bootstrap",
@@ -168,12 +149,11 @@ const HELP_TOPICS = [
168
149
  title: "Validate Room Workflow",
169
150
  summary: "Run validation before merge/upload and resolve structured errors.",
170
151
  steps: [
171
- "Use recommend_context with your task to retrieve minimal validation resources.",
172
- "Read required resources and include contextToken on mutation calls.",
173
- "Use docs://workflows/validate-room as the quality checklist.",
152
+ "Call get_context with your validation task for relevant context.",
153
+ "Use run_python_tool with validate_room to check the room snapshot.",
174
154
  "After set_room_data, re-read room data and verify expected counts.",
175
155
  ],
176
- resources: ["docs://workflows/validate-room", "docs://usage-rules"],
156
+ resources: ["docs://usage-rules", "docs://reference/gotchas"],
177
157
  aliases: ["validate", "quality", "validation"],
178
158
  },
179
159
  {
@@ -181,15 +161,12 @@ const HELP_TOPICS = [
181
161
  title: "Design + Build Flow",
182
162
  summary: "Use the standardized sequence to maximize quality and consistency.",
183
163
  steps: [
184
- "Call recommend_context with your requested mechanic list before loading docs.",
185
- "Follow docs://workflows/game-designer for discovery and design approval.",
186
- "Follow docs://workflows/builder for implementation and iteration.",
187
- "Use docs://recipes/manifest for known mechanics.",
164
+ "Call get_context with your game concept for targeted specs, gotchas, and recipe matches.",
165
+ "Use search_recipes to find known mechanics before building from scratch.",
188
166
  "Use lookup tool for specific item/trigger/effect specs.",
189
167
  ],
190
168
  resources: [
191
- "docs://workflows/game-designer",
192
- "docs://workflows/builder",
169
+ "docs://workflows/builder-workflow",
193
170
  "docs://ref/systems/function-effector",
194
171
  "docs://recipes/manifest",
195
172
  ],
@@ -231,12 +208,11 @@ const HELP_TOPICS = [
231
208
  title: "Asset Upload Workflow",
232
209
  summary: "Upload GLB/image assets with MCP tools and keep returned URLs in context.",
233
210
  steps: [
234
- "Use recommend_context with stage=assets for minimal resource loading.",
235
- "Use upload_glb or upload_glbs_from_folder for model assets.",
236
- "Use upload_image or upload_images_from_folder for image assets.",
211
+ "Use upload_glb for model assets (supports single file or folder).",
212
+ "Use upload_image for image assets (supports single file or folder).",
237
213
  "Persist returned asset URLs before applying room data updates.",
238
214
  ],
239
- resources: ["docs://workflows/asset-pipeline", "docs://usage-rules"],
215
+ resources: ["docs://usage-rules"],
240
216
  aliases: ["glb", "images", "upload", "cdn"],
241
217
  },
242
218
  {
@@ -277,7 +253,7 @@ const HELP_TOPICS = [
277
253
  "Normalize non-standard input: use sentiment_map for words, out_of_range for numbers > 5.",
278
254
  "Branch: high (4-5) → ask what stood out, mid (3) → ask what would improve it, low (1-2) → ask what went wrong and offer to iterate.",
279
255
  "Use canonical tags: positive (great-layout, good-mechanics, nice-visuals, good-audio, matched-vision, fun-to-play) or negative (wrong-layout, missing-mechanic, wrong-recipe, needs-polish, missing-audio, too-hard, too-easy, confusing, incomplete, performance).",
280
- "Call rate_build with contextToken, rating, tags, optional aspects (Match, Polish, Completeness), and comment.",
256
+ "Call rate_build with rating, tags, optional aspects (Match, Polish, Completeness), and comment.",
281
257
  ],
282
258
  resources: ["docs://usage-rules", "docs://guide/workflow-steps"],
283
259
  aliases: ["rate", "rating", "review"],
@@ -287,11 +263,9 @@ const HELP_TOPICS = [
287
263
  title: "Context Selection",
288
264
  summary: "Load high-signal resources in stable order to reduce token usage.",
289
265
  steps: [
290
- "Load docs://ai/bootstrap first.",
266
+ "Call get_context with your task description for targeted inline knowledge.",
291
267
  "Use the lookup tool for specific item/trigger/effect specs (~100 tokens each).",
292
- "Call recommend_context for broader task-specific resource selection.",
293
268
  "Read docs://index/knowledge-map to navigate all available content.",
294
- "Load priority-2 resources from recommend_context when task details are ambiguous.",
295
269
  "Avoid loading full snapshots unless condensed summaries are insufficient.",
296
270
  ],
297
271
  resources: [
@@ -558,7 +532,6 @@ export function registerDocsResources(server) {
558
532
  description: "Machine-readable JSON catalog of all docs:// resources exposed by this server.",
559
533
  mimeType: "application/json",
560
534
  }, async (uri) => {
561
- markResourceRead(uri.toString());
562
535
  return {
563
536
  contents: [
564
537
  {
@@ -575,7 +548,6 @@ export function registerDocsResources(server) {
575
548
  description: resource.description,
576
549
  mimeType: resource.mimeType,
577
550
  }, async (uri) => {
578
- markResourceRead(uri.toString());
579
551
  const text = await readDocResource(resource);
580
552
  return {
581
553
  contents: [
@@ -1,15 +1,15 @@
1
- export function buildSceneRefinementPrompt(contextToken, roomId, filePath, mode) {
1
+ export function buildSceneRefinementPrompt(sessionId, roomId, filePath, mode) {
2
2
  if (mode === "full") {
3
3
  return {
4
4
  required_action: true,
5
5
  action: "scene_refinement_loop",
6
- instruction: "Before collecting a push rating, you MUST run the scene refinement loop. " +
6
+ instruction: "You MUST run the scene refinement loop. " +
7
7
  "Call analyze_scene with the snapshot file path. Review the check results. " +
8
8
  "For each failed check, apply the suggested corrections via apply_operations. " +
9
9
  "Then ask the user: 'Would you like another scene review pass?' " +
10
10
  "If yes, call analyze_scene again and repeat. " +
11
- "If no (or all checks pass), proceed to collect the push rating.",
12
- context_token: contextToken,
11
+ "If no (or all checks pass), proceed.",
12
+ session_id: sessionId,
13
13
  room_id: roomId,
14
14
  file_path: filePath,
15
15
  convergence: {
@@ -26,95 +26,9 @@ export function buildSceneRefinementPrompt(contextToken, roomId, filePath, mode)
26
26
  "Call analyze_scene with focus.objectIds set to the IDs of modified/added items. " +
27
27
  "Review any failed checks related to those items and apply fixes if needed. " +
28
28
  "This is a single-pass review, not an iterative loop.",
29
- context_token: contextToken,
29
+ session_id: sessionId,
30
30
  room_id: roomId,
31
31
  file_path: filePath,
32
32
  submit_via: "analyze_scene",
33
33
  };
34
34
  }
35
- export function buildPushRatingPrompt(contextToken, roomId, pushInfo) {
36
- return {
37
- required_action: true,
38
- action: "collect_push_rating",
39
- instruction: "You MUST ask the user this question and wait for their response before calling rate_push. " +
40
- "Do NOT self-assess or guess — the rating must come from the user. " +
41
- "If the user declines or ignores, say 'No problem!' and move on without calling rate_push.",
42
- context_token: contextToken ?? null,
43
- room_id: roomId,
44
- phase_number: pushInfo.phaseNumber,
45
- push_in_phase: pushInfo.pushCountInPhase,
46
- total_pushes: pushInfo.pushCountInSession,
47
- script: {
48
- prompt: "Quick check — on a scale of 1-5, how did this update land? (Feel free to add any comments too)",
49
- scale_labels: {
50
- "1": "Poor",
51
- "2": "Below expectations",
52
- "3": "Decent",
53
- "4": "Good",
54
- "5": "Nailed it",
55
- },
56
- after_rating: "If they want more changes, use phase_trigger on the next push. If they're satisfied, call rate_build for the overall session rating.",
57
- },
58
- submit_via: "rate_push",
59
- };
60
- }
61
- export function buildSessionRatingPrompt(contextToken, roomId) {
62
- return {
63
- trigger: "after_user_tests_build",
64
- context_token: contextToken ?? null,
65
- room_id: roomId,
66
- script: {
67
- opening: "Nice work testing the build! I'd love to hear how it turned out.",
68
- rating_question: "On a quick scale of 1-5, how did this build land for you?",
69
- scale_labels: {
70
- "1": "Poor — major issues",
71
- "2": "Below expectations",
72
- "3": "Decent — works but needs improvement",
73
- "4": "Good — close to what I wanted",
74
- "5": "Excellent — nailed it",
75
- },
76
- followup_high: "Awesome! Anything that stood out? I'll tag the highlights so future builds keep that quality.",
77
- followup_mid: "Got it. What would push this from a 3 to a 5? I can note the specific areas.",
78
- followup_low: "Sorry about that. Was it the layout, the mechanics, missing features, or something else? I want to capture what needs fixing and can take another pass.",
79
- closing: "Thanks for the feedback — it helps the system get smarter at building.",
80
- },
81
- tag_vocabulary: {
82
- positive: [
83
- { tag: "great-layout", meaning: "Spatial design felt right" },
84
- { tag: "good-mechanics", meaning: "Mechanics were fun and worked" },
85
- { tag: "nice-visuals", meaning: "Lighting and visual polish were strong" },
86
- { tag: "good-audio", meaning: "Sound enhanced the experience" },
87
- { tag: "matched-vision", meaning: "Build matched what was described" },
88
- { tag: "fun-to-play", meaning: "Overall experience was enjoyable" },
89
- ],
90
- negative: [
91
- { tag: "wrong-layout", meaning: "Items misplaced, floating, or poorly arranged" },
92
- { tag: "missing-mechanic", meaning: "A requested mechanic was absent or broken" },
93
- { tag: "wrong-recipe", meaning: "Wrong approach or pattern for a mechanic" },
94
- { tag: "needs-polish", meaning: "Functional but rough" },
95
- { tag: "missing-audio", meaning: "Sound effects or ambient audio absent" },
96
- { tag: "too-hard", meaning: "Difficulty too high" },
97
- { tag: "too-easy", meaning: "Not enough challenge" },
98
- { tag: "confusing", meaning: "Unclear what to do" },
99
- { tag: "incomplete", meaning: "Build felt unfinished" },
100
- { tag: "performance", meaning: "Lag or too many objects" },
101
- ],
102
- },
103
- normalization: {
104
- out_of_range: "Map to 1-5: 0/negative→1, 6-7→4, 8-10→5. Percentages proportionally.",
105
- sentiment_map: {
106
- terrible: 1, awful: 1, hate: 1,
107
- bad: 2, disappointing: 2, meh: 2,
108
- okay: 3, fine: 3, decent: 3, alright: 3,
109
- good: 4, nice: 4, solid: 4, "pretty good": 4,
110
- amazing: 5, perfect: 5, "love it": 5, excellent: 5, awesome: 5, great: 5,
111
- },
112
- non_numeric: "Map sentiment to closest rating, confirm with user: 'Sounds like a 4/5 — does that feel right?'",
113
- },
114
- aspects: {
115
- accuracy: "Match — did this match what you asked for?",
116
- quality: "Polish — does it look and feel good?",
117
- completeness: "Completeness — is anything missing?",
118
- },
119
- };
120
- }
package/dist/index.js CHANGED
@@ -60,7 +60,7 @@ async function main() {
60
60
  }
61
61
  // Initialize intelligence service in the background (non-blocking)
62
62
  initIntelligence().catch(() => {
63
- // Intelligence init failed — recommend_context falls back to keyword matching
63
+ // Intelligence init failed — get_context falls back to keyword matching
64
64
  });
65
65
  // Start WebSocket bridge for browser communication
66
66
  startBridge();
@@ -95,7 +95,21 @@ export async function ensureIntelligence() {
95
95
  })();
96
96
  return retryInFlight;
97
97
  }
98
- export function shutdownIntelligence() {
98
+ export function shutdownIntelligence(activeSessionIds) {
99
+ // Close any open sessions before shutting down
100
+ if (initialized && activeSessionIds) {
101
+ for (const sessionId of activeSessionIds) {
102
+ try {
103
+ // endSession auto-computes avg confidence from ai_feedback
104
+ endSession(sessionId);
105
+ triggerRetrievalLearning();
106
+ uploadSessionTrace(sessionId);
107
+ }
108
+ catch {
109
+ // Best-effort — shutting down
110
+ }
111
+ }
112
+ }
99
113
  initialized = false;
100
114
  }
101
115
  // ── Search (blocking) ──
@@ -152,7 +166,7 @@ export function markSessionCompleted(sessionId) {
152
166
  export function endSession(sessionId, feedback) {
153
167
  if (!initialized)
154
168
  return;
155
- fireAndForget(`/sessions/${sessionId}/end`, feedback);
169
+ fireAndForget(`/sessions/${sessionId}/end`, feedback ?? {});
156
170
  }
157
171
  // ── Steps (fire-and-forget) ──
158
172
  export function recordStep(sessionId, step) {
@@ -244,17 +258,17 @@ export function recordPushToPhase(phaseId) {
244
258
  return;
245
259
  fireAndForget(`/phases/${phaseId}/push`, {});
246
260
  }
247
- // ── Push ratings ──
248
- export function ratePush(sessionId, opts) {
261
+ // ── AI feedback ──
262
+ export function recordAiFeedback(sessionId, feedback) {
249
263
  if (!initialized)
250
264
  return;
251
- fireAndForget(`/sessions/${sessionId}/push-ratings`, opts);
265
+ fireAndForget(`/sessions/${sessionId}/ai-feedback`, feedback);
252
266
  }
253
- export async function getPushRatings(sessionId) {
267
+ export async function getAiFeedback(sessionId) {
254
268
  if (!initialized)
255
269
  return [];
256
270
  try {
257
- return await httpGet(`/sessions/${sessionId}/push-ratings`);
271
+ return await httpGet(`/sessions/${sessionId}/ai-feedback`);
258
272
  }
259
273
  catch {
260
274
  return [];
@@ -293,11 +307,14 @@ export function uploadSessionTrace(sessionId) {
293
307
  Promise.all([
294
308
  getSession(sessionId),
295
309
  getPhases(sessionId),
296
- getPushRatings(sessionId),
310
+ getAiFeedback(sessionId),
297
311
  ])
298
- .then(([session, phases, pushRatings]) => {
312
+ .then(([session, phases, aiFeedback]) => {
299
313
  if (!session || session.rating == null)
300
314
  return;
315
+ // Aggregate doc gaps and recipe requests across all feedback entries
316
+ const allDocGaps = aiFeedback.flatMap((f) => f.docGaps);
317
+ const allRecipeRequests = aiFeedback.flatMap((f) => f.recipeRequests);
301
318
  // Build a simplified anonymized trace from available data
302
319
  const trace = {
303
320
  contributorId: getContributorId(),
@@ -311,8 +328,10 @@ export function uploadSessionTrace(sessionId) {
311
328
  contextSignals: [],
312
329
  toolSequence: [],
313
330
  phaseCount: phases.length,
314
- pushRatings: pushRatings.map((pr) => pr.rating),
315
- schemaVersion: 1,
331
+ pushConfidences: aiFeedback.map((f) => f.confidence),
332
+ docGaps: [...new Set(allDocGaps)],
333
+ recipeRequests: [...new Set(allRecipeRequests)],
334
+ schemaVersion: 2,
316
335
  };
317
336
  uploadTrace(trace);
318
337
  })
package/dist/lookup.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { readFile, readdir, stat } from "node:fs/promises";
2
2
  import { join, extname, relative } from "node:path";
3
- import { markResourceRead } from "./context-handshake.js";
4
3
  const index = [];
5
4
  let initialized = false;
6
5
  function normalizeKey(value) {
@@ -228,7 +227,6 @@ export function performLookup(term, category, type) {
228
227
  const readUris = new Set();
229
228
  for (const entry of matches) {
230
229
  if (!readUris.has(entry.uri)) {
231
- markResourceRead(entry.uri);
232
230
  readUris.add(entry.uri);
233
231
  }
234
232
  }
@@ -260,7 +258,6 @@ export function performLookup(term, category, type) {
260
258
  });
261
259
  if (exactMatches.length > 0) {
262
260
  for (const entry of exactMatches) {
263
- markResourceRead(entry.uri);
264
261
  }
265
262
  return {
266
263
  match_type: "exact",
@@ -289,7 +286,6 @@ export function performLookup(term, category, type) {
289
286
  })
290
287
  .slice(0, 5);
291
288
  for (const entry of fuzzyMatches) {
292
- markResourceRead(entry.uri);
293
289
  }
294
290
  return {
295
291
  match_type: "fuzzy",