@growthub/cli 0.10.0 → 0.12.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.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +307 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +372 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/receipts/route.js +47 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +664 -82
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +1371 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +1383 -24
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +7 -21
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/ownership/ownership-panel.jsx +222 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/ownership/page.jsx +19 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/settings-shell.jsx +2 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +116 -24
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +497 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/growthub.config.json +20 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +19 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +23 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper-apply.js +473 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +583 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +34 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +3 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/helpers/export-training-traces.mjs +144 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/helpers/grade-raw-pairs.mjs +279 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/helpers/harvest-cursor-traces.mjs +288 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/helpers/upload-graded-traces.mjs +128 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +19 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/templates/seeded-configs/alignment-loop.config.json +264 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/workers/custom-workspace-operator/CLAUDE.md +38 -0
- package/dist/index.js +1416 -2627
- package/package.json +1 -1
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Helper — grammar-aware planning engine.
|
|
3
|
+
*
|
|
4
|
+
* This module provides three public functions:
|
|
5
|
+
*
|
|
6
|
+
* sanitizeWorkspaceSnapshot(workspaceConfig)
|
|
7
|
+
* Strip credentials, envRefs values, and source-record contents from the
|
|
8
|
+
* live config so only schema shape enters the inference prompt. Never
|
|
9
|
+
* let secret-adjacent fields travel to a local model.
|
|
10
|
+
*
|
|
11
|
+
* buildHelperSystemPrompt(snapshot, intent)
|
|
12
|
+
* Return a workspace-grammar-injected system prompt. The prompt teaches
|
|
13
|
+
* the local model about the PATCH allowlist, known widget kinds, known
|
|
14
|
+
* object types, and proposal shape — so its JSON output maps cleanly to
|
|
15
|
+
* the apply step without heuristic translation.
|
|
16
|
+
*
|
|
17
|
+
* parseHelperEnvelope(rawEnvelope, intent)
|
|
18
|
+
* Unwrap the growthub-local-model-sandbox-v1 envelope returned by the
|
|
19
|
+
* local-intelligence adapter and extract typed proposals[].
|
|
20
|
+
*
|
|
21
|
+
* validateProposals(proposals)
|
|
22
|
+
* Check each proposal type against WORKSPACE_HELPER_PROPOSAL_TYPES and
|
|
23
|
+
* confirm affectedField is consistent with PROPOSAL_TYPE_TO_PATCH_FIELD.
|
|
24
|
+
*
|
|
25
|
+
* The local-intelligence adapter is the only execution backend used here.
|
|
26
|
+
* No tool execution, no credential access, no direct workspace writes.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const WORKSPACE_HELPER_PROPOSAL_TYPES = [
|
|
30
|
+
"dashboard.create",
|
|
31
|
+
"dashboard.update",
|
|
32
|
+
"widgetType.bind",
|
|
33
|
+
"canvas.widget.add",
|
|
34
|
+
"canvas.tab.create",
|
|
35
|
+
"dataModel.object.create",
|
|
36
|
+
"dataModel.object.update",
|
|
37
|
+
"dataModel.row.add",
|
|
38
|
+
"repair.binding",
|
|
39
|
+
"explain.object",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const PROPOSAL_TYPE_TO_PATCH_FIELD = {
|
|
43
|
+
"dashboard.create": "dashboards",
|
|
44
|
+
"dashboard.update": "dashboards",
|
|
45
|
+
"widgetType.bind": "widgetTypes",
|
|
46
|
+
"canvas.widget.add": "canvas",
|
|
47
|
+
"canvas.tab.create": "canvas",
|
|
48
|
+
"dataModel.object.create": "dataModel",
|
|
49
|
+
"dataModel.object.update": "dataModel",
|
|
50
|
+
"dataModel.row.add": "dataModel",
|
|
51
|
+
"repair.binding": "dataModel",
|
|
52
|
+
"explain.object": "dataModel",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const KNOWN_WIDGET_KINDS = ["chart", "view", "iframe", "rich-text"];
|
|
56
|
+
const KNOWN_OBJECT_TYPES = ["data-source", "api-registry", "people", "tasks", "sandbox-environment", "custom"];
|
|
57
|
+
const PATCH_ALLOWLIST = ["dashboards", "widgetTypes", "canvas", "dataModel"];
|
|
58
|
+
|
|
59
|
+
const INTENT_DESCRIPTIONS = {
|
|
60
|
+
build_dashboard:
|
|
61
|
+
"Draft one or more dashboard objects and the widget layout to populate them. Propose new dashboards with starter sections and widget placements that match the user's business brief.",
|
|
62
|
+
create_widget:
|
|
63
|
+
"Suggest which widgetTypes belong on the workspace based on the object schema and target KPIs. Propose widgetType entries and canvas widget placements.",
|
|
64
|
+
register_api:
|
|
65
|
+
"Draft API Registry rows (dataModel object of objectType api-registry) including integration labels, credential prompts, base URL, endpoint, auth header, and method.",
|
|
66
|
+
create_object:
|
|
67
|
+
"Translate the user's domain language into a new dataModel object: objectType, label, columns, starter rows, and field settings that make sense for their business.",
|
|
68
|
+
edit_view:
|
|
69
|
+
"Propose updates to an existing dashboard or canvas configuration to improve the view layout, widget bindings, or tab structure.",
|
|
70
|
+
repair:
|
|
71
|
+
"Inspect the workspace snapshot for broken references, missing bindings, empty objects, or incomplete views. Propose the minimum changes needed to repair each issue.",
|
|
72
|
+
explain:
|
|
73
|
+
"Return a clear explanation of what one or more workspace objects, widgets, or configurations do. Use the explain.object proposal type — payload is { explanation: string }.",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* L3 heuristic intent router.
|
|
78
|
+
*
|
|
79
|
+
* Pure regex, safe, non-destructive. Used when the caller did not explicitly
|
|
80
|
+
* pick a pill — or when the user's prompt strongly contradicts the default
|
|
81
|
+
* intent. Returns the inferred intent and a small confidence score (number
|
|
82
|
+
* of matched signals). The caller decides whether to honor it.
|
|
83
|
+
*
|
|
84
|
+
* Order matters: more specific intents come first so `register_api` wins over
|
|
85
|
+
* `create_object` when the prompt mentions both "object" and "API".
|
|
86
|
+
*/
|
|
87
|
+
const INTENT_HEURISTIC_PATTERNS = [
|
|
88
|
+
{ intent: "register_api", patterns: [
|
|
89
|
+
/\b(api|endpoint|webhook|integration|connector|oauth|bearer\s+token|auth\s+header)\b/i,
|
|
90
|
+
/\b(register|connect|wire|hook\s*up)\b.*\b(api|endpoint|webhook|service|integration)\b/i,
|
|
91
|
+
/\b(rest|graphql|grpc)\b/i,
|
|
92
|
+
]},
|
|
93
|
+
{ intent: "repair", patterns: [
|
|
94
|
+
/\b(repair|fix|broken|missing|orphan|incomplete|dangling|stale|drift)\b/i,
|
|
95
|
+
/\b(why\s+is(n'?t)?|what'?s\s+wrong\s+with)\b/i,
|
|
96
|
+
/\bbroken\s+(binding|reference|link|widget|view)\b/i,
|
|
97
|
+
]},
|
|
98
|
+
{ intent: "explain", patterns: [
|
|
99
|
+
/\b(explain|describe|summari[sz]e|what\s+(is|does|are)\b|how\s+(does|do|is|are)\b|tell\s+me\s+about)\b/i,
|
|
100
|
+
/\bwhy\s+does\b/i,
|
|
101
|
+
]},
|
|
102
|
+
{ intent: "build_dashboard", patterns: [
|
|
103
|
+
/\b(dashboard|kpi|metric|report(ing)?|overview|home\s*page)\b/i,
|
|
104
|
+
/\b(build|create|draft|design|set\s*up|spin\s*up)\b.*\b(dashboard|view|page|report)\b/i,
|
|
105
|
+
]},
|
|
106
|
+
{ intent: "create_widget", patterns: [
|
|
107
|
+
/\b(widget|chart|graph|tile|card|visuali[sz]ation|plot|figure)\b/i,
|
|
108
|
+
/\b(add|create|insert|place)\b.*\b(widget|chart|graph|tile|card)\b/i,
|
|
109
|
+
]},
|
|
110
|
+
{ intent: "edit_view", patterns: [
|
|
111
|
+
/\b(edit|update|change|modify|tweak|adjust|rename|reorder|move|resize|recolor|relayout|tidy)\b/i,
|
|
112
|
+
/\b(rearrange|reorganize|polish|improve)\b.*\b(view|layout|dashboard|tab|page)\b/i,
|
|
113
|
+
]},
|
|
114
|
+
{ intent: "create_object", patterns: [
|
|
115
|
+
/\b(object|table|list|collection|entity|record(s)?|database|schema|model)\b/i,
|
|
116
|
+
/\btrack(ing)?\b/i,
|
|
117
|
+
/\b(create|add|build|spin\s*up|set\s*up)\b.*\b(object|table|list|entity|catalog)\b/i,
|
|
118
|
+
]},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const VALID_INTENT_VALUES = Object.keys(INTENT_DESCRIPTIONS);
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Infer the user's intent from free-form prompt text. The fallback is
|
|
125
|
+
* returned when no pattern matches. Returns:
|
|
126
|
+
* { intent: <one of VALID_INTENT_VALUES>, confidence: number, matched: string[] }
|
|
127
|
+
*
|
|
128
|
+
* Confidence is just the count of matched patterns across all intent groups
|
|
129
|
+
* for the winning intent. Zero means "no signal, keep the caller's choice".
|
|
130
|
+
*
|
|
131
|
+
* This is L3 of the helper routing ladder: L1 = user prompt init,
|
|
132
|
+
* L2 = parse, L3 = heuristic regex. All three stages run server-side; the
|
|
133
|
+
* client never has to interpret the prompt itself.
|
|
134
|
+
*/
|
|
135
|
+
function inferIntentFromPrompt(prompt, fallback) {
|
|
136
|
+
const text = typeof prompt === "string" ? prompt.trim() : "";
|
|
137
|
+
const safeFallback = VALID_INTENT_VALUES.includes(fallback) ? fallback : "create_object";
|
|
138
|
+
if (!text) return { intent: safeFallback, confidence: 0, matched: [] };
|
|
139
|
+
const counts = new Map();
|
|
140
|
+
const matched = [];
|
|
141
|
+
for (const { intent, patterns } of INTENT_HEURISTIC_PATTERNS) {
|
|
142
|
+
for (const re of patterns) {
|
|
143
|
+
if (re.test(text)) {
|
|
144
|
+
counts.set(intent, (counts.get(intent) || 0) + 1);
|
|
145
|
+
matched.push(`${intent}::${re.source}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (counts.size === 0) return { intent: safeFallback, confidence: 0, matched: [] };
|
|
150
|
+
let bestIntent = safeFallback;
|
|
151
|
+
let best = 0;
|
|
152
|
+
for (const [intent, count] of counts) {
|
|
153
|
+
if (count > best) { best = count; bestIntent = intent; }
|
|
154
|
+
}
|
|
155
|
+
return { intent: bestIntent, confidence: best, matched };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build a stable system message for chat completions.
|
|
160
|
+
*
|
|
161
|
+
* Intentionally split from the snapshot-aware system prompt so the system
|
|
162
|
+
* message stays IDENTICAL across all turns of the same intent inside one
|
|
163
|
+
* thread — this is what lets the local-intelligence endpoint's KV cache
|
|
164
|
+
* reuse the prefix on consecutive turns. Snapshot state travels as a
|
|
165
|
+
* regular message at the start of the conversation, not in the system slot.
|
|
166
|
+
*/
|
|
167
|
+
function buildStableSystemPrompt(intent) {
|
|
168
|
+
const intentDesc = INTENT_DESCRIPTIONS[intent] || INTENT_DESCRIPTIONS["explain"];
|
|
169
|
+
return [
|
|
170
|
+
"You are the Growthub Workspace Helper — a governed, workspace-grammar-aware planning engine.",
|
|
171
|
+
"",
|
|
172
|
+
"## Operating contract",
|
|
173
|
+
"- You are propose-only. Mutation happens through a separate governed apply step the user explicitly triggers.",
|
|
174
|
+
"- You speak in valid JSON only. The user-facing UI extracts `summary` and `proposals` from your output.",
|
|
175
|
+
"- On the FIRST user turn of a thread, briefly confirm what you understood and, if necessary, ask ONE clarifying question via the `summary` field with `proposals: []`. Otherwise, propose immediately.",
|
|
176
|
+
"- On every subsequent turn, react to the delta between the user's latest message and the conversation so far.",
|
|
177
|
+
"- Never invent proposal types. Never invent affectedField values outside the PATCH allowlist.",
|
|
178
|
+
"- Never include credentials, env-ref values, provider tokens, or secrets in any payload.",
|
|
179
|
+
"",
|
|
180
|
+
"## Current intent",
|
|
181
|
+
`${intent} — ${intentDesc}`,
|
|
182
|
+
"",
|
|
183
|
+
"## Growthub workspace grammar",
|
|
184
|
+
`Known widget kinds: ${KNOWN_WIDGET_KINDS.join(", ")}`,
|
|
185
|
+
`Known object types: ${KNOWN_OBJECT_TYPES.join(", ")}`,
|
|
186
|
+
`PATCH allowlist (only these top-level keys can be mutated): ${PATCH_ALLOWLIST.join(", ")}`,
|
|
187
|
+
"",
|
|
188
|
+
"## Valid proposal types and their target patch field",
|
|
189
|
+
WORKSPACE_HELPER_PROPOSAL_TYPES.map(
|
|
190
|
+
(t) => ` ${t} → ${PROPOSAL_TYPE_TO_PATCH_FIELD[t]}`
|
|
191
|
+
).join("\n"),
|
|
192
|
+
"",
|
|
193
|
+
"## Output envelope — ALWAYS one JSON object",
|
|
194
|
+
JSON.stringify({
|
|
195
|
+
summary: "One sentence: what you understood, or what you are proposing and why",
|
|
196
|
+
proposals: [
|
|
197
|
+
{
|
|
198
|
+
type: "<proposal type from the list above>",
|
|
199
|
+
affectedField: "<dashboards | widgetTypes | canvas | dataModel>",
|
|
200
|
+
payload: { "...": "partial config fragment for this patch field" },
|
|
201
|
+
rationale: "Why this change helps the user",
|
|
202
|
+
confidence: 0.9,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
warnings: ["any non-blocking issues or caveats"],
|
|
206
|
+
}),
|
|
207
|
+
].join("\n");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Build a workspace-state user message for the start of a conversation.
|
|
212
|
+
*
|
|
213
|
+
* Travels as a regular `user` message (not a system message) so the static
|
|
214
|
+
* system prompt above stays cacheable. The model is instructed in the
|
|
215
|
+
* stable system message to treat the first user message as state context.
|
|
216
|
+
*/
|
|
217
|
+
function buildWorkspaceStateMessage(snapshot) {
|
|
218
|
+
const summary = snapshot
|
|
219
|
+
? [
|
|
220
|
+
snapshot.workspaceName ? `Workspace: "${snapshot.workspaceName}"` : null,
|
|
221
|
+
snapshot.dashboards?.length
|
|
222
|
+
? `Dashboards: ${snapshot.dashboards.map((d) => `"${d.name}" (${d.status || "draft"})`).join(", ")}`
|
|
223
|
+
: "Dashboards: none yet",
|
|
224
|
+
snapshot.widgetTypes?.length
|
|
225
|
+
? `Widget types registered: ${snapshot.widgetTypes.map((w) => w.kind).join(", ")}`
|
|
226
|
+
: "Widget types: none yet",
|
|
227
|
+
snapshot.dataModelObjects?.length
|
|
228
|
+
? `Data model objects: ${snapshot.dataModelObjects.map((o) => `"${o.label}" [${o.objectType || "custom"}] (${o.rowCount} rows)`).join("; ")}`
|
|
229
|
+
: "Data model objects: none yet",
|
|
230
|
+
snapshot.canvasSummary
|
|
231
|
+
? `Canvas: ${snapshot.canvasSummary.widgetCount} widgets across ${snapshot.canvasSummary.tabCount} tab(s)`
|
|
232
|
+
: null,
|
|
233
|
+
]
|
|
234
|
+
.filter(Boolean)
|
|
235
|
+
.join("\n")
|
|
236
|
+
: "No existing workspace context provided.";
|
|
237
|
+
return `Workspace state (read-only context — do not echo back):\n${summary}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Assemble the full chat-completion message array for one turn of a
|
|
242
|
+
* helper thread. Includes the stable system prompt, the workspace-state
|
|
243
|
+
* user message, all prior turns from the thread row's `messages[]`
|
|
244
|
+
* (capped to the most recent N), and the new user message at the end.
|
|
245
|
+
*
|
|
246
|
+
* This is what gets passed to the local-intelligence adapter via
|
|
247
|
+
* `intelligenceSandbox.messages`. The adapter forwards it verbatim to
|
|
248
|
+
* the OpenAI-compatible chat completions endpoint.
|
|
249
|
+
*/
|
|
250
|
+
function buildChatMessages({ snapshot, intent, priorMessages, newUserPrompt, maxPriorTurns = 200 }) {
|
|
251
|
+
const out = [];
|
|
252
|
+
out.push({ role: "system", content: buildStableSystemPrompt(intent) });
|
|
253
|
+
out.push({ role: "user", content: buildWorkspaceStateMessage(snapshot) });
|
|
254
|
+
out.push({ role: "assistant", content: '{"summary":"Acknowledged the workspace state. Ready to help.","proposals":[],"warnings":[]}' });
|
|
255
|
+
// Replay history (capped). Filter to clean user/assistant turns only.
|
|
256
|
+
const prior = Array.isArray(priorMessages) ? priorMessages : [];
|
|
257
|
+
const cleaned = prior
|
|
258
|
+
.filter((m) => m && typeof m.role === "string" && typeof m.content === "string" && (m.role === "user" || m.role === "assistant"));
|
|
259
|
+
const capped = cleaned.slice(Math.max(0, cleaned.length - maxPriorTurns));
|
|
260
|
+
for (const m of capped) out.push({ role: m.role, content: m.content });
|
|
261
|
+
out.push({ role: "user", content: newUserPrompt });
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Strip envRefs values, credentials, and row data from the live config.
|
|
267
|
+
* Only schema shape (column names, object types, dashboard ids/names) travels
|
|
268
|
+
* into the inference prompt.
|
|
269
|
+
*
|
|
270
|
+
* Accepts both shapes:
|
|
271
|
+
* - a full workspaceConfig (with `dataModel.objects`, `dashboards`, `canvas`).
|
|
272
|
+
* - an already-sanitized snapshot (with `dataModelObjects`, `canvasSummary`).
|
|
273
|
+
* In both cases the output is a fresh snapshot containing only schema shape.
|
|
274
|
+
*/
|
|
275
|
+
function sanitizeWorkspaceSnapshot(input) {
|
|
276
|
+
if (!input || typeof input !== "object") return {};
|
|
277
|
+
|
|
278
|
+
const isSnapshotShape =
|
|
279
|
+
Array.isArray(input.dataModelObjects) ||
|
|
280
|
+
typeof input.canvasSummary === "object";
|
|
281
|
+
|
|
282
|
+
const dashboards = Array.isArray(input.dashboards)
|
|
283
|
+
? input.dashboards.map((d) => ({
|
|
284
|
+
id: typeof d?.id === "string" ? d.id : undefined,
|
|
285
|
+
name: typeof d?.name === "string" ? d.name : undefined,
|
|
286
|
+
status: typeof d?.status === "string" ? d.status : undefined,
|
|
287
|
+
}))
|
|
288
|
+
: [];
|
|
289
|
+
|
|
290
|
+
const widgetTypes = Array.isArray(input.widgetTypes)
|
|
291
|
+
? input.widgetTypes.map((w) => ({
|
|
292
|
+
kind: typeof w?.kind === "string" ? w.kind : undefined,
|
|
293
|
+
label: typeof w?.label === "string" ? w.label : undefined,
|
|
294
|
+
}))
|
|
295
|
+
: [];
|
|
296
|
+
|
|
297
|
+
let canvasSummary;
|
|
298
|
+
if (isSnapshotShape && input.canvasSummary && typeof input.canvasSummary === "object") {
|
|
299
|
+
canvasSummary = {
|
|
300
|
+
widgetCount: Number.isFinite(input.canvasSummary.widgetCount) ? input.canvasSummary.widgetCount : 0,
|
|
301
|
+
tabCount: Number.isFinite(input.canvasSummary.tabCount) ? input.canvasSummary.tabCount : 1,
|
|
302
|
+
activeTabId: typeof input.canvasSummary.activeTabId === "string" ? input.canvasSummary.activeTabId : undefined,
|
|
303
|
+
};
|
|
304
|
+
} else if (input.canvas && typeof input.canvas === "object") {
|
|
305
|
+
canvasSummary = {
|
|
306
|
+
widgetCount: Array.isArray(input.canvas.widgets) ? input.canvas.widgets.length : 0,
|
|
307
|
+
tabCount: Array.isArray(input.canvas.tabs) ? input.canvas.tabs.length : 1,
|
|
308
|
+
activeTabId: typeof input.canvas.activeTabId === "string" ? input.canvas.activeTabId : undefined,
|
|
309
|
+
};
|
|
310
|
+
} else {
|
|
311
|
+
canvasSummary = { widgetCount: 0, tabCount: 1 };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const rawObjects = isSnapshotShape
|
|
315
|
+
? (Array.isArray(input.dataModelObjects) ? input.dataModelObjects : [])
|
|
316
|
+
: (Array.isArray(input.dataModel?.objects) ? input.dataModel.objects : []);
|
|
317
|
+
const dataModelObjects = rawObjects.map((obj) => ({
|
|
318
|
+
id: typeof obj?.id === "string" ? obj.id : undefined,
|
|
319
|
+
label: typeof obj?.label === "string" ? obj.label : undefined,
|
|
320
|
+
objectType: typeof obj?.objectType === "string" ? obj.objectType : undefined,
|
|
321
|
+
columns: Array.isArray(obj?.columns) ? obj.columns.filter((c) => typeof c === "string") : [],
|
|
322
|
+
rowCount: Number.isFinite(obj?.rowCount)
|
|
323
|
+
? obj.rowCount
|
|
324
|
+
: (Array.isArray(obj?.rows) ? obj.rows.length : 0),
|
|
325
|
+
}));
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
workspaceId: typeof input.workspaceId === "string" ? input.workspaceId : (typeof input.id === "string" ? input.id : undefined),
|
|
329
|
+
workspaceName: typeof input.workspaceName === "string" ? input.workspaceName : (typeof input.name === "string" ? input.name : undefined),
|
|
330
|
+
dashboards,
|
|
331
|
+
widgetTypes,
|
|
332
|
+
canvasSummary,
|
|
333
|
+
dataModelObjects,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Build the workspace-grammar-injected system prompt for a given intent.
|
|
339
|
+
* The prompt teaches the model the exact proposal shape, PATCH allowlist,
|
|
340
|
+
* and schema constraints so output maps cleanly to the apply step.
|
|
341
|
+
*/
|
|
342
|
+
function buildHelperSystemPrompt(snapshot, intent) {
|
|
343
|
+
const intentDesc = INTENT_DESCRIPTIONS[intent] || INTENT_DESCRIPTIONS["explain"];
|
|
344
|
+
|
|
345
|
+
const snapshotSummary = snapshot
|
|
346
|
+
? [
|
|
347
|
+
snapshot.workspaceName ? `Workspace: "${snapshot.workspaceName}"` : null,
|
|
348
|
+
snapshot.dashboards?.length
|
|
349
|
+
? `Dashboards: ${snapshot.dashboards.map((d) => `"${d.name}" (${d.status || "draft"})`).join(", ")}`
|
|
350
|
+
: "Dashboards: none yet",
|
|
351
|
+
snapshot.widgetTypes?.length
|
|
352
|
+
? `Widget types registered: ${snapshot.widgetTypes.map((w) => w.kind).join(", ")}`
|
|
353
|
+
: "Widget types: none yet",
|
|
354
|
+
snapshot.dataModelObjects?.length
|
|
355
|
+
? `Data model objects: ${snapshot.dataModelObjects.map((o) => `"${o.label}" [${o.objectType || "custom"}] (${o.rowCount} rows)`).join("; ")}`
|
|
356
|
+
: "Data model objects: none yet",
|
|
357
|
+
snapshot.canvasSummary
|
|
358
|
+
? `Canvas: ${snapshot.canvasSummary.widgetCount} widgets across ${snapshot.canvasSummary.tabCount} tab(s)`
|
|
359
|
+
: null,
|
|
360
|
+
]
|
|
361
|
+
.filter(Boolean)
|
|
362
|
+
.join("\n")
|
|
363
|
+
: "No existing workspace context provided.";
|
|
364
|
+
|
|
365
|
+
return [
|
|
366
|
+
"You are the Growthub Workspace Helper — a governed, workspace-grammar-aware planning engine.",
|
|
367
|
+
"",
|
|
368
|
+
"## Your task",
|
|
369
|
+
intentDesc,
|
|
370
|
+
"",
|
|
371
|
+
"## Workspace context",
|
|
372
|
+
snapshotSummary,
|
|
373
|
+
"",
|
|
374
|
+
"## Growthub workspace grammar",
|
|
375
|
+
`Known widget kinds: ${KNOWN_WIDGET_KINDS.join(", ")}`,
|
|
376
|
+
`Known object types: ${KNOWN_OBJECT_TYPES.join(", ")}`,
|
|
377
|
+
`PATCH allowlist (only these keys can be mutated): ${PATCH_ALLOWLIST.join(", ")}`,
|
|
378
|
+
"",
|
|
379
|
+
"## Valid proposal types and their target patch field",
|
|
380
|
+
WORKSPACE_HELPER_PROPOSAL_TYPES.map(
|
|
381
|
+
(t) => ` ${t} → ${PROPOSAL_TYPE_TO_PATCH_FIELD[t]}`
|
|
382
|
+
).join("\n"),
|
|
383
|
+
"",
|
|
384
|
+
"## Output format — reply with a SINGLE JSON object only",
|
|
385
|
+
JSON.stringify({
|
|
386
|
+
summary: "One sentence: what you are proposing and why",
|
|
387
|
+
proposals: [
|
|
388
|
+
{
|
|
389
|
+
type: "<proposal type from the list above>",
|
|
390
|
+
affectedField: "<dashboards | widgetTypes | canvas | dataModel>",
|
|
391
|
+
payload: { "...": "partial config fragment for this patch field" },
|
|
392
|
+
rationale: "Why this change helps the user",
|
|
393
|
+
confidence: 0.9,
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
warnings: ["any non-blocking issues or caveats"],
|
|
397
|
+
}),
|
|
398
|
+
"",
|
|
399
|
+
"Rules:",
|
|
400
|
+
"- Use only proposal types from the list above. Never invent new types.",
|
|
401
|
+
"- affectedField must match PROPOSAL_TYPE_TO_PATCH_FIELD exactly.",
|
|
402
|
+
"- payload must be a partial fragment valid for its affectedField, matching Growthub workspace schema.",
|
|
403
|
+
"- Do NOT include envRefs values, API keys, or secrets in any payload.",
|
|
404
|
+
"- toolIntents are not applicable here — the helper is propose-only.",
|
|
405
|
+
"- If you cannot produce valid proposals for the given intent, return an empty proposals array with a clear warning.",
|
|
406
|
+
].join("\n");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Unwrap the growthub-local-model-sandbox-v1 envelope from the
|
|
411
|
+
* local-intelligence adapter and extract typed proposals.
|
|
412
|
+
*
|
|
413
|
+
* The adapter stdout is a stringified envelope object. Inside the envelope,
|
|
414
|
+
* result.json should contain { summary, proposals, warnings }.
|
|
415
|
+
*/
|
|
416
|
+
function parseHelperEnvelope(rawEnvelope, intent) {
|
|
417
|
+
let envelope;
|
|
418
|
+
try {
|
|
419
|
+
envelope = typeof rawEnvelope === "string" ? JSON.parse(rawEnvelope) : rawEnvelope;
|
|
420
|
+
} catch {
|
|
421
|
+
return {
|
|
422
|
+
summary: "Failed to parse helper response.",
|
|
423
|
+
proposals: [],
|
|
424
|
+
warnings: ["Adapter returned non-JSON output."],
|
|
425
|
+
confidence: 0,
|
|
426
|
+
model: "unknown",
|
|
427
|
+
adapterMode: "unknown",
|
|
428
|
+
endpoint: "unknown",
|
|
429
|
+
latencyMs: 0,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const result = envelope?.result ?? {};
|
|
434
|
+
const adapterMeta = envelope?.adapter ?? {};
|
|
435
|
+
const latencyMs = typeof envelope?.latencyMs === "number" ? envelope.latencyMs : 0;
|
|
436
|
+
|
|
437
|
+
let parsed;
|
|
438
|
+
if (result.json && typeof result.json === "object") {
|
|
439
|
+
parsed = result.json;
|
|
440
|
+
} else if (typeof result.text === "string") {
|
|
441
|
+
try {
|
|
442
|
+
parsed = JSON.parse(result.text);
|
|
443
|
+
} catch {
|
|
444
|
+
parsed = null;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// rawText fallback — same pattern `helpers/grade-raw-pairs.mjs` uses
|
|
448
|
+
// when the adapter doesn't shape the inner gemma output under
|
|
449
|
+
// result.text / result.json. Pull the chat-completion content out of
|
|
450
|
+
// the raw response and try to parse it directly. This is what makes
|
|
451
|
+
// gemma3:4b output reliably reach the helper across every turn.
|
|
452
|
+
if (!parsed && typeof envelope?.rawText === "string") {
|
|
453
|
+
try {
|
|
454
|
+
const outer = JSON.parse(envelope.rawText);
|
|
455
|
+
const content = outer?.choices?.[0]?.message?.content;
|
|
456
|
+
if (typeof content === "string") {
|
|
457
|
+
const inner = JSON.parse(content);
|
|
458
|
+
if (inner && typeof inner === "object") {
|
|
459
|
+
// If the model emitted the canonical adapter envelope, descend
|
|
460
|
+
// into `json`. Otherwise treat the entire object as the helper
|
|
461
|
+
// envelope (model dropped the wrapper level — common with small
|
|
462
|
+
// models when response_format is set to json_object).
|
|
463
|
+
if (inner.json && typeof inner.json === "object") {
|
|
464
|
+
parsed = inner.json;
|
|
465
|
+
} else if (inner.summary !== undefined || inner.proposals !== undefined) {
|
|
466
|
+
parsed = inner;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} catch {
|
|
471
|
+
// fall through — caller surfaces "No structured response"
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (!parsed || typeof parsed !== "object") {
|
|
476
|
+
return {
|
|
477
|
+
summary: result.text || "No structured response from helper.",
|
|
478
|
+
proposals: [],
|
|
479
|
+
warnings: Array.isArray(result.warnings) ? result.warnings : ["Model did not return structured JSON proposals."],
|
|
480
|
+
confidence: 0,
|
|
481
|
+
model: adapterMeta.modelId || "unknown",
|
|
482
|
+
adapterMode: adapterMeta.mode || "unknown",
|
|
483
|
+
endpoint: adapterMeta.endpoint || "unknown",
|
|
484
|
+
latencyMs,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const rawProposals = Array.isArray(parsed.proposals) ? parsed.proposals : [];
|
|
489
|
+
const validProposals = rawProposals.filter(
|
|
490
|
+
(p) =>
|
|
491
|
+
p &&
|
|
492
|
+
typeof p.type === "string" &&
|
|
493
|
+
WORKSPACE_HELPER_PROPOSAL_TYPES.includes(p.type) &&
|
|
494
|
+
p.payload &&
|
|
495
|
+
typeof p.payload === "object"
|
|
496
|
+
);
|
|
497
|
+
const skippedCount = rawProposals.length - validProposals.length;
|
|
498
|
+
|
|
499
|
+
const warnings = Array.isArray(parsed.warnings) ? [...parsed.warnings] : [];
|
|
500
|
+
if (skippedCount > 0) {
|
|
501
|
+
warnings.push(`${skippedCount} proposal(s) had unknown types and were removed.`);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const proposals = validProposals.map((p) => ({
|
|
505
|
+
type: p.type,
|
|
506
|
+
affectedField: PROPOSAL_TYPE_TO_PATCH_FIELD[p.type],
|
|
507
|
+
payload: p.payload,
|
|
508
|
+
rationale: typeof p.rationale === "string" ? p.rationale : "",
|
|
509
|
+
confidence: typeof p.confidence === "number" ? p.confidence : undefined,
|
|
510
|
+
}));
|
|
511
|
+
|
|
512
|
+
const avgConfidence =
|
|
513
|
+
proposals.length > 0
|
|
514
|
+
? proposals.reduce((sum, p) => sum + (p.confidence ?? 0.5), 0) / proposals.length
|
|
515
|
+
: 0;
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
summary: typeof parsed.summary === "string" ? parsed.summary : `Workspace helper response for intent: ${intent}`,
|
|
519
|
+
proposals,
|
|
520
|
+
warnings,
|
|
521
|
+
confidence: avgConfidence,
|
|
522
|
+
model: adapterMeta.modelId || "unknown",
|
|
523
|
+
adapterMode: adapterMeta.mode || "unknown",
|
|
524
|
+
endpoint: adapterMeta.endpoint || "unknown",
|
|
525
|
+
latencyMs,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Validate proposals array before returning to the caller.
|
|
531
|
+
* Returns { valid: WorkspaceHelperProposal[], errors: string[] }.
|
|
532
|
+
*/
|
|
533
|
+
function validateProposals(proposals) {
|
|
534
|
+
if (!Array.isArray(proposals)) {
|
|
535
|
+
return { valid: [], errors: ["proposals must be an array"] };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const valid = [];
|
|
539
|
+
const errors = [];
|
|
540
|
+
|
|
541
|
+
for (let i = 0; i < proposals.length; i++) {
|
|
542
|
+
const p = proposals[i];
|
|
543
|
+
if (!p || typeof p !== "object") {
|
|
544
|
+
errors.push(`proposal[${i}]: must be an object`);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (!WORKSPACE_HELPER_PROPOSAL_TYPES.includes(p.type)) {
|
|
548
|
+
errors.push(`proposal[${i}]: unknown type "${p.type}"`);
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (!p.payload || typeof p.payload !== "object") {
|
|
552
|
+
errors.push(`proposal[${i}]: payload must be an object`);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const expectedField = PROPOSAL_TYPE_TO_PATCH_FIELD[p.type];
|
|
556
|
+
if (p.affectedField && p.affectedField !== expectedField) {
|
|
557
|
+
errors.push(
|
|
558
|
+
`proposal[${i}]: affectedField "${p.affectedField}" does not match expected "${expectedField}" for type "${p.type}"`
|
|
559
|
+
);
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
valid.push({ ...p, affectedField: expectedField });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return { valid, errors };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export {
|
|
569
|
+
sanitizeWorkspaceSnapshot,
|
|
570
|
+
buildHelperSystemPrompt,
|
|
571
|
+
buildStableSystemPrompt,
|
|
572
|
+
buildWorkspaceStateMessage,
|
|
573
|
+
buildChatMessages,
|
|
574
|
+
inferIntentFromPrompt,
|
|
575
|
+
parseHelperEnvelope,
|
|
576
|
+
validateProposals,
|
|
577
|
+
WORKSPACE_HELPER_PROPOSAL_TYPES,
|
|
578
|
+
PROPOSAL_TYPE_TO_PATCH_FIELD,
|
|
579
|
+
KNOWN_WIDGET_KINDS,
|
|
580
|
+
KNOWN_OBJECT_TYPES,
|
|
581
|
+
PATCH_ALLOWLIST,
|
|
582
|
+
VALID_INTENT_VALUES,
|
|
583
|
+
};
|
package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"name": "growthub-workspace-app",
|
|
9
9
|
"version": "1.0.0",
|
|
10
10
|
"dependencies": {
|
|
11
|
+
"@tanstack/react-table": "^8.21.3",
|
|
11
12
|
"lucide-react": "^0.468.0",
|
|
12
13
|
"next": "16.2.4",
|
|
13
14
|
"react": "19.2.4",
|
|
@@ -633,6 +634,39 @@
|
|
|
633
634
|
"tslib": "^2.8.0"
|
|
634
635
|
}
|
|
635
636
|
},
|
|
637
|
+
"node_modules/@tanstack/react-table": {
|
|
638
|
+
"version": "8.21.3",
|
|
639
|
+
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
|
|
640
|
+
"integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==",
|
|
641
|
+
"license": "MIT",
|
|
642
|
+
"dependencies": {
|
|
643
|
+
"@tanstack/table-core": "8.21.3"
|
|
644
|
+
},
|
|
645
|
+
"engines": {
|
|
646
|
+
"node": ">=12"
|
|
647
|
+
},
|
|
648
|
+
"funding": {
|
|
649
|
+
"type": "github",
|
|
650
|
+
"url": "https://github.com/sponsors/tannerlinsley"
|
|
651
|
+
},
|
|
652
|
+
"peerDependencies": {
|
|
653
|
+
"react": ">=16.8",
|
|
654
|
+
"react-dom": ">=16.8"
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
"node_modules/@tanstack/table-core": {
|
|
658
|
+
"version": "8.21.3",
|
|
659
|
+
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
|
|
660
|
+
"integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
|
|
661
|
+
"license": "MIT",
|
|
662
|
+
"engines": {
|
|
663
|
+
"node": ">=12"
|
|
664
|
+
},
|
|
665
|
+
"funding": {
|
|
666
|
+
"type": "github",
|
|
667
|
+
"url": "https://github.com/sponsors/tannerlinsley"
|
|
668
|
+
}
|
|
669
|
+
},
|
|
636
670
|
"node_modules/baseline-browser-mapping": {
|
|
637
671
|
"version": "2.10.21",
|
|
638
672
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz",
|