Skip to content

Commit 8af2598

Browse files
milstanclaude
andauthored
feat(components): headless artifact SDK + artifact-kit & team-activity tools (#114)
Add @leadbay/components: vanilla view-models (field/action/resource/list plus domain helpers like outreach/leadHistory/enrichment/teamActivity) for building interactive cowork HTML artifacts. The agent owns 100% of markup; the SDK owns the data lifecycle (populate-from-API, value/loading/error, validation, polling, the report_outreach verification + _triggered_by footguns). New always-on read tools: leadbay_artifact_kit (serves the runtime + usage guide) and leadbay_team_activity (per-rep activity KPIs, wraps /kpi/users + /kpi/trends). Build emits a minified runtime into core; a components:check drift guard fails CI if it goes stale. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b901ae7 commit 8af2598

32 files changed

Lines changed: 4147 additions & 12 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ docs/superpowers/
2121

2222
# Python bytecode cache for helper scripts.
2323
__pycache__/
24+
.gstack/

WORKFLOWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ The table is the human-readable index. The `yaml expected` + `yaml scenario` blo
4343
| 29 | **Audience build from dirty taxonomy (no-crash)** — "create a group for menuisiers, pergolas, vérandas" — `leadbay_adjust_audience` must tolerate a null-name sector-taxonomy row and ambiguous matches, returning a graceful ambiguous-sectors message rather than a TypeError (regression lock for the v0.17.3 sector-creation crash) | `leadbay_adjust_audience` | "Create a group for menuisiers, pergolas, vérandas" |
4444
| 30 | **Account status — silent on unreadable quota** — on an org whose `quota_status` 401s (no billing plan, `plan: null`), `leadbay_account_status` must answer user + org WITHOUT mentioning quota, an error, a 401, or telling the user to reconnect / re-authenticate (the token is valid — the same response read the user fine). Regression lock for the product#3761 401-hallucination | `leadbay_account_status` | "What account am I connected to?" |
4545
| 31 | **Account status — never volunteers the lens, name not id**`leadbay_account_status` must NOT mention the active lens unprompted; and if asked which lens is active, must answer with the lens NAME, never the raw numeric id (e.g. `40005`). Regression lock for the product#3761 lens-hygiene fix | `leadbay_account_status` | "What account am I connected to, and which lens is active?" |
46+
| 32 | **Build an interactive artifact** — "build me a call sheet / interactive lead board with a campaign dropdown, notes, statuses, likes per lead" — the agent fetches headless view-models + usage guide via `leadbay_artifact_kit`, then assembles a single-file HTML artifact whose `lb.field`/`lb.action` view-models POPULATE a dropdown from `leadbay_list_campaigns` and submit `leadbay_report_outreach` / `leadbay_add_leads_to_campaign` / `leadbay_like_lead` (carrying `verification` + `_triggered_by` where required); the artifact owns all rendering | `leadbay_artifact_kit` *(no dedicated prompt)* | "Build me an interactive call sheet for these leads." |
47+
| 33 | **Manager team-activity view** — "how is my team doing", "top performers this month", "activity by rep" — `leadbay_team_activity` returns a per-rep leaderboard (`reps`, sorted by `total_activities`) + an activity time-series (`trend`) for a look-back window, the data behind the web Dashboard-Manager screen. Feeds a manager artifact (`lb.teamActivity` → table + Chart.js); quota/remaining stays on `leadbay_account_status` | `leadbay_team_activity` *(no dedicated prompt)* | "How is my team doing this month?" |
4648

4749
---
4850

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
"prompts:build": "pnpm --filter @leadbay/promptforge build",
99
"prompts:check": "pnpm --filter @leadbay/promptforge check",
1010
"prompts:approve-drift": "pnpm --filter @leadbay/promptforge exec tsx src/cli.ts approve-drift",
11-
"build": "pnpm prompts:build && pnpm -r build",
11+
"components:build": "pnpm --filter @leadbay/components build",
12+
"components:check": "pnpm --filter @leadbay/components check",
13+
"build": "pnpm components:build && pnpm prompts:build && pnpm -r build",
1214
"typecheck": "pnpm -r typecheck",
13-
"test": "pnpm prompts:check && pnpm -r test",
15+
"test": "pnpm components:check && pnpm prompts:check && pnpm -r test",
1416
"test:smoke": "pnpm --filter @leadbay/core --filter @leadbay/mcp test:smoke",
1517
"test:eval": "EVAL=1 pnpm --filter @leadbay/mcp exec vitest run --config vitest.eval.config.ts",
1618
"test:gate": "EVAL=1 EVAL_TIER=gate pnpm --filter @leadbay/mcp exec vitest run --config vitest.eval.config.ts",

packages/components/package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "@leadbay/components",
3+
"version": "0.3.1",
4+
"private": true,
5+
"description": "Headless artifact SDK: vanilla view-models (field/action/resource/list + domain helpers) over window.cowork.callMcpTool. The agent owns 100% of markup, layout, and look.",
6+
"type": "module",
7+
"main": "src/runtime.ts",
8+
"scripts": {
9+
"build": "tsx src/build.ts",
10+
"check": "tsx src/build.ts --check",
11+
"typecheck": "tsc --noEmit",
12+
"test": "vitest run"
13+
},
14+
"devDependencies": {
15+
"esbuild": "^0.25.0",
16+
"jsdom": "^25.0.1",
17+
"tsx": "^4.19.2"
18+
},
19+
"license": "MIT"
20+
}

packages/components/src/build.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Build entry — bundles the runtime to a single inlinable IIFE string and emits
2+
// the committed generated module that @leadbay/core imports. Mirrors the
3+
// promptforge emit pattern (writeIfDifferent + AUTO-GENERATED header; the
4+
// generated file lives in src/, not dist/, so it is committed source).
5+
//
6+
// Run via `tsx src/build.ts` (the package's `build` script). @leadbay/core's
7+
// `prebuild` runs this before its tsc so the generated string is fresh.
8+
9+
import { build as esbuild } from "esbuild";
10+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
11+
import { dirname } from "node:path";
12+
import { fileURLToPath } from "node:url";
13+
import { VERSION } from "./runtime.js";
14+
15+
const RUNTIME_ENTRY = fileURLToPath(new URL("./runtime.ts", import.meta.url));
16+
const USAGE_GUIDE = fileURLToPath(new URL("./usage-guide.md", import.meta.url));
17+
const OUT = fileURLToPath(
18+
new URL("../../core/src/artifact-runtime.generated.ts", import.meta.url),
19+
);
20+
21+
const HEADER = `// AUTO-GENERATED by @leadbay/components — do not edit directly.
22+
// Regenerate via: pnpm --filter @leadbay/components build
23+
// Source-of-truth: packages/components/src/{runtime.ts, usage-guide.md}
24+
25+
`;
26+
27+
async function bundleRuntime(): Promise<string> {
28+
const result = await esbuild({
29+
entryPoints: [RUNTIME_ENTRY],
30+
bundle: true,
31+
minify: true,
32+
format: "iife",
33+
// No globalName: the runtime self-attaches `window.LeadbayArtifacts` from
34+
// inside its module body (see runtime.ts), which is robust across script VMs
35+
// that don't expose esbuild's top-level `var` (jsdom, wrapped <script>).
36+
target: ["es2020"],
37+
legalComments: "none",
38+
write: false,
39+
});
40+
return result.outputFiles[0].text.trim();
41+
}
42+
43+
function writeIfDifferent(path: string, content: string): boolean {
44+
mkdirSync(dirname(path), { recursive: true });
45+
if (existsSync(path) && readFileSync(path, "utf8") === content) return false;
46+
writeFileSync(path, content, "utf8");
47+
return true;
48+
}
49+
50+
async function main(): Promise<void> {
51+
const runtime = await bundleRuntime();
52+
const usageGuide = readFileSync(USAGE_GUIDE, "utf8").trimEnd();
53+
54+
// JSON.stringify gives a safely-escaped TS string literal — no backtick or
55+
// `${` hazards from the minified runtime or the markdown.
56+
const module =
57+
HEADER +
58+
`export const ARTIFACT_KIT_VERSION = ${JSON.stringify(VERSION)};\n\n` +
59+
`/** Minified, self-contained IIFE. Inline once per artifact; it self-attaches\n` +
60+
` * window.LeadbayArtifacts (lb.field / lb.action / lb.resource / lb.list / …).\n` +
61+
` * Zero dependencies. */\n` +
62+
`export const ARTIFACT_RUNTIME: string = ${JSON.stringify(runtime)};\n\n` +
63+
`/** Markdown assembly recipe the agent reads to build an artifact. */\n` +
64+
`export const ARTIFACT_USAGE_GUIDE: string = ${JSON.stringify(usageGuide)};\n`;
65+
66+
const rel = "packages/core/src/artifact-runtime.generated.ts";
67+
68+
// Drift guard: `build --check` verifies the committed generated module matches
69+
// a fresh build, and FAILS otherwise — so editing runtime.ts/usage-guide.md
70+
// without re-running the build can't silently ship a stale runtime/guide.
71+
if (process.argv.includes("--check")) {
72+
const current = existsSync(OUT) ? readFileSync(OUT, "utf8") : null;
73+
if (current !== module) {
74+
console.error(
75+
`[components] DRIFT: ${rel} is stale. Run \`pnpm --filter @leadbay/components build\` and commit the result.`,
76+
);
77+
process.exit(1);
78+
}
79+
console.log(`[components] ${rel}: up to date`);
80+
return;
81+
}
82+
83+
const changed = writeIfDifferent(OUT, module);
84+
console.log(
85+
`[components] ${rel}: ${changed ? "wrote" : "unchanged"} ` +
86+
`(runtime ${runtime.length}B, guide ${usageGuide.length}B)`,
87+
);
88+
}
89+
90+
main().catch((err) => {
91+
console.error("[components] build failed:", err);
92+
process.exit(1);
93+
});

0 commit comments

Comments
 (0)