@bendyline/gilde 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +17 -0
  2. package/data/chat-models/ge/{gemma4-e2b-q8 → gemma4-e2b-q4}/manifest.json +28 -31
  3. package/data/chat-models/ge/gemma4-e2b-q4/versions/1.2.0/manifest.json +71 -0
  4. package/data/chat-models/ge/{gemma4-e4b-q8 → gemma4-e4b-q4}/manifest.json +28 -26
  5. package/data/chat-models/ge/gemma4-e4b-q4/versions/1.2.0/manifest.json +76 -0
  6. package/data/chat-models/index.json +1 -1
  7. package/data/project-types/ca/caregiving-binder/versions/1.0.1/about.md +8 -0
  8. package/data/project-types/ca/caregiving-binder/versions/1.0.1/care.json +4 -0
  9. package/data/project-types/ca/caregiving-binder/versions/1.0.1/manifest.json +177 -0
  10. package/data/project-types/ca/caregiving-binder/versions/1.0.1/meds.json +8 -0
  11. package/data/project-types/ca/caregiving-binder/versions/1.0.1/mission.md +8 -0
  12. package/data/project-types/ca/caregiving-binder/versions/1.0.1/pages/dashboard/index.html +167 -0
  13. package/data/project-types/ch/checkers/versions/1.1.2/about.md +9 -0
  14. package/data/project-types/ch/checkers/versions/1.1.2/game.json +77 -0
  15. package/data/project-types/ch/checkers/versions/1.1.2/manifest.json +166 -0
  16. package/data/project-types/ch/checkers/versions/1.1.2/mission.md +9 -0
  17. package/data/project-types/ch/checkers/versions/1.1.2/pages/board/index.html +1091 -0
  18. package/data/project-types/ch/chess/versions/1.0.2/about.md +9 -0
  19. package/data/project-types/ch/chess/versions/1.0.2/game.json +127 -0
  20. package/data/project-types/ch/chess/versions/1.0.2/manifest.json +180 -0
  21. package/data/project-types/ch/chess/versions/1.0.2/mission.md +8 -0
  22. package/data/project-types/ch/chess/versions/1.0.2/pages/board/index.html +627 -0
  23. package/data/project-types/ch/chess/versions/1.0.3/about.md +9 -0
  24. package/data/project-types/ch/chess/versions/1.0.3/game.json +127 -0
  25. package/data/project-types/ch/chess/versions/1.0.3/manifest.json +180 -0
  26. package/data/project-types/ch/chess/versions/1.0.3/mission.md +8 -0
  27. package/data/project-types/ch/chess/versions/1.0.3/pages/board/index.html +769 -0
  28. package/data/project-types/de/design-scheme/versions/1.0.1/about.md +6 -0
  29. package/data/project-types/de/design-scheme/versions/1.0.1/manifest.json +69 -0
  30. package/data/project-types/de/design-scheme/versions/1.0.1/mission.md +3 -0
  31. package/data/project-types/de/design-scheme/versions/1.0.1/pages/gallery/index.html +105 -0
  32. package/data/project-types/de/design-scheme/versions/1.0.1/schemes/index.json +9 -0
  33. package/data/project-types/de/design-scheme/versions/1.0.1/schemes/warm-dusk.json +4 -0
  34. package/data/project-types/ev/event-planner/versions/1.0.1/about.md +7 -0
  35. package/data/project-types/ev/event-planner/versions/1.0.1/budget.json +5 -0
  36. package/data/project-types/ev/event-planner/versions/1.0.1/guests.json +10 -0
  37. package/data/project-types/ev/event-planner/versions/1.0.1/manifest.json +170 -0
  38. package/data/project-types/ev/event-planner/versions/1.0.1/mission.md +6 -0
  39. package/data/project-types/ev/event-planner/versions/1.0.1/pages/dashboard/index.html +186 -0
  40. package/data/project-types/fi/fitness-coach/versions/1.0.1/about.md +7 -0
  41. package/data/project-types/fi/fitness-coach/versions/1.0.1/manifest.json +125 -0
  42. package/data/project-types/fi/fitness-coach/versions/1.0.1/mission.md +7 -0
  43. package/data/project-types/fi/fitness-coach/versions/1.0.1/pages/dashboard/index.html +172 -0
  44. package/data/project-types/fi/fitness-coach/versions/1.0.1/training.json +6 -0
  45. package/data/project-types/fl/flashcards/versions/1.0.1/about.md +8 -0
  46. package/data/project-types/fl/flashcards/versions/1.0.1/deck.json +7 -0
  47. package/data/project-types/fl/flashcards/versions/1.0.1/manifest.json +167 -0
  48. package/data/project-types/fl/flashcards/versions/1.0.1/mission.md +6 -0
  49. package/data/project-types/fl/flashcards/versions/1.0.1/pages/review/index.html +396 -0
  50. package/data/project-types/fr/freelance-office/versions/1.0.1/about.md +6 -0
  51. package/data/project-types/fr/freelance-office/versions/1.0.1/invoices.json +9 -0
  52. package/data/project-types/fr/freelance-office/versions/1.0.1/manifest.json +142 -0
  53. package/data/project-types/fr/freelance-office/versions/1.0.1/mission.md +6 -0
  54. package/data/project-types/fr/freelance-office/versions/1.0.1/pages/dashboard/index.html +157 -0
  55. package/data/project-types/fu/fundraiser-hq/versions/1.0.1/about.md +7 -0
  56. package/data/project-types/fu/fundraiser-hq/versions/1.0.1/donations.json +7 -0
  57. package/data/project-types/fu/fundraiser-hq/versions/1.0.1/manifest.json +126 -0
  58. package/data/project-types/fu/fundraiser-hq/versions/1.0.1/mission.md +6 -0
  59. package/data/project-types/fu/fundraiser-hq/versions/1.0.1/pages/dashboard/index.html +160 -0
  60. package/data/project-types/go/go/versions/1.0.1/about.md +9 -0
  61. package/data/project-types/go/go/versions/1.0.1/game.json +103 -0
  62. package/data/project-types/go/go/versions/1.0.1/manifest.json +166 -0
  63. package/data/project-types/go/go/versions/1.0.1/mission.md +8 -0
  64. package/data/project-types/go/go/versions/1.0.1/pages/board/index.html +778 -0
  65. package/data/project-types/ho/household-budget/versions/1.0.1/about.md +6 -0
  66. package/data/project-types/ho/household-budget/versions/1.0.1/ledger.json +5 -0
  67. package/data/project-types/ho/household-budget/versions/1.0.1/manifest.json +163 -0
  68. package/data/project-types/ho/household-budget/versions/1.0.1/mission.md +6 -0
  69. package/data/project-types/ho/household-budget/versions/1.0.1/pages/dashboard/index.html +233 -0
  70. package/data/project-types/ho/household-manual/versions/1.0.1/about.md +7 -0
  71. package/data/project-types/ho/household-manual/versions/1.0.1/manifest.json +152 -0
  72. package/data/project-types/ho/household-manual/versions/1.0.1/mission.md +8 -0
  73. package/data/project-types/ho/household-manual/versions/1.0.1/pages/dashboard/index.html +157 -0
  74. package/data/project-types/ho/household-manual/versions/1.0.1/systems.json +8 -0
  75. package/data/project-types/index.json +1 -1
  76. package/data/project-types/jo/job-hunt/versions/1.0.1/about.md +10 -0
  77. package/data/project-types/jo/job-hunt/versions/1.0.1/activity.json +4 -0
  78. package/data/project-types/jo/job-hunt/versions/1.0.1/manifest.json +212 -0
  79. package/data/project-types/jo/job-hunt/versions/1.0.1/mission.md +9 -0
  80. package/data/project-types/jo/job-hunt/versions/1.0.1/pages/dashboard/index.html +327 -0
  81. package/data/project-types/jo/job-hunt/versions/1.0.1/pipeline.json +6 -0
  82. package/data/project-types/la/language-trainer/versions/1.0.1/about.md +6 -0
  83. package/data/project-types/la/language-trainer/versions/1.0.1/manifest.json +79 -0
  84. package/data/project-types/la/language-trainer/versions/1.0.1/mission.md +4 -0
  85. package/data/project-types/la/language-trainer/versions/1.0.1/pages/dashboard/index.html +153 -0
  86. package/data/project-types/la/language-trainer/versions/1.0.1/progress.json +7 -0
  87. package/data/project-types/li/life-binder/versions/1.0.1/about.md +7 -0
  88. package/data/project-types/li/life-binder/versions/1.0.1/documents.json +145 -0
  89. package/data/project-types/li/life-binder/versions/1.0.1/manifest.json +137 -0
  90. package/data/project-types/li/life-binder/versions/1.0.1/mission.md +7 -0
  91. package/data/project-types/li/life-binder/versions/1.0.1/pages/dashboard/index.html +147 -0
  92. package/data/project-types/me/meal-planner/versions/1.0.1/about.md +6 -0
  93. package/data/project-types/me/meal-planner/versions/1.0.1/manifest.json +159 -0
  94. package/data/project-types/me/meal-planner/versions/1.0.1/menu.json +5 -0
  95. package/data/project-types/me/meal-planner/versions/1.0.1/mission.md +5 -0
  96. package/data/project-types/me/meal-planner/versions/1.0.1/pages/dashboard/index.html +152 -0
  97. package/data/project-types/me/meal-planner/versions/1.0.1/pantry.json +9 -0
  98. package/data/project-types/no/novel-writing-room/versions/1.0.1/about.md +7 -0
  99. package/data/project-types/no/novel-writing-room/versions/1.0.1/chapters.json +10 -0
  100. package/data/project-types/no/novel-writing-room/versions/1.0.1/manifest.json +157 -0
  101. package/data/project-types/no/novel-writing-room/versions/1.0.1/mission.md +7 -0
  102. package/data/project-types/no/novel-writing-room/versions/1.0.1/pages/dashboard/index.html +165 -0
  103. package/data/project-types/no/novel-writing-room/versions/1.0.1/sessions.json +6 -0
  104. package/data/project-types/tr/trip-planner/versions/1.0.1/about.md +7 -0
  105. package/data/project-types/tr/trip-planner/versions/1.0.1/journal.json +4 -0
  106. package/data/project-types/tr/trip-planner/versions/1.0.1/manifest.json +193 -0
  107. package/data/project-types/tr/trip-planner/versions/1.0.1/mission.md +4 -0
  108. package/data/project-types/tr/trip-planner/versions/1.0.1/packing.json +8 -0
  109. package/data/project-types/tr/trip-planner/versions/1.0.1/pages/dashboard/index.html +215 -0
  110. package/data/project-types/tr/trip-planner/versions/1.0.1/trip.json +7 -0
  111. package/package.json +4 -2
  112. /package/data/chat-models/ge/{gemma4-e2b-q8 → gemma4-e2b-q4}/versions/1.1.0/manifest.json +0 -0
  113. /package/data/chat-models/ge/{gemma4-e2b-q8 → gemma4-e2b-q4}/versions/1.1.2/manifest.json +0 -0
  114. /package/data/chat-models/ge/{gemma4-e4b-q8 → gemma4-e4b-q4}/versions/1.1.0/manifest.json +0 -0
  115. /package/data/chat-models/ge/{gemma4-e4b-q8 → gemma4-e4b-q4}/versions/1.1.2/manifest.json +0 -0
@@ -0,0 +1,125 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "version": "1.0.1",
4
+ "releasedAt": "2026-08-01T00:00:00Z",
5
+ "meesterManaged": false,
6
+ "params": {
7
+ "type": "object",
8
+ "properties": {
9
+ "goal": {
10
+ "type": "string",
11
+ "title": "What are you training toward?",
12
+ "description": "e.g. 'feel strong again', 'a 10k in October'.",
13
+ "default": "feeling strong and consistent"
14
+ },
15
+ "weeklyTarget": {
16
+ "type": "number",
17
+ "title": "Sessions per week",
18
+ "description": "How many training sessions a week to aim for.",
19
+ "default": 3
20
+ }
21
+ },
22
+ "required": [
23
+ "goal",
24
+ "weeklyTarget"
25
+ ]
26
+ },
27
+ "nameTemplate": "Fitness: {{goal}}",
28
+ "aboutTemplate": "about.md",
29
+ "missionTemplate": "mission.md",
30
+ "gezels": [
31
+ {
32
+ "templateId": "coach",
33
+ "voorman": true
34
+ }
35
+ ],
36
+ "tools": [
37
+ {
38
+ "name": "log_workout",
39
+ "description": "Log a training session the moment it is reported (activity + minutes).",
40
+ "script": "training-store",
41
+ "inputs": {
42
+ "type": "object",
43
+ "properties": {
44
+ "activity": {
45
+ "type": "string"
46
+ },
47
+ "minutes": {
48
+ "type": "number"
49
+ },
50
+ "note": {
51
+ "type": "string"
52
+ }
53
+ },
54
+ "required": [
55
+ "activity"
56
+ ]
57
+ },
58
+ "bind": {
59
+ "action": "log_workout"
60
+ }
61
+ },
62
+ {
63
+ "name": "set_target",
64
+ "description": "Change the weekly session target.",
65
+ "script": "training-store",
66
+ "inputs": {
67
+ "type": "object",
68
+ "properties": {
69
+ "weeklyTarget": {
70
+ "type": "number"
71
+ }
72
+ },
73
+ "required": [
74
+ "weeklyTarget"
75
+ ]
76
+ },
77
+ "bind": {
78
+ "action": "set_target"
79
+ }
80
+ },
81
+ {
82
+ "name": "training_status",
83
+ "description": "Streak, this week against the target, all-time count.",
84
+ "script": "training-store",
85
+ "inputs": {
86
+ "type": "object",
87
+ "properties": {}
88
+ },
89
+ "bind": {
90
+ "action": "status"
91
+ }
92
+ },
93
+ {
94
+ "name": "workout_history",
95
+ "description": "The recent sessions, most recent last.",
96
+ "script": "training-store",
97
+ "inputs": {
98
+ "type": "object",
99
+ "properties": {}
100
+ },
101
+ "bind": {
102
+ "action": "history"
103
+ }
104
+ }
105
+ ],
106
+ "craftbooks": [
107
+ "weekly-review"
108
+ ],
109
+ "schedules": [],
110
+ "pages": {
111
+ "entry": "dashboard/index.html",
112
+ "reads": [
113
+ {
114
+ "source": "workspace",
115
+ "path": "training.json"
116
+ }
117
+ ]
118
+ },
119
+ "workspaceSeed": [
120
+ "training.json"
121
+ ],
122
+ "scripts": {
123
+ "training-store": "import { type InferredInput, defineScript, gezel } from '@bendyline/gezel-sdk';\nimport { logStore } from '@bendyline/gezel-sdk/stores';\n\nexport const meta = defineScript({\n name: 'training-store',\n description:\n \"Read and update the training log (training.json). Call action:'status' for streak + this week vs the target; 'log_workout' the moment a session is reported (activity + minutes); 'set_target' to change the weekly session target; 'history' for the recent sessions.\",\n inputs: {\n action: {\n type: 'string',\n description: \"One of 'status', 'log_workout', 'set_target', or 'history'.\",\n required: true,\n },\n activity: { type: 'string', description: \"What they did, e.g. 'run', 'gym', 'long walk'.\" },\n minutes: { type: 'number', description: 'How long the session ran, in minutes.' },\n note: { type: 'string', description: 'Anything worth remembering about the session.' },\n weeklyTarget: { type: 'number', description: 'Sessions per week to aim for (set_target).' },\n },\n outputs: {\n streakDays: { type: 'number', description: 'Consecutive days with a session, ending at the latest.' },\n thisWeek: { type: 'number', description: 'Sessions logged this week (Monday UTC start).' },\n weeklyTarget: { type: 'number', description: 'The current weekly session target.' },\n summary: { type: 'string', description: 'The training picture.' },\n },\n requires: ['workspace.read', 'workspace.write'],\n} as const);\n\nconst input = gezel.input as InferredInput<typeof meta>;\n\n// ── fitness-rules-start ──\n// Pure derivations over logStore-shaped workout events: kind 'workout',\n// data {activity, minutes}. Weeks start Monday 00:00 UTC; nowIso is a\n// parameter so the rules stay clock-independent and testable.\n\nfunction weekStartMs(nowIso) {\n var day = new Date(nowIso.slice(0, 10) + 'T00:00:00Z');\n var sinceMonday = (day.getUTCDay() + 6) % 7;\n return day.getTime() - sinceMonday * 86400000;\n}\n\nfunction thisWeekCount(events, nowIso) {\n var start = weekStartMs(nowIso);\n return events.filter(function (e) {\n return Date.parse(e.at) >= start;\n }).length;\n}\n\nfunction trainingLine(stats, weekCount, weeklyTarget) {\n var target = weeklyTarget > 0 ? weeklyTarget : 0;\n var weekPart =\n 'This week: ' +\n weekCount +\n (target > 0 ? ' of ' + target + ' session(s)' : ' session(s)') +\n (target > 0 && weekCount >= target ? ' — target met.' : '.');\n var streakPart =\n stats.total === 0\n ? 'No sessions logged yet.'\n : 'Streak: ' + stats.streakDays + ' day(s); ' + stats.total + ' session(s) all time.';\n return weekPart + ' ' + streakPart;\n}\n// ── fitness-rules-end ──\n\nconst training = logStore(gezel.fs, 'training.json');\n\nasync function readWeeklyTarget(): Promise<number> {\n try {\n const parsed = JSON.parse(await gezel.fs.read('training.json')) as { weeklyTarget?: unknown };\n const target = Number(parsed.weeklyTarget);\n return Number.isFinite(target) && target > 0 ? Math.round(target) : 0;\n } catch {\n return 0;\n }\n}\n\nif (input.action === 'log_workout') {\n if (!input.activity) throw new Error(\"log_workout needs 'activity' (what they did).\");\n await training.append({\n kind: 'workout',\n ...(input.note ? { note: input.note } : {}),\n data: {\n activity: input.activity,\n ...(typeof input.minutes === 'number' && input.minutes > 0\n ? { minutes: Math.round(input.minutes) }\n : {}),\n },\n });\n} else if (input.action === 'set_target') {\n const target = Number(input.weeklyTarget);\n if (!Number.isFinite(target) || target <= 0) {\n throw new Error(\"set_target needs a positive 'weeklyTarget' (sessions per week).\");\n }\n // logStore has no field API; the passenger key flips via a targeted\n // read-modify-write that preserves the rest of the file.\n const raw = JSON.parse(await gezel.fs.read('training.json')) as Record<string, unknown>;\n raw.weeklyTarget = Math.round(target);\n await gezel.fs.write('training.json', `${JSON.stringify(raw, null, 2)}\\n`);\n} else if (input.action !== 'status' && input.action !== 'history') {\n throw new Error(`Unknown action '${input.action}'. Use status, log_workout, set_target, or history.`);\n}\n\nconst events = await training.list();\nconst stats = await training.stats();\nconst weeklyTarget = await readWeeklyTarget();\nconst nowIso = new Date().toISOString();\nconst weekCount = thisWeekCount(events, nowIso);\nconst listing =\n input.action === 'history'\n ? events\n .slice(-12)\n .map(\n (e) =>\n `${e.at.slice(0, 10)} ${String(e.data?.activity ?? 'session')}${typeof e.data?.minutes === 'number' ? ` (${e.data.minutes} min)` : ''}${e.note ? ` — ${e.note}` : ''}`,\n )\n .join('\\n') || 'No sessions yet.'\n : '';\n\ngezel.output({\n streakDays: stats.streakDays,\n thisWeek: weekCount,\n weeklyTarget,\n summary: listing || trainingLine(stats, weekCount, weeklyTarget),\n});\n"
124
+ }
125
+ }
@@ -0,0 +1,7 @@
1
+ - Build a training rhythm that holds: {{weeklyTarget}} session(s) a week toward
2
+ {{goal}}.
3
+ - Every session gets logged the day it happens — the streak and the weekly
4
+ count come from the record, never from memory.
5
+ - Consistency over intensity: a shrunk plan that survives a hard week beats an
6
+ ambitious one that dies in it.
7
+ - Encouragement always, guilt never.
@@ -0,0 +1,172 @@
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
+ <meta name="gezel-demo" content="standalone" />
7
+ <title>Fitness Coach</title>
8
+ <style>
9
+ :root {
10
+ color-scheme: light dark;
11
+ --bg: #faf7f2; --card: #fff; --ink: #2b2620; --muted: #7a7166;
12
+ --accent: #b5651d; --track: #ece5da; --good: #4a7c59;
13
+ }
14
+ @media (prefers-color-scheme: dark) {
15
+ :root {
16
+ --bg: #1c1a17; --card: #262320; --ink: #efe9e0; --muted: #a59a8c;
17
+ --accent: #e0894a; --track: #3a352f; --good: #7fae8d;
18
+ }
19
+ }
20
+ * { box-sizing: border-box; }
21
+ body { margin: 0; font: 15px/1.5 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--ink); padding: 1.5rem 1.25rem; }
22
+ main { max-width: 620px; margin: 0 auto; }
23
+ h1 { font-family: Georgia, 'PT Serif', 'Times New Roman', serif; font-size: 1.35rem; font-weight: 600; margin: 0 0 0.15rem; }
24
+ .sub { color: var(--muted); font-size: 0.9rem; margin: 0 0 0.9rem; }
25
+ .stats { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; margin-bottom: 0.75rem; }
26
+ .stat { background: var(--card); border: 1px solid var(--track); border-radius: 10px; padding: 0.85rem 1rem; }
27
+ .stat .num { font-size: 1.9rem; font-weight: 700; font-variant-numeric: tabular-nums; }
28
+ .stat .num.good { color: var(--good); }
29
+ .stat .lbl { color: var(--muted); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; }
30
+ .week { background: var(--card); border: 1px solid var(--track); border-radius: 10px; padding: 0.85rem 1rem; margin-bottom: 0.75rem; }
31
+ .week h2, .recent h2 { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin: 0 0 0.5rem; }
32
+ .dots { display: flex; gap: 0.4rem; }
33
+ .dot { width: 22px; height: 22px; border-radius: 6px; background: var(--track); }
34
+ .dot.hit { background: var(--good); }
35
+ .week .note { color: var(--muted); font-size: 0.85rem; margin-top: 0.5rem; }
36
+ .recent { background: var(--card); border: 1px solid var(--track); border-radius: 10px; padding: 0.85rem 1rem; font-size: 0.9rem; }
37
+ .recent ul { margin: 0; padding-left: 1.05rem; }
38
+ .recent li { margin: 0.25rem 0; }
39
+ .recent .when { color: var(--muted); font-variant-numeric: tabular-nums; }
40
+ .empty { color: var(--muted); font-style: italic; }
41
+ + .demo-banner {
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: space-between;
45
+ gap: 0.75rem;
46
+ margin: 0 0 0.85rem;
47
+ padding: 0.5rem 0.65rem;
48
+ border: 1px solid var(--accent, #8a5a2b);
49
+ border-radius: 6px;
50
+ background: color-mix(in srgb, var(--card, var(--surface, #fffdf8)) 88%, var(--accent, #8a5a2b));
51
+ color: var(--ink, #2b2620);
52
+ font-size: 0.8rem;
53
+ }
54
+ .demo-banner[hidden] { display: none; }
55
+ .demo-banner strong { font-weight: 700; }
56
+ .demo-reset {
57
+ flex: none;
58
+ padding: 0.25rem 0.5rem;
59
+ border: 1px solid var(--accent, #8a5a2b);
60
+ border-radius: 6px;
61
+ background: var(--card, var(--surface, #fffdf8));
62
+ color: var(--ink, #2b2620);
63
+ cursor: pointer;
64
+ font: inherit;
65
+ }
66
+ .demo-reset:hover { border-color: var(--ink, #2b2620); }
67
+ </style>
68
+ </head>
69
+ <body>
70
+ <main>
71
+ + <div class="demo-banner" data-gezel-demo-banner hidden>
72
+ <span><strong>Demo mode</strong> — sample data stays in this page.</span>
73
+ <button type="button" class="demo-reset" onclick="location.reload()">Reset demo</button>
74
+ </div>
75
+ <h1 id="title">Fitness Coach</h1>
76
+ <p class="sub" id="sub">Loading the training log…</p>
77
+ <div class="stats">
78
+ <div class="stat"><div class="num good" id="streak">—</div><div class="lbl">Day streak</div></div>
79
+ <div class="stat"><div class="num" id="total">—</div><div class="lbl">Sessions all time</div></div>
80
+ </div>
81
+ <div class="week">
82
+ <h2>This week</h2>
83
+ <div class="dots" id="dots"></div>
84
+ <div class="note" id="weekNote"></div>
85
+ </div>
86
+ <div class="recent">
87
+ <h2>Recent sessions</h2>
88
+ <ul id="recent"><li class="empty">Nothing logged yet — tell your Coach about your first session.</li></ul>
89
+ </div>
90
+ </main>
91
+ <script>
92
+ (function () {
93
+ 'use strict';
94
+ var parts = location.pathname.split('/').filter(Boolean);
95
+ var typeIdx = parts.indexOf('type');
96
+ var capability = typeIdx > 0 ? parts[typeIdx - 1] : null;
97
+ var projectId = typeIdx >= 0 ? parts[typeIdx + 1] : null;
98
+ var failedPolls = 0;
99
+ var DAY = 86400000;
100
+ var demoMode = !capability || !projectId;
101
+ var demoTraining = {
102
+ goal: 'a comfortable 10K',
103
+ weeklyTarget: 4,
104
+ events: [
105
+ { at: new Date(Date.now() - 6 * DAY).toISOString(), data: { activity: 'Easy run', minutes: 32 } },
106
+ { at: new Date(Date.now() - 4 * DAY).toISOString(), data: { activity: 'Mobility and strength', minutes: 25 } },
107
+ { at: new Date(Date.now() - 2 * DAY).toISOString(), data: { activity: 'Tempo intervals', minutes: 38 } },
108
+ { at: new Date().toISOString(), data: { activity: 'Recovery walk', minutes: 28 } },
109
+ ],
110
+ };
111
+ if (demoMode) document.querySelector('[data-gezel-demo-banner]').hidden = false;
112
+
113
+ function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); }
114
+ function utcDay(iso) { return iso.slice(0, 10); }
115
+ function weekStartMs(nowIso) {
116
+ var d = new Date(nowIso.slice(0, 10) + 'T00:00:00Z');
117
+ return d.getTime() - ((d.getUTCDay() + 6) % 7) * DAY;
118
+ }
119
+ function streakDays(events) {
120
+ if (!events.length) return 0;
121
+ var days = {};
122
+ var last = null;
123
+ events.forEach(function (e) { days[utcDay(e.at)] = true; if (last === null || e.at > last) last = e.at; });
124
+ var streak = 0;
125
+ var cursor = Date.parse(utcDay(last) + 'T00:00:00Z');
126
+ while (days[new Date(cursor).toISOString().slice(0, 10)]) { streak += 1; cursor -= DAY; }
127
+ return streak;
128
+ }
129
+
130
+ function render(doc) {
131
+ var events = Array.isArray(doc.events) ? doc.events : [];
132
+ var target = Number(doc.weeklyTarget);
133
+ if (!isFinite(target) || target <= 0) target = 0;
134
+ if (doc.goal) document.getElementById('title').textContent = 'Training toward: ' + doc.goal;
135
+ document.getElementById('sub').textContent = events.length ? events.length + ' session(s) on record' : 'The record starts with the first session.';
136
+ document.getElementById('streak').textContent = String(streakDays(events));
137
+ document.getElementById('total').textContent = String(events.length);
138
+
139
+ var start = weekStartMs(new Date().toISOString());
140
+ var week = events.filter(function (e) { return Date.parse(e.at) >= start; }).length;
141
+ var slots = Math.max(target, week, 1);
142
+ var dots = '';
143
+ for (var i = 0; i < slots; i++) dots += '<span class="dot' + (i < week ? ' hit' : '') + '"></span>';
144
+ document.getElementById('dots').innerHTML = dots;
145
+ document.getElementById('weekNote').textContent = target > 0
146
+ ? week + ' of ' + target + ' session(s)' + (week >= target ? ' — target met.' : '.')
147
+ : week + ' session(s) this week.';
148
+
149
+ if (events.length) {
150
+ document.getElementById('recent').innerHTML = events.slice(-8).reverse().map(function (e) {
151
+ var mins = e.data && isFinite(Number(e.data.minutes)) && Number(e.data.minutes) > 0 ? ' (' + Number(e.data.minutes) + ' min)' : '';
152
+ return '<li><span class="when">' + esc(utcDay(e.at)) + '</span> ' + esc((e.data && e.data.activity) || 'session') + mins + '</li>';
153
+ }).join('');
154
+ }
155
+ }
156
+
157
+ function poll() {
158
+ if (demoMode) { render(demoTraining); return Promise.resolve(); }
159
+ if (!capability || !projectId) return Promise.resolve();
160
+ return fetch('/preview/' + encodeURIComponent(capability) + '/workspace/' + encodeURIComponent(projectId) + '/training.json', { cache: 'no-store' })
161
+ .then(function (r) { if (!r.ok) throw new Error('poll ' + r.status); return r.json(); })
162
+ .then(function (next) { failedPolls = 0; render(next); })
163
+ .catch(function () {
164
+ failedPolls += 1;
165
+ if (failedPolls === 3 && window.parent !== window) window.parent.postMessage({ __gezelPageRefresh: true }, '*');
166
+ });
167
+ }
168
+ poll().then(function () { setInterval(function () { void poll(); }, 5000); });
169
+ })();
170
+ </script>
171
+ </body>
172
+ </html>
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": 1,
3
+ "goal": "{{goal}}",
4
+ "weeklyTarget": {{weeklyTarget}},
5
+ "events": []
6
+ }
@@ -0,0 +1,8 @@
1
+ A spaced-repetition practice bench for **{{subject}}**.
2
+
3
+ Your Studiemaat writes the cards — ask for them in chat, or let them propose
4
+ cards when a weak spot shows. You review in the **Output** tab: flip the card,
5
+ mark it got-it or missed, and the deck's schedule decides when each card comes
6
+ back (daily, every three days, weekly, as you prove them).
7
+
8
+ After each session your Studiemaat checks in on how it went.
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 1,
3
+ "subject": "{{subject}}",
4
+ "cards": [],
5
+ "dueCardIds": [],
6
+ "updatedAt": "2026-07-18T00:00:00.000Z"
7
+ }
@@ -0,0 +1,167 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "version": "1.0.1",
4
+ "releasedAt": "2026-08-01T00:00:00Z",
5
+ "meesterManaged": false,
6
+ "params": {
7
+ "type": "object",
8
+ "properties": {
9
+ "subject": {
10
+ "type": "string",
11
+ "title": "What are you learning?",
12
+ "description": "The deck's subject — e.g. Spanish vocabulary, AWS certification, organic chemistry.",
13
+ "default": "Spanish vocabulary"
14
+ }
15
+ },
16
+ "required": [
17
+ "subject"
18
+ ]
19
+ },
20
+ "nameTemplate": "{{subject}} Flashcards",
21
+ "aboutTemplate": "about.md",
22
+ "missionTemplate": "mission.md",
23
+ "gezels": [
24
+ {
25
+ "templateId": "study-buddy",
26
+ "voorman": true
27
+ }
28
+ ],
29
+ "scripts": {
30
+ "deck-store": "import { type InferredInput, defineScript, gezel } from '@bendyline/gezel-sdk';\n\nexport const meta = defineScript({\n name: 'deck-store',\n description:\n \"Read and update the flashcard deck (deck.json). Call action:'add_cards' with a cards array ([{front, back}, ...]) to add practice cards; action:'state' to see due counts and box distribution; action:'list_deck' to read every card. The review page drives 'record_review' and 'finish_session' itself.\",\n inputs: {\n action: {\n type: 'string',\n description:\n \"One of 'state', 'list_deck', 'add_cards', 'record_review', or 'finish_session'.\",\n required: true,\n },\n cards: {\n type: 'json',\n description: 'For add_cards: an array of {front, back} objects.',\n },\n cardId: { type: 'string', description: \"For record_review: the card id, e.g. 'card-3'.\" },\n verdict: { type: 'string', description: \"For record_review: 'got-it' or 'missed'.\" },\n reviewed: { type: 'number', description: 'For finish_session: how many cards were reviewed.' },\n missedIds: { type: 'json', description: 'For finish_session: array of card ids that were missed.' },\n },\n outputs: {\n total: { type: 'number', description: 'Cards in the deck.' },\n due: { type: 'number', description: 'Cards due for review now.' },\n summary: { type: 'string', description: 'Human-readable deck or session summary.' },\n missedList: { type: 'string', description: 'Fronts of the cards missed this session (finish_session).' },\n },\n requires: ['workspace.read', 'workspace.write'],\n} as const);\n\nconst input = gezel.input as InferredInput<typeof meta>;\n\n// ── leitner-rules-start ──\n// Pure Leitner-box scheduling (3 boxes). Box 1 reviews daily, box 2 every\n// 3 days, box 3 weekly. A got-it promotes (capped at 3); a miss goes back\n// to box 1. Never-reviewed cards are always due. Kept self-contained\n// between these sentinels so unit tests execute the shipped bytes.\n\nvar BOX_INTERVAL_DAYS = { 1: 1, 2: 3, 3: 7 };\nvar DAY_MS = 24 * 60 * 60 * 1000;\n\nfunction isDue(card, nowMs) {\n if (!card.lastReviewedAt) return true;\n var interval = BOX_INTERVAL_DAYS[card.box] || 1;\n return nowMs - Date.parse(card.lastReviewedAt) >= interval * DAY_MS;\n}\n\nfunction computeDueIds(cards, nowMs) {\n return cards\n .filter(function (c) {\n return isDue(c, nowMs);\n })\n .sort(function (a, b) {\n if (a.box !== b.box) return a.box - b.box;\n return a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0;\n })\n .map(function (c) {\n return c.id;\n });\n}\n\nfunction applyReview(card, verdict, nowIso) {\n if (verdict === 'got-it') card.box = Math.min(3, card.box + 1);\n else card.box = 1;\n card.lastReviewedAt = nowIso;\n card.stats = card.stats || { gotIt: 0, missed: 0 };\n if (verdict === 'got-it') card.stats.gotIt += 1;\n else card.stats.missed += 1;\n return card;\n}\n\nfunction nextCardId(cards) {\n var max = 0;\n for (var i = 0; i < cards.length; i += 1) {\n var m = /^card-(\\d+)$/.exec(cards[i].id);\n if (m) max = Math.max(max, Number(m[1]));\n }\n return 'card-' + (max + 1);\n}\n// ── leitner-rules-end ──\n\nconst DECK_FILE = 'deck.json';\n\ninterface Card {\n id: string;\n front: string;\n back: string;\n box: number;\n lastReviewedAt: string | null;\n createdAt: string;\n stats: { gotIt: number; missed: number };\n}\n\ninterface Deck {\n version: number;\n subject: string;\n cards: Card[];\n dueCardIds: string[];\n updatedAt: string;\n}\n\nasync function load(): Promise<Deck> {\n let raw: string;\n try {\n raw = await gezel.fs.read(DECK_FILE);\n } catch {\n return { version: 1, subject: 'your subject', cards: [], dueCardIds: [], updatedAt: new Date().toISOString() };\n }\n const parsed = JSON.parse(raw) as Partial<Deck>;\n return {\n version: 1,\n subject: typeof parsed.subject === 'string' ? parsed.subject : 'your subject',\n cards: Array.isArray(parsed.cards) ? (parsed.cards as Card[]) : [],\n dueCardIds: [],\n updatedAt: new Date().toISOString(),\n };\n}\n\nasync function persist(deck: Deck): Promise<Deck> {\n deck.dueCardIds = computeDueIds(deck.cards, Date.now());\n deck.updatedAt = new Date().toISOString();\n await gezel.fs.write(DECK_FILE, `${JSON.stringify(deck, null, 2)}\\n`);\n return deck;\n}\n\nconst deck = await load();\ndeck.dueCardIds = computeDueIds(deck.cards, Date.now());\nlet summary = '';\nlet missedList = '';\n\nif (input.action === 'add_cards') {\n const raw = Array.isArray(input.cards) ? input.cards : [];\n const added: string[] = [];\n for (const entry of raw) {\n const front = typeof (entry as { front?: unknown }).front === 'string' ? (entry as { front: string }).front.trim() : '';\n const back = typeof (entry as { back?: unknown }).back === 'string' ? (entry as { back: string }).back.trim() : '';\n if (!front || !back) continue;\n const card: Card = {\n id: nextCardId(deck.cards),\n front: front.slice(0, 300),\n back: back.slice(0, 500),\n box: 1,\n lastReviewedAt: null,\n createdAt: new Date().toISOString(),\n stats: { gotIt: 0, missed: 0 },\n };\n deck.cards.push(card);\n added.push(card.id);\n }\n if (added.length === 0) {\n throw new Error(\"add_cards needs a non-empty 'cards' array of {front, back} objects.\");\n }\n await persist(deck);\n summary = `Added ${added.length} card(s). ${deck.cards.length} in the deck, ${deck.dueCardIds.length} due now.`;\n} else if (input.action === 'record_review') {\n if (!input.cardId || (input.verdict !== 'got-it' && input.verdict !== 'missed')) {\n throw new Error(\"record_review needs 'cardId' and verdict 'got-it' or 'missed'.\");\n }\n const card = deck.cards.find((c) => c.id === input.cardId);\n if (!card) throw new Error(`No card '${input.cardId}' in the deck.`);\n applyReview(card, input.verdict, new Date().toISOString());\n await persist(deck);\n summary = `${card.id} marked ${input.verdict} (now box ${card.box}). ${deck.dueCardIds.length} still due.`;\n} else if (input.action === 'finish_session') {\n const reviewed = typeof input.reviewed === 'number' ? input.reviewed : 0;\n const missedIds = Array.isArray(input.missedIds) ? input.missedIds.map(String) : [];\n const missedFronts = missedIds\n .map((id) => deck.cards.find((c) => c.id === id)?.front)\n .filter((f): f is string => typeof f === 'string');\n missedList = missedFronts.length ? missedFronts.join(' | ') : '(none)';\n summary = `Reviewed ${reviewed} card(s): ${reviewed - missedIds.length} got it, ${missedIds.length} missed. ${deck.dueCardIds.length} due next.`;\n} else if (input.action === 'list_deck') {\n summary = deck.cards.length\n ? deck.cards\n .map((c) => `${c.id} [box ${c.box}] ${c.front} -> ${c.back}`)\n .join('\\n')\n : 'The deck is empty — add cards with add_cards.';\n} else if (input.action === 'state') {\n const boxes = { 1: 0, 2: 0, 3: 0 } as Record<number, number>;\n for (const c of deck.cards) boxes[c.box] = (boxes[c.box] ?? 0) + 1;\n summary = deck.cards.length\n ? `${deck.cards.length} card(s) on \"${deck.subject}\": ${deck.dueCardIds.length} due now. Boxes: ${boxes[1]} daily, ${boxes[2]} every-3-days, ${boxes[3]} weekly.`\n : `The deck for \"${deck.subject}\" is empty — offer to write the first cards.`;\n} else {\n throw new Error(\n `Unknown action '${input.action}'. Use state, list_deck, add_cards, record_review, or finish_session.`,\n );\n}\n\ngezel.output({\n total: deck.cards.length,\n due: deck.dueCardIds.length,\n summary,\n missedList,\n});\n"
31
+ },
32
+ "tools": [
33
+ {
34
+ "name": "add_cards",
35
+ "description": "Add flashcards to the deck. Pass a cards array of {front, back} objects — one fact per card.",
36
+ "script": "deck-store",
37
+ "inputs": {
38
+ "type": "object",
39
+ "properties": {
40
+ "cards": {
41
+ "type": "array",
42
+ "items": {
43
+ "type": "object",
44
+ "properties": {
45
+ "front": {
46
+ "type": "string"
47
+ },
48
+ "back": {
49
+ "type": "string"
50
+ }
51
+ },
52
+ "required": [
53
+ "front",
54
+ "back"
55
+ ]
56
+ },
57
+ "description": "The cards to add."
58
+ }
59
+ },
60
+ "required": [
61
+ "cards"
62
+ ]
63
+ },
64
+ "bind": {
65
+ "action": "add_cards"
66
+ }
67
+ },
68
+ {
69
+ "name": "deck_status",
70
+ "description": "See the deck: how many cards, how many due, and the box distribution.",
71
+ "script": "deck-store",
72
+ "inputs": {
73
+ "type": "object",
74
+ "properties": {}
75
+ },
76
+ "bind": {
77
+ "action": "state"
78
+ }
79
+ },
80
+ {
81
+ "name": "list_deck",
82
+ "description": "Read every card in the deck (id, box, front, back).",
83
+ "script": "deck-store",
84
+ "inputs": {
85
+ "type": "object",
86
+ "properties": {}
87
+ },
88
+ "bind": {
89
+ "action": "list_deck"
90
+ }
91
+ },
92
+ {
93
+ "name": "record_review",
94
+ "description": "The review page's per-card verdict.",
95
+ "script": "deck-store",
96
+ "inputs": {
97
+ "type": "object",
98
+ "properties": {
99
+ "cardId": {
100
+ "type": "string"
101
+ },
102
+ "verdict": {
103
+ "type": "string",
104
+ "enum": [
105
+ "got-it",
106
+ "missed"
107
+ ]
108
+ }
109
+ },
110
+ "required": [
111
+ "cardId",
112
+ "verdict"
113
+ ]
114
+ },
115
+ "bind": {
116
+ "action": "record_review"
117
+ }
118
+ },
119
+ {
120
+ "name": "finish_session",
121
+ "description": "The review page's end-of-session summary.",
122
+ "script": "deck-store",
123
+ "inputs": {
124
+ "type": "object",
125
+ "properties": {
126
+ "reviewed": {
127
+ "type": "number"
128
+ },
129
+ "missedIds": {
130
+ "type": "array",
131
+ "items": {
132
+ "type": "string"
133
+ }
134
+ }
135
+ },
136
+ "required": [
137
+ "reviewed"
138
+ ]
139
+ },
140
+ "bind": {
141
+ "action": "finish_session"
142
+ },
143
+ "reaction": {
144
+ "gezel": "study-buddy",
145
+ "prompt": "The user finished a flashcard review session. {{output.summary}}\nMissed cards: {{output.missedList}}\nReply with ONE short encouraging line — specific beats generic. If any cards were missed, offer to add fresh practice cards that come at those same ideas from a new angle (add them with your deck capabilities if the user agrees)."
146
+ }
147
+ }
148
+ ],
149
+ "craftbooks": [],
150
+ "schedules": [],
151
+ "pages": {
152
+ "entry": "review/index.html",
153
+ "reads": [
154
+ {
155
+ "source": "workspace",
156
+ "path": "deck.json"
157
+ }
158
+ ],
159
+ "tools": [
160
+ "record_review",
161
+ "finish_session"
162
+ ]
163
+ },
164
+ "workspaceSeed": [
165
+ "deck.json"
166
+ ]
167
+ }
@@ -0,0 +1,6 @@
1
+ - A living deck on {{subject}}: a few sharp new cards whenever they're needed,
2
+ never bulk filler.
3
+ - Due cards get reviewed most days; the page's schedule is the source of truth.
4
+ - Every missed card gets another road in — fresh cards on the same idea from a
5
+ new angle, offered after the session.
6
+ - Encouragement always, shame never: a miss is the system working.