AryaYT commited on
Commit
71c7323
·
0 Parent(s):

init: meeseeks bun docker space (server, ui, samples, receipts)

Browse files
Files changed (35) hide show
  1. .gitignore +3 -0
  2. Dockerfile +17 -0
  3. README.md +31 -0
  4. package.json +14 -0
  5. receipts/discovery-objection/01-open-doc-after.png +0 -0
  6. receipts/discovery-objection/01-open-doc-before.png +0 -0
  7. receipts/discovery-objection/02-type-caption-after.png +0 -0
  8. receipts/discovery-objection/02-type-caption-before.png +0 -0
  9. receipts/discovery-objection/03-draft-objection-after.png +0 -0
  10. receipts/discovery-objection/03-draft-objection-before.png +0 -0
  11. receipts/discovery-objection/04-save-draft-after.png +0 -0
  12. receipts/discovery-objection/04-save-draft-before.png +0 -0
  13. receipts/discovery-objection/receipt.json +53 -0
  14. receipts/meet-and-confer/01-open-letter-template-after.png +0 -0
  15. receipts/meet-and-confer/01-open-letter-template-before.png +0 -0
  16. receipts/meet-and-confer/02-fill-caption-after.png +0 -0
  17. receipts/meet-and-confer/02-fill-caption-before.png +0 -0
  18. receipts/meet-and-confer/03-fill-recipient-after.png +0 -0
  19. receipts/meet-and-confer/03-fill-recipient-before.png +0 -0
  20. receipts/meet-and-confer/04-summarize-dispute-after.png +0 -0
  21. receipts/meet-and-confer/04-summarize-dispute-before.png +0 -0
  22. receipts/meet-and-confer/05-propose-dates-after.png +0 -0
  23. receipts/meet-and-confer/05-propose-dates-before.png +0 -0
  24. receipts/meet-and-confer/06-save-and-route-after.png +0 -0
  25. receipts/meet-and-confer/06-save-and-route-before.png +0 -0
  26. receipts/meet-and-confer/receipt.json +77 -0
  27. samples/draft-discovery-objection.json +68 -0
  28. samples/draft-meet-and-confer-letter.json +98 -0
  29. src/lib/exporter.ts +95 -0
  30. src/lib/plan.ts +85 -0
  31. src/lib/skill.ts +49 -0
  32. src/lib/substitute.ts +50 -0
  33. src/server.ts +191 -0
  34. src/static/index.html +530 -0
  35. tsconfig.json +19 -0
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ node_modules/
2
+ .DS_Store
3
+ *.log
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Meeseeks HF Space — Bun runtime, port 7860 (HF default).
2
+ FROM oven/bun:1.3-alpine
3
+
4
+ WORKDIR /app
5
+
6
+ COPY package.json bun.lock* ./
7
+ RUN bun install --production --frozen-lockfile 2>/dev/null || bun install --production
8
+
9
+ COPY src ./src
10
+ COPY samples ./samples
11
+ COPY receipts ./receipts
12
+ COPY tsconfig.json ./
13
+
14
+ ENV PORT=7860
15
+ EXPOSE 7860
16
+
17
+ CMD ["bun", "run", "src/server.ts"]
README.md ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Meeseeks
3
+ emoji: ⚖️
4
+ colorFrom: indigo
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: true
9
+ license: mit
10
+ short_description: Agent-agnostic skill workbench — capture, redact, run anywhere
11
+ ---
12
+
13
+ # Meeseeks
14
+
15
+ Meeseeks turns repeatable computer workflows into structured, **agent-agnostic** skills. The same JSON skill description drops into Claude, GPT, Gemini, or any local model that speaks the open Anthropic Agent Skills format — no translation layer.
16
+
17
+ This Space is a live demo of the authoring + grounding pipeline:
18
+
19
+ - **Author** — hand-edit a skill JSON, see the placeholder plan substitute live
20
+ - **Bundle** — derive `SKILL.md`, `manifest.json`, and `actions.toon` (token-oriented action format) from one source of truth
21
+ - **Receipts** — see the executor's screenshot trail when the skill walks through a real DOM (Playwright + accessibility-tree grounding)
22
+
23
+ The full project (CLI, runtime, mock paralegal target, sample skills) lives at the root of the repo.
24
+
25
+ ## Why this matters
26
+
27
+ Today, every agent platform reinvents skill capture: ChatGPT custom GPTs, Claude Skills, GLaDOS plugins, OpenAI Operator macros — all incompatible. Meeseeks is the smallest possible spec that keeps a skill portable across all of them while preserving the grounding metadata (bbox, accessibility role/name, success checks, fallbacks) that makes a skill safe to run.
28
+
29
+ ## Domain
30
+
31
+ The current sample set is paralegal — discovery objections, meet-and-confer letters — because it's a high-value workflow domain where redacted templates + verified grounding genuinely save hours. The same shape applies to healthcare intake, financial back-office, and code-review workflows.
package.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "meeseeks-hf-space",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "start": "bun run src/server.ts",
8
+ "dev": "bun --watch run src/server.ts"
9
+ },
10
+ "dependencies": {
11
+ "yaml": "^2.5.0",
12
+ "zod": "^4.4.3"
13
+ }
14
+ }
receipts/discovery-objection/01-open-doc-after.png ADDED
receipts/discovery-objection/01-open-doc-before.png ADDED
receipts/discovery-objection/02-type-caption-after.png ADDED
receipts/discovery-objection/02-type-caption-before.png ADDED
receipts/discovery-objection/03-draft-objection-after.png ADDED
receipts/discovery-objection/03-draft-objection-before.png ADDED
receipts/discovery-objection/04-save-draft-after.png ADDED
receipts/discovery-objection/04-save-draft-before.png ADDED
receipts/discovery-objection/receipt.json ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "skill_name": "draft-discovery-objection",
3
+ "url": "http://127.0.0.1:8787/",
4
+ "started_at": "2026-05-09T09:14:27.217Z",
5
+ "finished_at": "2026-05-09T09:14:30.351Z",
6
+ "total_ms": 3134,
7
+ "unbound_placeholders": [],
8
+ "steps": [
9
+ {
10
+ "id": "open-doc",
11
+ "action": "click",
12
+ "ok": true,
13
+ "resolved_via": "ax_role+ax_name",
14
+ "durationMs": 608,
15
+ "screenshot_before": "receipts/discovery-objection-demo/01-open-doc-before.png",
16
+ "screenshot_after": "receipts/discovery-objection-demo/01-open-doc-after.png"
17
+ },
18
+ {
19
+ "id": "type-caption",
20
+ "action": "type",
21
+ "ok": true,
22
+ "resolved_via": "ax_role+ax_name",
23
+ "durationMs": 72,
24
+ "screenshot_before": "receipts/discovery-objection-demo/02-type-caption-before.png",
25
+ "screenshot_after": "receipts/discovery-objection-demo/02-type-caption-after.png",
26
+ "notes": [
27
+ "would type: \"Type the case caption Doe v. Acme Corp., No. 24-cv-123 at the top of the document\""
28
+ ]
29
+ },
30
+ {
31
+ "id": "draft-objection",
32
+ "action": "type",
33
+ "ok": true,
34
+ "resolved_via": "ax_role+ax_name",
35
+ "durationMs": 63,
36
+ "screenshot_before": "receipts/discovery-objection-demo/03-draft-objection-before.png",
37
+ "screenshot_after": "receipts/discovery-objection-demo/03-draft-objection-after.png",
38
+ "notes": [
39
+ "would type: \"Draft an objection citing FRCP 26(b)(1) noting that request 7 is over-broad and unduly burdensome relative to the proportional needs of the case\""
40
+ ]
41
+ },
42
+ {
43
+ "id": "save-draft",
44
+ "action": "click",
45
+ "ok": true,
46
+ "resolved_via": "ax_role+ax_name",
47
+ "durationMs": 98,
48
+ "screenshot_before": "receipts/discovery-objection-demo/04-save-draft-before.png",
49
+ "screenshot_after": "receipts/discovery-objection-demo/04-save-draft-after.png"
50
+ }
51
+ ],
52
+ "ok": true
53
+ }
receipts/meet-and-confer/01-open-letter-template-after.png ADDED
receipts/meet-and-confer/01-open-letter-template-before.png ADDED
receipts/meet-and-confer/02-fill-caption-after.png ADDED
receipts/meet-and-confer/02-fill-caption-before.png ADDED
receipts/meet-and-confer/03-fill-recipient-after.png ADDED
receipts/meet-and-confer/03-fill-recipient-before.png ADDED
receipts/meet-and-confer/04-summarize-dispute-after.png ADDED
receipts/meet-and-confer/04-summarize-dispute-before.png ADDED
receipts/meet-and-confer/05-propose-dates-after.png ADDED
receipts/meet-and-confer/05-propose-dates-before.png ADDED
receipts/meet-and-confer/06-save-and-route-after.png ADDED
receipts/meet-and-confer/06-save-and-route-before.png ADDED
receipts/meet-and-confer/receipt.json ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "skill_name": "draft-meet-and-confer-letter",
3
+ "url": "http://127.0.0.1:8787/",
4
+ "started_at": "2026-05-09T09:14:53.006Z",
5
+ "finished_at": "2026-05-09T09:14:54.532Z",
6
+ "total_ms": 1526,
7
+ "unbound_placeholders": [],
8
+ "steps": [
9
+ {
10
+ "id": "open-letter-template",
11
+ "action": "click",
12
+ "ok": true,
13
+ "resolved_via": "ax_role+ax_name",
14
+ "durationMs": 272,
15
+ "screenshot_before": "receipts/meet-and-confer-demo/01-open-letter-template-before.png",
16
+ "screenshot_after": "receipts/meet-and-confer-demo/01-open-letter-template-after.png"
17
+ },
18
+ {
19
+ "id": "fill-caption",
20
+ "action": "type",
21
+ "ok": true,
22
+ "resolved_via": "ax_role",
23
+ "durationMs": 71,
24
+ "screenshot_before": "receipts/meet-and-confer-demo/02-fill-caption-before.png",
25
+ "screenshot_after": "receipts/meet-and-confer-demo/02-fill-caption-after.png",
26
+ "notes": [
27
+ "would type: \"Insert the case caption Doe v. Acme Corp., No. 24-cv-123 into the letter header\""
28
+ ]
29
+ },
30
+ {
31
+ "id": "fill-recipient",
32
+ "action": "type",
33
+ "ok": true,
34
+ "resolved_via": "ax_role",
35
+ "durationMs": 65,
36
+ "screenshot_before": "receipts/meet-and-confer-demo/03-fill-recipient-before.png",
37
+ "screenshot_after": "receipts/meet-and-confer-demo/03-fill-recipient-after.png",
38
+ "notes": [
39
+ "would type: \"Address the letter to Jane Smith at jsmith@opposing.law\""
40
+ ]
41
+ },
42
+ {
43
+ "id": "summarize-dispute",
44
+ "action": "type",
45
+ "ok": true,
46
+ "resolved_via": "ax_role",
47
+ "durationMs": 77,
48
+ "screenshot_before": "receipts/meet-and-confer-demo/04-summarize-dispute-before.png",
49
+ "screenshot_after": "receipts/meet-and-confer-demo/04-summarize-dispute-after.png",
50
+ "notes": [
51
+ "would type: \"Type a one-paragraph statement of the discovery dispute citing Local Rule 37-1 and reference the specific request numbers in Defendant's responses to Requests 1-7 were uniformly evasive and non-responsive\""
52
+ ]
53
+ },
54
+ {
55
+ "id": "propose-dates",
56
+ "action": "type",
57
+ "ok": true,
58
+ "resolved_via": "ax_role",
59
+ "durationMs": 81,
60
+ "screenshot_before": "receipts/meet-and-confer-demo/05-propose-dates-before.png",
61
+ "screenshot_after": "receipts/meet-and-confer-demo/05-propose-dates-after.png",
62
+ "notes": [
63
+ "would type: \"Propose three meet-and-confer dates from May 14, May 16, May 19 in the closing paragraph and request a confirmation by reply\""
64
+ ]
65
+ },
66
+ {
67
+ "id": "save-and-route",
68
+ "action": "click",
69
+ "ok": true,
70
+ "resolved_via": "ax_role+ax_name",
71
+ "durationMs": 104,
72
+ "screenshot_before": "receipts/meet-and-confer-demo/06-save-and-route-before.png",
73
+ "screenshot_after": "receipts/meet-and-confer-demo/06-save-and-route-after.png"
74
+ }
75
+ ],
76
+ "ok": true
77
+ }
samples/draft-discovery-objection.json ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schemaVersion": "0.1",
3
+ "name": "draft-discovery-objection",
4
+ "intent": "Draft an objection to an over-broad discovery request citing FRCP 26(b)(1)",
5
+ "inputs": ["request_text", "case_caption", "request_num"],
6
+ "preconditions": [
7
+ "User has the discovery request text loaded",
8
+ "User has the case caption with court and case number"
9
+ ],
10
+ "steps": [
11
+ {
12
+ "id": "open-doc",
13
+ "instruction": "Click 'New Document' in the case management toolbar to start the objection",
14
+ "action": "click",
15
+ "target": {
16
+ "description": "New Document button in the case management toolbar",
17
+ "bbox": [120, 80, 240, 110],
18
+ "ax_role": "button",
19
+ "ax_name": "New Document"
20
+ },
21
+ "successCheck": "An empty document editor pane is visible and focused",
22
+ "fallback": "If the toolbar button is hidden, open File menu and select New Document"
23
+ },
24
+ {
25
+ "id": "type-caption",
26
+ "instruction": "Type the case caption {{CASE_CAPTION}} at the top of the document",
27
+ "action": "type",
28
+ "target": {
29
+ "description": "Document body — caption area at top of page 1",
30
+ "ax_role": "textbox",
31
+ "ax_name": "Document body"
32
+ },
33
+ "successCheck": "The caption {{CASE_CAPTION}} is rendered at top of document",
34
+ "fallback": "Use the firm's saved caption template if one exists for this court"
35
+ },
36
+ {
37
+ "id": "draft-objection",
38
+ "instruction": "Draft an objection citing FRCP 26(b)(1) noting that request {{REQUEST_NUM}} is over-broad and unduly burdensome relative to the proportional needs of the case",
39
+ "action": "type",
40
+ "target": {
41
+ "description": "Document body — objection paragraph area",
42
+ "bbox": [40, 280, 760, 600],
43
+ "ax_role": "textbox",
44
+ "ax_name": "Document body"
45
+ },
46
+ "successCheck": "Objection paragraph references FRCP 26(b)(1) and request number {{REQUEST_NUM}}"
47
+ },
48
+ {
49
+ "id": "save-draft",
50
+ "instruction": "Save the draft objection to the case folder for attorney review",
51
+ "action": "click",
52
+ "target": {
53
+ "description": "Save button in the document toolbar",
54
+ "bbox": [620, 80, 700, 110],
55
+ "ax_role": "button",
56
+ "ax_name": "Save"
57
+ },
58
+ "successCheck": "Document is saved with a timestamp and appears in the case folder",
59
+ "fallback": "Use Cmd+S keyboard shortcut if the Save button is unresponsive"
60
+ }
61
+ ],
62
+ "meta": {
63
+ "domain": "paralegal",
64
+ "redacted": true,
65
+ "placeholders": ["{{CASE_CAPTION}}", "{{REQUEST_NUM}}"]
66
+ },
67
+ "extensions": {}
68
+ }
samples/draft-meet-and-confer-letter.json ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schemaVersion": "0.1",
3
+ "name": "draft-meet-and-confer-letter",
4
+ "intent": "Draft a Local Rule 37-1 meet-and-confer letter to opposing counsel proposing dates to resolve a discovery dispute before motion practice",
5
+ "inputs": ["case_caption", "opposing_counsel_name", "opposing_counsel_email", "dispute_summary", "proposed_dates"],
6
+ "preconditions": [
7
+ "User has the case caption with court and case number",
8
+ "User has identified opposing counsel for the disputed request",
9
+ "User has a one-paragraph summary of the dispute (which requests, the asserted deficiency)"
10
+ ],
11
+ "steps": [
12
+ {
13
+ "id": "open-letter-template",
14
+ "instruction": "Open the firm's meet-and-confer letter template from the templates pane",
15
+ "action": "click",
16
+ "target": {
17
+ "description": "Templates pane — Meet-and-Confer Letter entry",
18
+ "bbox": [60, 240, 280, 268],
19
+ "ax_role": "menuitem",
20
+ "ax_name": "Meet-and-Confer Letter"
21
+ },
22
+ "successCheck": "Letter template opens with a header block ready for caption insertion",
23
+ "fallback": "If the template pane is collapsed, expand the Templates sidebar from the View menu"
24
+ },
25
+ {
26
+ "id": "fill-caption",
27
+ "instruction": "Insert the case caption {{CASE_CAPTION}} into the letter header",
28
+ "action": "type",
29
+ "target": {
30
+ "description": "Letter header — caption insertion zone above the salutation",
31
+ "ax_role": "textbox",
32
+ "ax_name": "Letter header"
33
+ },
34
+ "successCheck": "Caption {{CASE_CAPTION}} appears in the header on page 1",
35
+ "fallback": "If autocomplete suggests an outdated caption, override by clicking 'Use as typed'"
36
+ },
37
+ {
38
+ "id": "fill-recipient",
39
+ "instruction": "Address the letter to {{OPPOSING_COUNSEL_NAME}} at {{OPPOSING_COUNSEL_EMAIL}}",
40
+ "action": "type",
41
+ "target": {
42
+ "description": "Recipient block — Counsel Name and Counsel Email fields",
43
+ "ax_role": "textbox",
44
+ "ax_name": "Recipient block"
45
+ },
46
+ "successCheck": "Recipient block shows {{OPPOSING_COUNSEL_NAME}} on the name line and {{OPPOSING_COUNSEL_EMAIL}} on the email line"
47
+ },
48
+ {
49
+ "id": "summarize-dispute",
50
+ "instruction": "Type a one-paragraph statement of the discovery dispute citing Local Rule 37-1 and reference the specific request numbers in {{DISPUTE_SUMMARY}}",
51
+ "action": "type",
52
+ "target": {
53
+ "description": "Letter body — first paragraph after the salutation",
54
+ "bbox": [80, 360, 720, 520],
55
+ "ax_role": "textbox",
56
+ "ax_name": "Letter body"
57
+ },
58
+ "successCheck": "Letter body paragraph references Local Rule 37-1 and the dispute summary text {{DISPUTE_SUMMARY}}",
59
+ "fallback": "If the firm uses a different local rule (e.g. Local Rule 26-7), substitute that citation per the case's venue"
60
+ },
61
+ {
62
+ "id": "propose-dates",
63
+ "instruction": "Propose three meet-and-confer dates from {{PROPOSED_DATES}} in the closing paragraph and request a confirmation by reply",
64
+ "action": "type",
65
+ "target": {
66
+ "description": "Letter body — closing paragraph above the signature block",
67
+ "ax_role": "textbox",
68
+ "ax_name": "Letter body"
69
+ },
70
+ "successCheck": "Closing paragraph lists three distinct dates from {{PROPOSED_DATES}} and asks for confirmation"
71
+ },
72
+ {
73
+ "id": "save-and-route",
74
+ "instruction": "Save the draft to the case folder and route it to the supervising attorney for review before sending",
75
+ "action": "click",
76
+ "target": {
77
+ "description": "Save and Route button in the document toolbar",
78
+ "bbox": [620, 80, 760, 110],
79
+ "ax_role": "button",
80
+ "ax_name": "Save and Route"
81
+ },
82
+ "successCheck": "Document is saved with a timestamp and the supervising attorney is added to the routing queue",
83
+ "fallback": "If Save and Route is unavailable, save with Cmd+S then add the attorney from the routing dropdown"
84
+ }
85
+ ],
86
+ "meta": {
87
+ "domain": "paralegal",
88
+ "redacted": true,
89
+ "placeholders": [
90
+ "{{CASE_CAPTION}}",
91
+ "{{OPPOSING_COUNSEL_NAME}}",
92
+ "{{OPPOSING_COUNSEL_EMAIL}}",
93
+ "{{DISPUTE_SUMMARY}}",
94
+ "{{PROPOSED_DATES}}"
95
+ ]
96
+ },
97
+ "extensions": {}
98
+ }
src/lib/exporter.ts ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Vendored from packages/exporter/src/exporter.ts.
2
+ // Sole change: import Skill/Step from local ./skill.ts.
3
+
4
+ import { stringify as yamlStringify } from "yaml";
5
+ import type { Skill, Step } from "./skill.ts";
6
+
7
+ export interface SkillBundle {
8
+ skill_md: string;
9
+ manifest_json: string;
10
+ actions_toon: string;
11
+ }
12
+
13
+ export const TOON_HEADER_ACTIONS = "@meeseeks/v0.1 actions";
14
+
15
+ export function exportSkill(skill: Skill): SkillBundle {
16
+ return {
17
+ skill_md: renderSkillMd(skill),
18
+ manifest_json: renderManifestJson(skill),
19
+ actions_toon: renderActionsToon(skill),
20
+ };
21
+ }
22
+
23
+ function renderSkillMd(s: Skill): string {
24
+ const params = s.inputs.map((i) => ` - name: ${i}\n required: true`).join("\n");
25
+ const preconds = s.preconditions.map((p) => `- ${p}`).join("\n");
26
+ const steps = s.steps.map((st, i) => `${i + 1}. ${st.instruction}`).join("\n");
27
+ const verify = s.steps.map((st) => `- ${st.successCheck}`).join("\n");
28
+ return [
29
+ "---",
30
+ `name: ${s.name}`,
31
+ `description: ${s.intent}`,
32
+ `domain: ${s.meta.domain}`,
33
+ `version: ${s.schemaVersion}`,
34
+ "parameters:",
35
+ params,
36
+ "---",
37
+ "",
38
+ "# When to use",
39
+ s.intent,
40
+ "",
41
+ "# Preconditions",
42
+ preconds,
43
+ "",
44
+ "# Steps",
45
+ steps,
46
+ "",
47
+ "# Verification",
48
+ verify,
49
+ "",
50
+ ].join("\n");
51
+ }
52
+
53
+ function renderManifestJson(s: Skill): string {
54
+ const grounding = s.steps.map((st) => ({
55
+ step_id: st.id,
56
+ description: st.target.description,
57
+ bbox: st.target.bbox ?? null,
58
+ ax_role: st.target.ax_role ?? null,
59
+ ax_name: st.target.ax_name ?? null,
60
+ success_check: st.successCheck,
61
+ fallback: st.fallback ?? null,
62
+ }));
63
+ const manifest = {
64
+ schema: "meeseeks/v0.1",
65
+ name: s.name,
66
+ description: s.intent,
67
+ domain: s.meta.domain,
68
+ parameters: s.inputs,
69
+ preconditions: s.preconditions,
70
+ redacted: s.meta.redacted,
71
+ placeholders: s.meta.placeholders,
72
+ meeseeks: { grounding },
73
+ extensions: s.extensions ?? {},
74
+ };
75
+ return JSON.stringify(manifest, null, 2);
76
+ }
77
+
78
+ function renderActionsToon(s: Skill): string {
79
+ const body = {
80
+ name: s.name,
81
+ intent: s.intent,
82
+ schema_version: s.schemaVersion,
83
+ steps: s.steps.map((st) => stepToToon(st)),
84
+ };
85
+ return TOON_HEADER_ACTIONS + "\n" + yamlStringify(body);
86
+ }
87
+
88
+ function stepToToon(st: Step) {
89
+ return {
90
+ id: st.id,
91
+ action: st.action,
92
+ instruction: st.instruction,
93
+ target_description: st.target.description,
94
+ };
95
+ }
src/lib/plan.ts ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Vendored from packages/runtime/src/plan.ts.
2
+ // Sole change: import Skill/Action from local ./skill.ts instead of @meeseeks/schema.
3
+
4
+ import type { Action, Skill } from "./skill.ts";
5
+ import { findPlaceholders, substitute, substituteWithReport } from "./substitute.ts";
6
+
7
+ export interface StepPlan {
8
+ id: string;
9
+ action: Action;
10
+ instruction: string;
11
+ target: {
12
+ description: string;
13
+ bbox?: [number, number, number, number];
14
+ ax_role?: string;
15
+ ax_name?: string;
16
+ };
17
+ successCheck: string;
18
+ fallback?: string;
19
+ }
20
+
21
+ export interface ExecutionPlan {
22
+ skill_name: string;
23
+ intent: string;
24
+ domain: string;
25
+ inputs: Record<string, string>;
26
+ steps: StepPlan[];
27
+ unbound_placeholders: string[];
28
+ }
29
+
30
+ export function planSkill(
31
+ skill: Skill,
32
+ inputs: Record<string, string> = {},
33
+ ): ExecutionPlan {
34
+ const unbound = new Set<string>();
35
+ const collect = (s: string) => {
36
+ for (const ph of findPlaceholders(s)) {
37
+ const key = ph.slice(2, -2);
38
+ if (inputs[key] === undefined && inputs[key.toLowerCase()] === undefined) {
39
+ unbound.add(ph);
40
+ }
41
+ }
42
+ };
43
+
44
+ for (const ph of skill.meta.placeholders) {
45
+ const key = ph.slice(2, -2);
46
+ if (inputs[key] === undefined && inputs[key.toLowerCase()] === undefined) {
47
+ unbound.add(ph);
48
+ }
49
+ }
50
+
51
+ const steps: StepPlan[] = skill.steps.map((s) => {
52
+ collect(s.instruction);
53
+ collect(s.target.description);
54
+ collect(s.successCheck);
55
+ if (s.fallback) collect(s.fallback);
56
+
57
+ const target: StepPlan["target"] = {
58
+ description: substitute(s.target.description, inputs),
59
+ };
60
+ if (s.target.bbox) target.bbox = s.target.bbox;
61
+ if (s.target.ax_role) target.ax_role = s.target.ax_role;
62
+ if (s.target.ax_name) target.ax_name = s.target.ax_name;
63
+
64
+ const step: StepPlan = {
65
+ id: s.id,
66
+ action: s.action,
67
+ instruction: substitute(s.instruction, inputs),
68
+ target,
69
+ successCheck: substitute(s.successCheck, inputs),
70
+ };
71
+ if (s.fallback) step.fallback = substitute(s.fallback, inputs);
72
+ return step;
73
+ });
74
+
75
+ return {
76
+ skill_name: skill.name,
77
+ intent: substitute(skill.intent, inputs),
78
+ domain: skill.meta.domain,
79
+ inputs,
80
+ steps,
81
+ unbound_placeholders: [...unbound].sort(),
82
+ };
83
+ }
84
+
85
+ export { substitute, substituteWithReport, findPlaceholders };
src/lib/skill.ts ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Sarah-JSON authoring schema for Meeseeks skills.
2
+ // Frozen at "SkillCore + extensions envelope" (plan v3 § 15.3 C1).
3
+ // This is the canonical authoring shape; the export bundle (SKILL.md +
4
+ // manifest.json + actions.toon) is derived from it.
5
+
6
+ import { z } from "zod";
7
+
8
+ export const SKILL_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
9
+
10
+ export const ACTIONS = ["click", "type", "read", "wait", "verify"] as const;
11
+ export type Action = (typeof ACTIONS)[number];
12
+
13
+ export const SkillTarget = z.object({
14
+ description: z.string().min(1),
15
+ bbox: z.tuple([z.number(), z.number(), z.number(), z.number()]).optional(),
16
+ ax_role: z.string().optional(),
17
+ ax_name: z.string().optional(),
18
+ });
19
+
20
+ export const SkillStep = z.object({
21
+ id: z.string().min(1),
22
+ instruction: z.string().min(1),
23
+ action: z.enum(ACTIONS),
24
+ target: SkillTarget,
25
+ successCheck: z.string().min(1),
26
+ fallback: z.string().optional(),
27
+ });
28
+
29
+ export const SkillMeta = z.object({
30
+ domain: z.string().min(1),
31
+ redacted: z.literal(true),
32
+ placeholders: z.array(z.string()),
33
+ });
34
+
35
+ export const SkillCore = z.object({
36
+ schemaVersion: z.literal("0.1"),
37
+ name: z.string().regex(SKILL_NAME_PATTERN),
38
+ intent: z.string().min(1),
39
+ inputs: z.array(z.string()),
40
+ preconditions: z.array(z.string()),
41
+ steps: z.array(SkillStep).min(1),
42
+ meta: SkillMeta,
43
+ extensions: z.record(z.string(), z.unknown()).default({}),
44
+ });
45
+
46
+ export type Skill = z.infer<typeof SkillCore>;
47
+ export type Step = z.infer<typeof SkillStep>;
48
+ export type Target = z.infer<typeof SkillTarget>;
49
+ export type Meta = z.infer<typeof SkillMeta>;
src/lib/substitute.ts ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Placeholder substitution for skill text.
2
+ // Convention: placeholders look like {{KEY}} (uppercase + underscores).
3
+ // Inputs may be passed as either {KEY: value} or {key: value} — both resolve.
4
+
5
+ export interface SubstituteResult {
6
+ text: string;
7
+ bound: string[];
8
+ unbound: string[];
9
+ }
10
+
11
+ const PLACEHOLDER_RE = /\{\{([A-Z][A-Z0-9_]*)\}\}/g;
12
+
13
+ export function substitute(text: string, inputs: Record<string, string>): string {
14
+ return text.replace(PLACEHOLDER_RE, (match, key: string) => {
15
+ if (inputs[key] !== undefined) return inputs[key];
16
+ const lc = key.toLowerCase();
17
+ if (inputs[lc] !== undefined) return inputs[lc];
18
+ return match;
19
+ });
20
+ }
21
+
22
+ export function substituteWithReport(
23
+ text: string,
24
+ inputs: Record<string, string>,
25
+ ): SubstituteResult {
26
+ const bound = new Set<string>();
27
+ const unbound = new Set<string>();
28
+ const out = text.replace(PLACEHOLDER_RE, (match, key: string) => {
29
+ if (inputs[key] !== undefined) {
30
+ bound.add(`{{${key}}}`);
31
+ return inputs[key];
32
+ }
33
+ const lc = key.toLowerCase();
34
+ if (inputs[lc] !== undefined) {
35
+ bound.add(`{{${key}}}`);
36
+ return inputs[lc];
37
+ }
38
+ unbound.add(`{{${key}}}`);
39
+ return match;
40
+ });
41
+ return { text: out, bound: [...bound], unbound: [...unbound] };
42
+ }
43
+
44
+ export function findPlaceholders(text: string): string[] {
45
+ const found = new Set<string>();
46
+ for (const m of text.matchAll(PLACEHOLDER_RE)) {
47
+ found.add(`{{${m[1]}}}`);
48
+ }
49
+ return [...found];
50
+ }
src/server.ts ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Meeseeks HF Space — Bun.serve HTTP server.
2
+ // Routes:
3
+ // GET / → static index.html
4
+ // GET /static/<file> → static assets (css/js/images)
5
+ // GET /samples → list sample skill names
6
+ // GET /samples/<name>.json → sample skill JSON
7
+ // POST /api/plan → {skill, inputs} → ExecutionPlan
8
+ // POST /api/export → {skill} → SkillBundle
9
+ // POST /api/validate → {skill} → {ok, issues}
10
+ // GET /receipts/<demo>/ → list receipt files
11
+ // GET /receipts/<demo>/<file> → receipt assets
12
+ //
13
+ // HF Spaces convention: bind to port 7860.
14
+
15
+ import { readdir } from "node:fs/promises";
16
+ import { extname, join, normalize, resolve } from "node:path";
17
+ import { SkillCore } from "./lib/skill.ts";
18
+ import { planSkill } from "./lib/plan.ts";
19
+ import { exportSkill } from "./lib/exporter.ts";
20
+
21
+ const PORT = Number(process.env.PORT ?? 7860);
22
+ const ROOT = resolve(import.meta.dir, "..");
23
+ const STATIC_DIR = join(ROOT, "src/static");
24
+ const SAMPLES_DIR = join(ROOT, "samples");
25
+ const RECEIPTS_DIR = join(ROOT, "receipts");
26
+
27
+ const MIME: Record<string, string> = {
28
+ ".html": "text/html; charset=utf-8",
29
+ ".css": "text/css; charset=utf-8",
30
+ ".js": "application/javascript; charset=utf-8",
31
+ ".json": "application/json; charset=utf-8",
32
+ ".png": "image/png",
33
+ ".svg": "image/svg+xml",
34
+ ".webp": "image/webp",
35
+ ".ico": "image/x-icon",
36
+ };
37
+
38
+ function mimeFor(path: string): string {
39
+ return MIME[extname(path).toLowerCase()] ?? "application/octet-stream";
40
+ }
41
+
42
+ function safeJoin(base: string, rel: string): string | null {
43
+ const target = normalize(join(base, rel));
44
+ if (!target.startsWith(base)) return null;
45
+ return target;
46
+ }
47
+
48
+ async function serveFile(absPath: string): Promise<Response> {
49
+ const file = Bun.file(absPath);
50
+ if (!(await file.exists())) {
51
+ return new Response("not found", { status: 404 });
52
+ }
53
+ return new Response(file, { headers: { "Content-Type": mimeFor(absPath) } });
54
+ }
55
+
56
+ async function readJsonBody(req: Request): Promise<any> {
57
+ const text = await req.text();
58
+ if (!text) return {};
59
+ try {
60
+ return JSON.parse(text);
61
+ } catch (e) {
62
+ throw new Response(JSON.stringify({ error: "invalid JSON body", detail: (e as Error).message }), {
63
+ status: 400,
64
+ headers: { "Content-Type": "application/json" },
65
+ });
66
+ }
67
+ }
68
+
69
+ function jsonResponse(data: unknown, init: ResponseInit = {}): Response {
70
+ return new Response(JSON.stringify(data, null, 2), {
71
+ ...init,
72
+ headers: { "Content-Type": "application/json", ...(init.headers ?? {}) },
73
+ });
74
+ }
75
+
76
+ async function listSamples(): Promise<{ name: string; bytes: number }[]> {
77
+ try {
78
+ const entries = await readdir(SAMPLES_DIR);
79
+ const out: { name: string; bytes: number }[] = [];
80
+ for (const e of entries.filter((x) => x.endsWith(".json")).sort()) {
81
+ const file = Bun.file(join(SAMPLES_DIR, e));
82
+ out.push({ name: e.replace(/\.json$/, ""), bytes: file.size });
83
+ }
84
+ return out;
85
+ } catch {
86
+ return [];
87
+ }
88
+ }
89
+
90
+ async function listReceiptFiles(demo: string): Promise<string[]> {
91
+ const dir = safeJoin(RECEIPTS_DIR, demo);
92
+ if (!dir) return [];
93
+ try {
94
+ const entries = await readdir(dir);
95
+ return entries.sort();
96
+ } catch {
97
+ return [];
98
+ }
99
+ }
100
+
101
+ const server = Bun.serve({
102
+ port: PORT,
103
+ hostname: "0.0.0.0",
104
+ async fetch(req) {
105
+ const url = new URL(req.url);
106
+ const path = url.pathname;
107
+ const method = req.method;
108
+
109
+ try {
110
+ if (method === "GET" && (path === "/" || path === "/index.html")) {
111
+ return serveFile(join(STATIC_DIR, "index.html"));
112
+ }
113
+
114
+ if (method === "GET" && path.startsWith("/static/")) {
115
+ const rel = path.slice("/static/".length);
116
+ const abs = safeJoin(STATIC_DIR, rel);
117
+ if (!abs) return new Response("forbidden", { status: 403 });
118
+ return serveFile(abs);
119
+ }
120
+
121
+ if (method === "GET" && path === "/samples") {
122
+ return jsonResponse(await listSamples());
123
+ }
124
+ if (method === "GET" && path.startsWith("/samples/")) {
125
+ const rel = path.slice("/samples/".length);
126
+ const abs = safeJoin(SAMPLES_DIR, rel);
127
+ if (!abs) return new Response("forbidden", { status: 403 });
128
+ return serveFile(abs);
129
+ }
130
+
131
+ if (method === "GET" && path === "/receipts") {
132
+ try {
133
+ const dirs = (await readdir(RECEIPTS_DIR)).sort();
134
+ const out: { name: string; files: string[] }[] = [];
135
+ for (const d of dirs) out.push({ name: d, files: await listReceiptFiles(d) });
136
+ return jsonResponse(out);
137
+ } catch {
138
+ return jsonResponse([]);
139
+ }
140
+ }
141
+ if (method === "GET" && path.startsWith("/receipts/")) {
142
+ const rel = path.slice("/receipts/".length);
143
+ const abs = safeJoin(RECEIPTS_DIR, rel);
144
+ if (!abs) return new Response("forbidden", { status: 403 });
145
+ return serveFile(abs);
146
+ }
147
+
148
+ if (method === "POST" && path === "/api/validate") {
149
+ const body = await readJsonBody(req);
150
+ const result = SkillCore.safeParse(body.skill ?? body);
151
+ if (result.success) {
152
+ return jsonResponse({
153
+ ok: true,
154
+ name: result.data.name,
155
+ steps: result.data.steps.length,
156
+ placeholders: result.data.meta.placeholders,
157
+ });
158
+ }
159
+ return jsonResponse(
160
+ { ok: false, issues: result.error.issues.slice(0, 20) },
161
+ { status: 400 },
162
+ );
163
+ }
164
+
165
+ if (method === "POST" && path === "/api/plan") {
166
+ const body = await readJsonBody(req);
167
+ const skill = SkillCore.parse(body.skill ?? body);
168
+ const inputs = (body.inputs ?? {}) as Record<string, string>;
169
+ return jsonResponse(planSkill(skill, inputs));
170
+ }
171
+
172
+ if (method === "POST" && path === "/api/export") {
173
+ const body = await readJsonBody(req);
174
+ const skill = SkillCore.parse(body.skill ?? body);
175
+ return jsonResponse(exportSkill(skill));
176
+ }
177
+
178
+ if (method === "GET" && path === "/api/health") {
179
+ return jsonResponse({ ok: true, port: PORT });
180
+ }
181
+
182
+ return new Response("not found", { status: 404 });
183
+ } catch (e) {
184
+ if (e instanceof Response) return e;
185
+ const msg = (e as Error).message ?? "unknown";
186
+ return jsonResponse({ error: msg.slice(0, 600) }, { status: 500 });
187
+ }
188
+ },
189
+ });
190
+
191
+ console.log(`meeseeks hf-space listening at http://${server.hostname}:${server.port}/`);
src/static/index.html ADDED
@@ -0,0 +1,530 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Meeseeks — Agent-Agnostic Skill Workbench</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0b1020;
10
+ --panel: #131a30;
11
+ --panel-soft: #1c2444;
12
+ --line: #2a3358;
13
+ --text: #e6e9f5;
14
+ --muted: #8a93b6;
15
+ --accent: #7c8bff;
16
+ --accent-soft: #5566ff33;
17
+ --good: #3ddc84;
18
+ --warn: #ffb547;
19
+ --bad: #ff6b6b;
20
+ --code-bg: #0a0e20;
21
+ }
22
+ * { box-sizing: border-box; }
23
+ html, body { margin: 0; padding: 0; }
24
+ body {
25
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
26
+ background: var(--bg);
27
+ color: var(--text);
28
+ line-height: 1.5;
29
+ min-height: 100vh;
30
+ }
31
+ .wrap { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
32
+ header {
33
+ border-bottom: 1px solid var(--line);
34
+ padding: 18px 0;
35
+ }
36
+ header .row {
37
+ display: flex; align-items: center; gap: 14px;
38
+ }
39
+ header .badge {
40
+ width: 36px; height: 36px; border-radius: 8px;
41
+ background: linear-gradient(135deg, #7c8bff, #5b6eff);
42
+ display: grid; place-items: center; font-size: 20px;
43
+ }
44
+ header h1 { margin: 0; font-size: 18px; font-weight: 600; letter-spacing: 0.02em; }
45
+ header .sub { color: var(--muted); font-size: 13px; }
46
+ nav.tabs { display: flex; gap: 6px; margin-top: 14px; flex-wrap: wrap; }
47
+ nav.tabs button {
48
+ background: transparent;
49
+ color: var(--muted);
50
+ border: 1px solid var(--line);
51
+ border-radius: 6px;
52
+ padding: 6px 12px;
53
+ cursor: pointer;
54
+ font: inherit;
55
+ font-size: 13px;
56
+ }
57
+ nav.tabs button.active {
58
+ background: var(--accent-soft);
59
+ color: var(--accent);
60
+ border-color: var(--accent);
61
+ }
62
+ main { padding: 28px 0 60px; }
63
+ section.pane { display: none; }
64
+ section.pane.active { display: block; }
65
+ h2 { font-size: 22px; margin: 0 0 8px; }
66
+ h3 { font-size: 14px; margin: 18px 0 8px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.1em; }
67
+ p, li { color: var(--text); }
68
+ .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
69
+ @media (max-width: 900px) { .grid-2 { grid-template-columns: 1fr; } }
70
+ .card {
71
+ background: var(--panel);
72
+ border: 1px solid var(--line);
73
+ border-radius: 10px;
74
+ padding: 16px 18px;
75
+ }
76
+ .card.soft { background: var(--panel-soft); }
77
+ textarea, input[type="text"], select {
78
+ width: 100%;
79
+ background: var(--code-bg);
80
+ color: var(--text);
81
+ border: 1px solid var(--line);
82
+ border-radius: 6px;
83
+ padding: 10px 12px;
84
+ font: inherit;
85
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
86
+ font-size: 12.5px;
87
+ }
88
+ textarea { min-height: 320px; resize: vertical; }
89
+ pre.code {
90
+ background: var(--code-bg);
91
+ border: 1px solid var(--line);
92
+ border-radius: 6px;
93
+ padding: 10px 12px;
94
+ overflow: auto;
95
+ margin: 0;
96
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
97
+ font-size: 12.5px;
98
+ white-space: pre-wrap;
99
+ word-break: break-word;
100
+ max-height: 560px;
101
+ }
102
+ button.primary {
103
+ background: var(--accent);
104
+ color: white;
105
+ border: none;
106
+ border-radius: 6px;
107
+ padding: 8px 14px;
108
+ cursor: pointer;
109
+ font: inherit;
110
+ font-weight: 600;
111
+ }
112
+ button.primary:hover { filter: brightness(1.1); }
113
+ button.ghost {
114
+ background: transparent;
115
+ color: var(--text);
116
+ border: 1px solid var(--line);
117
+ border-radius: 6px;
118
+ padding: 6px 10px;
119
+ cursor: pointer;
120
+ font: inherit;
121
+ font-size: 12px;
122
+ }
123
+ .toolbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
124
+ .row-stack { display: flex; flex-direction: column; gap: 10px; }
125
+ .pill {
126
+ display: inline-block;
127
+ background: var(--panel-soft);
128
+ border: 1px solid var(--line);
129
+ border-radius: 999px;
130
+ padding: 2px 10px;
131
+ font-size: 11.5px;
132
+ color: var(--muted);
133
+ margin-right: 6px;
134
+ }
135
+ .pill.good { color: var(--good); border-color: var(--good); }
136
+ .pill.warn { color: var(--warn); border-color: var(--warn); }
137
+ .pill.bad { color: var(--bad); border-color: var(--bad); }
138
+ .placeholder-input {
139
+ display: grid; grid-template-columns: 200px 1fr; gap: 8px; margin-bottom: 6px; align-items: center;
140
+ }
141
+ .placeholder-input label { font-size: 12px; color: var(--muted); font-family: ui-monospace, monospace; }
142
+ .placeholder-input input { font-family: ui-sans-serif, system-ui, sans-serif; }
143
+ .step {
144
+ border-left: 3px solid var(--accent);
145
+ padding: 6px 12px 6px 14px;
146
+ margin-bottom: 10px;
147
+ background: var(--panel-soft);
148
+ border-radius: 0 6px 6px 0;
149
+ }
150
+ .step .head { display: flex; gap: 8px; align-items: baseline; margin-bottom: 2px; }
151
+ .step .id { font-family: ui-monospace, monospace; font-size: 13px; color: var(--accent); }
152
+ .step .action {
153
+ font-size: 11px; padding: 1px 7px; border-radius: 999px;
154
+ background: var(--accent-soft); color: var(--accent); text-transform: uppercase; letter-spacing: 0.06em;
155
+ }
156
+ .step .body { color: var(--muted); font-size: 13px; }
157
+ .step .body strong { color: var(--text); font-weight: 500; }
158
+ .receipt-grid {
159
+ display: grid;
160
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
161
+ gap: 12px;
162
+ }
163
+ .receipt-grid .frame {
164
+ background: var(--panel-soft);
165
+ border: 1px solid var(--line);
166
+ border-radius: 8px;
167
+ overflow: hidden;
168
+ }
169
+ .receipt-grid .frame img { width: 100%; display: block; }
170
+ .receipt-grid .frame .cap { padding: 8px 10px; font-size: 11.5px; color: var(--muted); font-family: ui-monospace, monospace; }
171
+ .err { color: var(--bad); }
172
+ .ok { color: var(--good); }
173
+ .small { font-size: 12px; color: var(--muted); }
174
+ .muted { color: var(--muted); }
175
+ .kvtable { width: 100%; font-size: 13px; }
176
+ .kvtable td { padding: 4px 8px; border-bottom: 1px solid var(--line); }
177
+ .kvtable td:first-child { color: var(--muted); width: 30%; }
178
+ .bundle-tabs { display: flex; gap: 4px; margin-bottom: 10px; }
179
+ .bundle-tabs button { font-size: 12px; padding: 4px 10px; }
180
+ .footer {
181
+ border-top: 1px solid var(--line);
182
+ padding: 18px 0;
183
+ color: var(--muted);
184
+ font-size: 12px;
185
+ }
186
+ a { color: var(--accent); text-decoration: none; }
187
+ a:hover { text-decoration: underline; }
188
+ </style>
189
+ </head>
190
+ <body>
191
+ <header>
192
+ <div class="wrap">
193
+ <div class="row">
194
+ <div class="badge">⚖️</div>
195
+ <div>
196
+ <h1>Meeseeks</h1>
197
+ <div class="sub">Agent-agnostic skill workbench — author once, run on Claude / GPT / Gemini / local</div>
198
+ </div>
199
+ </div>
200
+ <nav class="tabs">
201
+ <button data-tab="home" class="active">🏠 Home</button>
202
+ <button data-tab="author">✏️ Author</button>
203
+ <button data-tab="bundle">📦 Bundle</button>
204
+ <button data-tab="receipts">🧾 Receipts</button>
205
+ </nav>
206
+ </div>
207
+ </header>
208
+
209
+ <main class="wrap">
210
+
211
+ <section class="pane active" id="pane-home">
212
+ <h2>What this is</h2>
213
+ <p class="muted">
214
+ Meeseeks turns repeatable computer workflows into structured skills any agent can run. The same JSON drops into Claude (Agent Skills format), GPT (system prompt + tool calls), Gemini, or a local model — no translation layer, no per-agent rewrite.
215
+ </p>
216
+
217
+ <div class="grid-2" style="margin-top: 16px;">
218
+ <div class="card">
219
+ <h3>The pipeline</h3>
220
+ <ol class="muted" style="margin: 0; padding-left: 20px;">
221
+ <li><strong style="color:var(--text)">Author</strong> a skill in a small Zod-validated JSON shape with bbox / accessibility-tree / success-check grounding.</li>
222
+ <li><strong style="color:var(--text)">Bundle</strong> it: derive <code>SKILL.md</code> (Anthropic Agent Skills frontmatter), <code>manifest.json</code> (grounding metadata), and <code>actions.toon</code> (token-oriented action list).</li>
223
+ <li><strong style="color:var(--text)">Run</strong> through the Playwright executor — accessibility-first resolution, screenshots per step, JSON receipt.</li>
224
+ </ol>
225
+ </div>
226
+ <div class="card">
227
+ <h3>Why it matters</h3>
228
+ <p class="muted" style="margin: 0;">
229
+ Today, every agent platform reinvents skill capture: ChatGPT custom GPTs, Claude Skills, Operator macros — all incompatible.
230
+ Meeseeks is the smallest spec that keeps a skill portable while preserving the grounding metadata that makes execution safe.
231
+ </p>
232
+ </div>
233
+ </div>
234
+
235
+ <h3 style="margin-top: 28px;">Try it</h3>
236
+ <p class="muted" style="margin-top: 0;">
237
+ Click <strong>✏️ Author</strong> to paste a skill JSON (or pick a sample), see the plan with placeholders substituted, then <strong>📦 Bundle</strong> to see the three derived artifacts. <strong>🧾 Receipts</strong> shows real screenshots from the executor walking through these skills on a mock paralegal app.
238
+ </p>
239
+ </section>
240
+
241
+ <section class="pane" id="pane-author">
242
+ <div class="grid-2">
243
+ <div>
244
+ <h3>Skill JSON <span class="small">— paste or pick a sample</span></h3>
245
+ <div class="toolbar" style="margin-bottom: 8px;">
246
+ <select id="sample-picker"><option value="">— pick a sample —</option></select>
247
+ <button class="ghost" id="format-btn">Format</button>
248
+ </div>
249
+ <textarea id="skill-input" spellcheck="false" placeholder="Paste a Sarah-JSON skill here…"></textarea>
250
+
251
+ <div id="placeholders" style="margin-top: 14px;"></div>
252
+
253
+ <div class="toolbar" style="margin-top: 14px;">
254
+ <button class="primary" id="plan-btn">Generate plan</button>
255
+ <button class="ghost" id="validate-btn">Validate only</button>
256
+ </div>
257
+ </div>
258
+
259
+ <div>
260
+ <h3>Plan output</h3>
261
+ <div id="plan-summary" class="card soft" style="margin-bottom: 10px;">
262
+ <div class="muted small">Plan results appear here after you click <em>Generate plan</em>.</div>
263
+ </div>
264
+ <div id="plan-steps"></div>
265
+ </div>
266
+ </div>
267
+ </section>
268
+
269
+ <section class="pane" id="pane-bundle">
270
+ <div class="toolbar" style="margin-bottom: 12px;">
271
+ <select id="bundle-picker"><option value="">— pick a sample ���</option></select>
272
+ <button class="primary" id="export-btn">Export bundle</button>
273
+ <span class="small muted">Generates <code>SKILL.md</code>, <code>manifest.json</code>, and <code>actions.toon</code> from the picked sample.</span>
274
+ </div>
275
+ <div class="bundle-tabs">
276
+ <button class="ghost active" data-bundle-tab="skill_md">SKILL.md</button>
277
+ <button class="ghost" data-bundle-tab="manifest_json">manifest.json</button>
278
+ <button class="ghost" data-bundle-tab="actions_toon">actions.toon</button>
279
+ </div>
280
+ <pre class="code" id="bundle-output">Pick a sample and click Export bundle.</pre>
281
+ </section>
282
+
283
+ <section class="pane" id="pane-receipts">
284
+ <p class="muted">Real screenshots from the Playwright executor running these skills against the local mock paralegal app. Each pair is the page state before and after the step's action. Resolved via accessibility role + name (the gold-standard grounding path) — no bbox or text fallback needed.</p>
285
+ <div id="receipts-list"></div>
286
+ </section>
287
+
288
+ </main>
289
+
290
+ <div class="footer wrap">
291
+ <div>Built for the AMD/lablab hackathon · Meeseeks v0.1 · <a href="https://huggingface.co/spaces/AryaYT/meeseeks">Space source</a></div>
292
+ </div>
293
+
294
+ <script>
295
+ const $ = (s, root = document) => root.querySelector(s);
296
+ const $$ = (s, root = document) => Array.from(root.querySelectorAll(s));
297
+
298
+ // Tabs
299
+ $$("nav.tabs button").forEach((b) => {
300
+ b.addEventListener("click", () => {
301
+ $$("nav.tabs button").forEach((x) => x.classList.remove("active"));
302
+ b.classList.add("active");
303
+ const tab = b.dataset.tab;
304
+ $$(".pane").forEach((p) => p.classList.toggle("active", p.id === `pane-${tab}`));
305
+ });
306
+ });
307
+
308
+ // Bundle inner tabs
309
+ let currentBundle = null;
310
+ let currentBundleTab = "skill_md";
311
+ $$(".bundle-tabs button").forEach((b) => {
312
+ b.addEventListener("click", () => {
313
+ $$(".bundle-tabs button").forEach((x) => x.classList.remove("active"));
314
+ b.classList.add("active");
315
+ currentBundleTab = b.dataset.bundleTab;
316
+ renderBundle();
317
+ });
318
+ });
319
+
320
+ function renderBundle() {
321
+ if (!currentBundle) return;
322
+ $("#bundle-output").textContent = currentBundle[currentBundleTab] ?? "(empty)";
323
+ }
324
+
325
+ // Sample picker
326
+ async function loadSamples() {
327
+ const res = await fetch("/samples");
328
+ const samples = await res.json();
329
+ for (const picker of [$("#sample-picker"), $("#bundle-picker")]) {
330
+ for (const s of samples) {
331
+ const opt = document.createElement("option");
332
+ opt.value = s.name;
333
+ opt.textContent = `${s.name} (${s.bytes}b)`;
334
+ picker.appendChild(opt);
335
+ }
336
+ }
337
+ }
338
+
339
+ $("#sample-picker").addEventListener("change", async (e) => {
340
+ const name = e.target.value;
341
+ if (!name) return;
342
+ const res = await fetch(`/samples/${name}.json`);
343
+ const text = await res.text();
344
+ $("#skill-input").value = text;
345
+ rebuildPlaceholderInputs();
346
+ });
347
+
348
+ $("#format-btn").addEventListener("click", () => {
349
+ try {
350
+ const v = JSON.parse($("#skill-input").value);
351
+ $("#skill-input").value = JSON.stringify(v, null, 2);
352
+ rebuildPlaceholderInputs();
353
+ } catch (e) {
354
+ alert("Invalid JSON: " + e.message);
355
+ }
356
+ });
357
+
358
+ $("#skill-input").addEventListener("input", rebuildPlaceholderInputs);
359
+
360
+ function getCurrentSkill() {
361
+ try { return JSON.parse($("#skill-input").value); } catch { return null; }
362
+ }
363
+
364
+ function rebuildPlaceholderInputs() {
365
+ const skill = getCurrentSkill();
366
+ const host = $("#placeholders");
367
+ host.innerHTML = "";
368
+ if (!skill || !Array.isArray(skill.meta?.placeholders)) return;
369
+ if (skill.meta.placeholders.length === 0) return;
370
+ const wrap = document.createElement("div");
371
+ wrap.className = "card soft";
372
+ wrap.innerHTML = `<h3 style="margin-top: 0;">Inputs <span class="small">— bind these placeholders</span></h3>`;
373
+ for (const ph of skill.meta.placeholders) {
374
+ const key = ph.replace(/^\{\{|\}\}$/g, "");
375
+ const row = document.createElement("div");
376
+ row.className = "placeholder-input";
377
+ row.innerHTML = `<label>${ph}</label><input type="text" data-key="${key}" placeholder="value for ${key.toLowerCase()}" />`;
378
+ wrap.appendChild(row);
379
+ }
380
+ host.appendChild(wrap);
381
+ }
382
+
383
+ function collectInputs() {
384
+ const inputs = {};
385
+ $$("#placeholders input[data-key]").forEach((i) => {
386
+ if (i.value.trim()) inputs[i.dataset.key] = i.value;
387
+ });
388
+ return inputs;
389
+ }
390
+
391
+ function fmtAction(a) {
392
+ return `<span class="action">${a}</span>`;
393
+ }
394
+
395
+ function renderStep(s, i) {
396
+ const groundingBits = [];
397
+ if (s.target.bbox) groundingBits.push(`bbox=[${s.target.bbox.join(",")}]`);
398
+ if (s.target.ax_role) groundingBits.push(`role=${s.target.ax_role}`);
399
+ if (s.target.ax_name) groundingBits.push(`name="${s.target.ax_name}"`);
400
+ const grounding = groundingBits.length ? `<span class="small muted"> · ${groundingBits.join(" ")}</span>` : "";
401
+ const fb = s.fallback ? `<div class="body"><strong>else:</strong> ${escapeHtml(s.fallback)}</div>` : "";
402
+ return `<div class="step">
403
+ <div class="head">
404
+ <span class="id">${i + 1}. ${escapeHtml(s.id)}</span> ${fmtAction(s.action)}
405
+ </div>
406
+ <div class="body"><strong>target:</strong> ${escapeHtml(s.target.description)}${grounding}</div>
407
+ <div class="body"><strong>do:</strong> ${escapeHtml(s.instruction)}</div>
408
+ <div class="body"><strong>check:</strong> ${escapeHtml(s.successCheck)}</div>
409
+ ${fb}
410
+ </div>`;
411
+ }
412
+
413
+ function escapeHtml(s) {
414
+ return String(s ?? "")
415
+ .replaceAll("&", "&amp;")
416
+ .replaceAll("<", "&lt;")
417
+ .replaceAll(">", "&gt;");
418
+ }
419
+
420
+ $("#validate-btn").addEventListener("click", async () => {
421
+ const skill = getCurrentSkill();
422
+ if (!skill) return alert("Skill JSON is invalid.");
423
+ const res = await fetch("/api/validate", {
424
+ method: "POST",
425
+ headers: { "Content-Type": "application/json" },
426
+ body: JSON.stringify({ skill }),
427
+ });
428
+ const data = await res.json();
429
+ const sum = $("#plan-summary");
430
+ if (data.ok) {
431
+ sum.innerHTML = `<div class="ok">✓ Valid skill</div>
432
+ <table class="kvtable">
433
+ <tr><td>name</td><td><code>${data.name}</code></td></tr>
434
+ <tr><td>steps</td><td>${data.steps}</td></tr>
435
+ <tr><td>placeholders</td><td>${data.placeholders.join(", ") || "(none)"}</td></tr>
436
+ </table>`;
437
+ $("#plan-steps").innerHTML = "";
438
+ } else {
439
+ sum.innerHTML = `<div class="err">✗ Invalid skill</div><pre class="code">${escapeHtml(JSON.stringify(data.issues, null, 2))}</pre>`;
440
+ }
441
+ });
442
+
443
+ $("#plan-btn").addEventListener("click", async () => {
444
+ const skill = getCurrentSkill();
445
+ if (!skill) return alert("Skill JSON is invalid.");
446
+ const inputs = collectInputs();
447
+ const res = await fetch("/api/plan", {
448
+ method: "POST",
449
+ headers: { "Content-Type": "application/json" },
450
+ body: JSON.stringify({ skill, inputs }),
451
+ });
452
+ if (!res.ok) {
453
+ const err = await res.json();
454
+ $("#plan-summary").innerHTML = `<div class="err">${escapeHtml(err.error ?? "error")}</div>`;
455
+ $("#plan-steps").innerHTML = "";
456
+ return;
457
+ }
458
+ const plan = await res.json();
459
+ const sum = $("#plan-summary");
460
+ const unboundPill = plan.unbound_placeholders.length
461
+ ? `<span class="pill warn">⚠ ${plan.unbound_placeholders.length} unbound</span>`
462
+ : `<span class="pill good">✓ all placeholders bound</span>`;
463
+ sum.innerHTML = `<div><strong>${escapeHtml(plan.skill_name)}</strong> ${unboundPill}
464
+ <span class="pill">${plan.steps.length} steps</span>
465
+ <span class="pill">domain: ${escapeHtml(plan.domain)}</span></div>
466
+ <div class="muted small" style="margin-top:6px;">${escapeHtml(plan.intent)}</div>
467
+ ${plan.unbound_placeholders.length ? `<div class="small" style="margin-top:6px;">Unbound: <code>${plan.unbound_placeholders.join(", ")}</code></div>` : ""}`;
468
+ $("#plan-steps").innerHTML = plan.steps.map(renderStep).join("");
469
+ });
470
+
471
+ // Bundle pane
472
+ $("#export-btn").addEventListener("click", async () => {
473
+ const name = $("#bundle-picker").value;
474
+ if (!name) return alert("Pick a sample first.");
475
+ const skillRes = await fetch(`/samples/${name}.json`);
476
+ const skill = await skillRes.json();
477
+ const res = await fetch("/api/export", {
478
+ method: "POST",
479
+ headers: { "Content-Type": "application/json" },
480
+ body: JSON.stringify({ skill }),
481
+ });
482
+ if (!res.ok) {
483
+ $("#bundle-output").textContent = "Error: " + (await res.text());
484
+ return;
485
+ }
486
+ currentBundle = await res.json();
487
+ renderBundle();
488
+ });
489
+
490
+ // Receipts
491
+ async function loadReceipts() {
492
+ const res = await fetch("/receipts");
493
+ const list = await res.json();
494
+ const host = $("#receipts-list");
495
+ host.innerHTML = "";
496
+ for (const r of list) {
497
+ const section = document.createElement("div");
498
+ section.style.marginBottom = "32px";
499
+ const png = r.files.filter((f) => f.endsWith(".png"));
500
+ const json = r.files.find((f) => f.endsWith(".json"));
501
+ section.innerHTML = `<h3 style="margin-top:0;">${r.name} <span class="small muted">— ${png.length} frames${json ? ", receipt JSON" : ""}</span></h3>`;
502
+ if (json) {
503
+ const link = document.createElement("a");
504
+ link.href = `/receipts/${r.name}/${json}`;
505
+ link.target = "_blank";
506
+ link.textContent = json;
507
+ link.className = "small";
508
+ const lineWrap = document.createElement("div");
509
+ lineWrap.style.marginBottom = "8px";
510
+ lineWrap.append("Receipt: ", link);
511
+ section.appendChild(lineWrap);
512
+ }
513
+ const grid = document.createElement("div");
514
+ grid.className = "receipt-grid";
515
+ for (const f of png) {
516
+ const tile = document.createElement("div");
517
+ tile.className = "frame";
518
+ tile.innerHTML = `<img loading="lazy" src="/receipts/${r.name}/${f}" alt="${f}" /><div class="cap">${f}</div>`;
519
+ grid.appendChild(tile);
520
+ }
521
+ section.appendChild(grid);
522
+ host.appendChild(section);
523
+ }
524
+ }
525
+
526
+ loadSamples().catch(console.error);
527
+ loadReceipts().catch(console.error);
528
+ </script>
529
+ </body>
530
+ </html>
tsconfig.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022", "DOM"],
7
+ "types": ["bun-types"],
8
+ "strict": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "esModuleInterop": true,
11
+ "allowSyntheticDefaultImports": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "skipLibCheck": true,
15
+ "noEmit": true,
16
+ "allowImportingTsExtensions": true
17
+ },
18
+ "include": ["src/**/*"]
19
+ }