@bendyline/gilde 0.1.21 → 0.1.23

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 (69) hide show
  1. package/authoring/chat-models/nemotron3.5-lightning-30b-q4.json +3 -2
  2. package/authoring/chat-models/nemotron3.5-lightning-30b-q8.json +3 -2
  3. package/authoring/page-demo-stub.js +85 -0
  4. package/data/chat-models/index.json +1 -1
  5. package/data/chat-models/ne/nemotron3.5-lightning-30b-q4/manifest.json +3 -2
  6. package/data/chat-models/ne/nemotron3.5-lightning-30b-q8/manifest.json +3 -2
  7. package/data/connector-types/bl/bluesky-posts/manifest.json +16 -0
  8. package/data/connector-types/bl/bluesky-posts/versions/1.0.0/manifest.json +76 -0
  9. package/data/connector-types/ca/calendar-google/versions/1.0.0/manifest.json +14 -0
  10. package/data/connector-types/gi/github-issues/versions/1.0.1/manifest.json +67 -0
  11. package/data/connector-types/in/instagram-media/manifest.json +17 -0
  12. package/data/connector-types/in/instagram-media/versions/1.0.0/manifest.json +96 -0
  13. package/data/connector-types/index.json +1 -1
  14. package/data/connector-types/li/linkedin-posts/manifest.json +16 -0
  15. package/data/connector-types/li/linkedin-posts/versions/1.0.0/manifest.json +54 -0
  16. package/data/connector-types/ma/mail-gmail/versions/1.0.0/manifest.json +14 -0
  17. package/data/connector-types/ma/mail-microsoft365/versions/1.0.0/manifest.json +14 -1
  18. package/data/connector-types/ma/mail-outlook/versions/1.0.0/manifest.json +14 -1
  19. package/data/connector-types/x-/x-posts/manifest.json +17 -0
  20. package/data/connector-types/x-/x-posts/versions/1.0.0/manifest.json +92 -0
  21. package/data/craftbook-templates/dr/draft-social-post/manifest.json +24 -0
  22. package/data/craftbook-templates/dr/draft-social-post/versions/1.0.0/craftbook.json +178 -0
  23. package/data/craftbook-templates/dr/draft-social-post/versions/1.0.0/test.json +161 -0
  24. package/data/craftbook-templates/index.json +1 -1
  25. package/data/craftbook-templates/re/reception-report/manifest.json +21 -0
  26. package/data/craftbook-templates/re/reception-report/versions/1.0.0/craftbook.json +170 -0
  27. package/data/craftbook-templates/re/reception-report/versions/1.0.0/test.json +168 -0
  28. package/data/craftbook-templates/so/social-digest/manifest.json +21 -0
  29. package/data/craftbook-templates/so/social-digest/versions/1.0.0/craftbook.json +133 -0
  30. package/data/craftbook-templates/so/social-digest/versions/1.0.0/test.json +168 -0
  31. package/data/gezel-templates/index.json +1 -1
  32. package/data/gezel-templates/om/omroeper/manifest.json +20 -0
  33. package/data/gezel-templates/om/omroeper/versions/1.0.0/about.md +33 -0
  34. package/data/gezel-templates/om/omroeper/versions/1.0.0/manifest.json +23 -0
  35. package/data/project-types/ca/caregiving-binder/versions/1.1.0/about.md +8 -0
  36. package/data/project-types/ca/caregiving-binder/versions/1.1.0/care.json +4 -0
  37. package/data/project-types/ca/caregiving-binder/versions/1.1.0/manifest.json +178 -0
  38. package/data/project-types/ca/caregiving-binder/versions/1.1.0/meds.json +8 -0
  39. package/data/project-types/ca/caregiving-binder/versions/1.1.0/mission.md +8 -0
  40. package/data/project-types/ca/caregiving-binder/versions/1.1.0/pages/dashboard/index.html +265 -0
  41. package/data/project-types/ch/checkers/versions/1.2.0/about.md +9 -0
  42. package/data/project-types/ch/checkers/versions/1.2.0/game.json +77 -0
  43. package/data/project-types/ch/checkers/versions/1.2.0/manifest.json +167 -0
  44. package/data/project-types/ch/checkers/versions/1.2.0/mission.md +9 -0
  45. package/data/project-types/ch/checkers/versions/1.2.0/pages/board/index.html +1157 -0
  46. package/data/project-types/fl/flashcards/versions/1.1.0/about.md +8 -0
  47. package/data/project-types/fl/flashcards/versions/1.1.0/deck.json +7 -0
  48. package/data/project-types/fl/flashcards/versions/1.1.0/manifest.json +168 -0
  49. package/data/project-types/fl/flashcards/versions/1.1.0/mission.md +6 -0
  50. package/data/project-types/fl/flashcards/versions/1.1.0/pages/review/index.html +476 -0
  51. package/data/project-types/im/image-feed/manifest.json +20 -0
  52. package/data/project-types/im/image-feed/versions/1.0.0/about.md +21 -0
  53. package/data/project-types/im/image-feed/versions/1.0.0/manifest.json +330 -0
  54. package/data/project-types/im/image-feed/versions/1.0.0/mission.md +10 -0
  55. package/data/project-types/im/image-feed/versions/1.0.0/pages/gallery/index.html +832 -0
  56. package/data/project-types/im/image-feed/versions/1.0.0/posts/index.json +4 -0
  57. package/data/project-types/im/image-feed/versions/1.0.0/style-guide.md +24 -0
  58. package/data/project-types/index.json +1 -1
  59. package/data/project-types/so/social-feed/manifest.json +21 -0
  60. package/data/project-types/so/social-feed/versions/1.0.0/about.md +19 -0
  61. package/data/project-types/so/social-feed/versions/1.0.0/manifest.json +336 -0
  62. package/data/project-types/so/social-feed/versions/1.0.0/mission.md +10 -0
  63. package/data/project-types/so/social-feed/versions/1.0.0/pages/dashboard/index.html +756 -0
  64. package/data/project-types/so/social-feed/versions/1.0.0/posts/index.json +4 -0
  65. package/data/project-types/so/social-feed/versions/1.0.0/voice-guide.md +22 -0
  66. package/package.json +1 -1
  67. package/schemas/connector-type-version.schema.json +6 -0
  68. package/schemas/craftbook-test.schema.json +1 -0
  69. package/schemas/project-type-version.schema.json +4 -0
@@ -0,0 +1,265 @@
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>Caregiving binder</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; --warn: #a4552e;
13
+ }
14
+ :root[data-theme='dark'] {
15
+ --bg: #1c1a17; --card: #262320; --ink: #efe9e0; --muted: #a59a8c;
16
+ --accent: #e0894a; --track: #3a352f; --good: #7fae8d; --warn: #d98a63;
17
+ }
18
+ @media (prefers-color-scheme: dark) {
19
+ :root:not([data-theme='light']) {
20
+ --bg: #1c1a17; --card: #262320; --ink: #efe9e0; --muted: #a59a8c;
21
+ --accent: #e0894a; --track: #3a352f; --good: #7fae8d; --warn: #d98a63;
22
+ }
23
+ }
24
+ * { box-sizing: border-box; }
25
+ 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; }
26
+ main { max-width: 680px; margin: 0 auto; }
27
+ h1 { font-family: Georgia, 'PT Serif', 'Times New Roman', serif; font-size: 1.35rem; font-weight: 600; margin: 0 0 0.15rem; }
28
+ .sub { color: var(--muted); font-size: 0.9rem; margin: 0 0 0.9rem; }
29
+ .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; }
30
+ .panel { background: var(--card); border: 1px solid var(--track); border-radius: 10px; padding: 0.85rem 1rem; font-size: 0.9rem; }
31
+ .panel h2 { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin: 0 0 0.5rem; }
32
+ .panel ul { margin: 0; padding-left: 1.05rem; }
33
+ .panel li { margin: 0.25rem 0; }
34
+ .panel .meta { color: var(--muted); font-size: 0.78rem; }
35
+ .journal { margin-top: 0.6rem; }
36
+ .journal li { color: var(--muted); }
37
+ .journal li strong { color: var(--ink); font-weight: 600; }
38
+ .kind { display: inline-block; border: 1px solid var(--track); border-radius: 4px; padding: 0 0.3rem; font-size: 0.7rem; line-height: 1.4; margin-right: 0.3rem; color: var(--muted); }
39
+ .kind.symptom { color: var(--warn); border-color: var(--warn); }
40
+ .empty { color: var(--muted); font-style: italic; }
41
+ .privacy { color: var(--muted); font-size: 0.78rem; text-align: center; margin-top: 0.75rem; }
42
+ @media (max-width: 640px) { .cols { grid-template-columns: 1fr; } }
43
+ .demo-banner {
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: space-between;
47
+ gap: 0.75rem;
48
+ margin: 0 0 0.85rem;
49
+ padding: 0.5rem 0.65rem;
50
+ border: 1px solid var(--accent);
51
+ border-radius: 6px;
52
+ background: color-mix(in srgb, var(--card) 88%, var(--accent));
53
+ color: var(--ink);
54
+ font-size: 0.8rem;
55
+ }
56
+ .demo-banner[hidden] { display: none; }
57
+ .demo-banner strong { font-weight: 700; }
58
+ .demo-reset {
59
+ flex: none;
60
+ padding: 0.25rem 0.5rem;
61
+ border: 1px solid var(--accent);
62
+ border-radius: 6px;
63
+ background: var(--card);
64
+ color: var(--ink);
65
+ cursor: pointer;
66
+ font: inherit;
67
+ }
68
+ .demo-reset:hover { border-color: var(--ink); }
69
+ </style>
70
+ </head>
71
+ <body>
72
+ <main>
73
+ <div class="demo-banner" data-gezel-demo-banner hidden>
74
+ <span><strong>Demo mode</strong> — sample data stays in this page.</span>
75
+ <button type="button" class="demo-reset" onclick="location.reload()">Reset demo</button>
76
+ </div>
77
+ <h1>Caregiving binder</h1>
78
+ <p class="sub" id="sub">Loading the record…</p>
79
+ <div class="cols">
80
+ <div class="panel">
81
+ <h2>Active medications</h2>
82
+ <ul id="meds"><li class="empty">None recorded yet.</li></ul>
83
+ <h2 style="margin-top: 0.9rem">Upcoming</h2>
84
+ <ul id="upcoming"><li class="empty">Nothing scheduled.</li></ul>
85
+ </div>
86
+ <div class="panel">
87
+ <h2>Recent journal</h2>
88
+ <ul class="journal" id="journal"><li class="empty">The journal is empty — a quiet start.</li></ul>
89
+ </div>
90
+ </div>
91
+ <p class="privacy">This record lives on this machine and never leaves it.</p>
92
+ </main>
93
+ <script>
94
+ /* gezel-page-demo-stub v1 */
95
+ /*
96
+ * The canonical demo stub for Gezel type pages. MIT License.
97
+ *
98
+ * Paste this file's bytes verbatim into a <script> block ahead of the
99
+ * page's own code, then call makeDemoGezel({ page, tools, data }) with the
100
+ * page's demo handlers. Served by Gezel, the real window.gezel is already
101
+ * injected and the stub steps aside; opened as a raw file (the live demos
102
+ * on gezelgilde.com), it installs a window.gezel whose tools and data are
103
+ * the page-supplied handler maps — one code path in every mode. `tools`
104
+ * and `data` map a tool name / declared read path to a function; data
105
+ * handlers back read() and list(), and url() returns the handler's value
106
+ * or passes a data: URI through. See docs/page-authoring.md.
107
+ */
108
+ (function () {
109
+ 'use strict';
110
+ window.makeDemoGezel = function (opts) {
111
+ if (window.gezel) return window.gezel;
112
+ var page = (opts && opts.page) || {};
113
+ var tools = (opts && opts.tools) || {};
114
+ var data = (opts && opts.data) || {};
115
+ var themeListeners = [];
116
+ var media = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
117
+ var ui = {
118
+ theme: { mode: media && media.matches ? 'dark' : 'light' },
119
+ onTheme: function (cb) {
120
+ themeListeners.push(cb);
121
+ return function () {
122
+ var at = themeListeners.indexOf(cb);
123
+ if (at >= 0) themeListeners.splice(at, 1);
124
+ };
125
+ },
126
+ };
127
+ if (media && media.addEventListener) {
128
+ media.addEventListener('change', function (ev) {
129
+ ui.theme = { mode: ev.matches ? 'dark' : 'light' };
130
+ themeListeners.slice().forEach(function (cb) { cb(ui.theme); });
131
+ });
132
+ }
133
+ function fail(code, message) {
134
+ var err = new Error(message);
135
+ err.code = code;
136
+ return Promise.reject(err);
137
+ }
138
+ function run(map, key, arg) {
139
+ if (!Object.prototype.hasOwnProperty.call(map, key)) return null;
140
+ return Promise.resolve().then(function () { return map[key](arg); });
141
+ }
142
+ window.gezel = {
143
+ page: {
144
+ api: 1,
145
+ projectId: 'demo',
146
+ source: 'type',
147
+ entry: page.entry || 'index.html',
148
+ typeName: page.typeName || 'demo',
149
+ params: page.params || {},
150
+ mode: 'demo',
151
+ },
152
+ tools: {
153
+ list: function () { return Promise.resolve(Object.keys(tools)); },
154
+ invoke: function (name, input) {
155
+ var call = run(tools, name, input || {});
156
+ if (!call) return fail('not-allowed', "The demo does not handle the tool '" + name + "'.");
157
+ return call.then(function (output) { return { output: output }; });
158
+ },
159
+ },
160
+ data: {
161
+ read: function (path, o) {
162
+ return run(data, path, o || {}) || fail('not-allowed', "The demo has no data for '" + path + "'.");
163
+ },
164
+ list: function (path, o) {
165
+ return run(data, path, o || {}) || Promise.resolve([]);
166
+ },
167
+ watch: function () { return function () {}; },
168
+ url: function (path) {
169
+ if (Object.prototype.hasOwnProperty.call(data, path)) return String(data[path]({}));
170
+ return typeof path === 'string' && path.indexOf('data:') === 0 ? path : '';
171
+ },
172
+ },
173
+ ui: ui,
174
+ refresh: function () {},
175
+ };
176
+ return window.gezel;
177
+ };
178
+ })();
179
+ </script>
180
+ <script>
181
+ (function () {
182
+ 'use strict';
183
+
184
+ // ── demo record (used only when the page opens as a raw file) ──
185
+ var demoNow = Date.now();
186
+ var demoMeds = { records: [
187
+ { id: 'med-1', status: 'active', fields: { name: 'Lisinopril', dose: '10 mg each morning', prescriber: 'Dr. Chen' } },
188
+ { id: 'med-2', status: 'active', fields: { name: 'Vitamin D', dose: '1 tablet with lunch' } },
189
+ ] };
190
+ // Append order, oldest first — the shape logStore writes to care.json.
191
+ var demoCare = { events: [
192
+ { kind: 'update', at: new Date(demoNow - 6 * 86400000).toISOString(), note: 'Blood pressure readings added to the visit notes.' },
193
+ { kind: 'note', at: new Date(demoNow - 3 * 86400000).toISOString(), note: 'Refill collected; pill organizer set for the week.' },
194
+ { kind: 'symptom', at: new Date(demoNow - 86400000).toISOString(), note: 'Energy was better after an afternoon walk.' },
195
+ { kind: 'appointment', at: new Date(demoNow).toISOString(), note: 'Primary care follow-up', data: { whenAt: new Date(demoNow + 12 * 86400000).toISOString() } },
196
+ ] };
197
+
198
+ makeDemoGezel({
199
+ page: { entry: 'dashboard/index.html', typeName: 'Caregiving Binder' },
200
+ data: {
201
+ 'meds.json': function () { return demoMeds; },
202
+ 'care.json': function () { return demoCare; },
203
+ },
204
+ });
205
+
206
+ // ── one code path from here: window.gezel exists in every mode ──
207
+ var demoMode = gezel.page.mode === 'demo';
208
+
209
+ function applyTheme(mode) {
210
+ document.documentElement.setAttribute('data-theme', mode === 'dark' ? 'dark' : 'light');
211
+ }
212
+ applyTheme(gezel.ui.theme.mode);
213
+ gezel.ui.onTheme(function (theme) { applyTheme(theme.mode); });
214
+
215
+ if (demoMode) document.querySelector('[data-gezel-demo-banner]').hidden = false;
216
+
217
+ function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); }
218
+
219
+ function render(meds, care) {
220
+ var active = ((meds && meds.records) || []).filter(function (r) { return r.status === 'active'; });
221
+ var events = (care && care.events) || [];
222
+ var now = Date.now();
223
+ var upcoming = events.filter(function (e) {
224
+ var when = e.data && e.data.whenAt ? Date.parse(e.data.whenAt) : NaN;
225
+ return e.kind === 'appointment' && isFinite(when) && when >= now;
226
+ }).sort(function (a, b) { return Date.parse(a.data.whenAt) - Date.parse(b.data.whenAt); });
227
+
228
+ document.getElementById('sub').textContent =
229
+ active.length + ' active medication(s) · ' + upcoming.length + ' upcoming appointment(s)';
230
+
231
+ if (active.length) {
232
+ document.getElementById('meds').innerHTML = active.map(function (m) {
233
+ var f = m.fields || {};
234
+ return '<li>' + esc(f.name || m.id) + ' <span class="meta">' + esc(f.dose || '') +
235
+ (f.prescriber ? ' · ' + esc(f.prescriber) : '') + '</span></li>';
236
+ }).join('');
237
+ }
238
+ if (upcoming.length) {
239
+ document.getElementById('upcoming').innerHTML = upcoming.slice(0, 4).map(function (e) {
240
+ return '<li>' + esc(String(e.data.whenAt).slice(0, 16).replace('T', ' ')) + ' <span class="meta">' + esc(e.note || '') + '</span></li>';
241
+ }).join('');
242
+ }
243
+ var recent = events.filter(function (e) { return e.kind !== 'appointment'; }).slice(-6).reverse();
244
+ if (recent.length) {
245
+ document.getElementById('journal').innerHTML = recent.map(function (e) {
246
+ return '<li><span class="kind ' + esc(e.kind) + '">' + esc(e.kind) + '</span>' +
247
+ '<strong>' + esc(String(e.at).slice(0, 10)) + '</strong> — ' + esc(e.note || '') + '</li>';
248
+ }).join('');
249
+ }
250
+ }
251
+
252
+ function loadRecord() {
253
+ return Promise.all([
254
+ gezel.data.read('meds.json').catch(function () { return { records: [] }; }),
255
+ gezel.data.read('care.json').catch(function () { return { events: [] }; }),
256
+ ]).then(function (results) { render(results[0], results[1]); });
257
+ }
258
+
259
+ loadRecord();
260
+ gezel.data.watch('meds.json', function () { loadRecord(); });
261
+ gezel.data.watch('care.json', function () { loadRecord(); });
262
+ })();
263
+ </script>
264
+ </body>
265
+ </html>
@@ -0,0 +1,9 @@
1
+ A standing checkers table. You play red on the board in the **Output** tab; your
2
+ Damspeler plays black — watching the position, playing back, and keeping up a
3
+ running line of table talk in chat ({{personality}} style, always friendly).
4
+ The Damspeler plays as an **{{playStyle}}**: opponents keep their strategy to
5
+ themselves, while instructors offer brief teaching observations.
6
+
7
+ Click a red piece, click a highlighted square, and the game is on. The board
8
+ enforces the rules for both sides — the chosen capture rule, multi-jumps, kings
9
+ — so every game is a fair one.
@@ -0,0 +1,77 @@
1
+ {
2
+ "version": 1,
3
+ "squares": ".b.b.b.bb.b.b.b..b.b.b.b................r.r.r.r..r.r.r.rr.r.r.r.",
4
+ "turn": "user",
5
+ "status": "playing",
6
+ "userColor": "red",
7
+ "forceCaptures": true,
8
+ "legalMoves": [
9
+ {
10
+ "from": "a3",
11
+ "to": "b4",
12
+ "path": [
13
+ "a3",
14
+ "b4"
15
+ ],
16
+ "captures": []
17
+ },
18
+ {
19
+ "from": "c3",
20
+ "to": "b4",
21
+ "path": [
22
+ "c3",
23
+ "b4"
24
+ ],
25
+ "captures": []
26
+ },
27
+ {
28
+ "from": "c3",
29
+ "to": "d4",
30
+ "path": [
31
+ "c3",
32
+ "d4"
33
+ ],
34
+ "captures": []
35
+ },
36
+ {
37
+ "from": "e3",
38
+ "to": "d4",
39
+ "path": [
40
+ "e3",
41
+ "d4"
42
+ ],
43
+ "captures": []
44
+ },
45
+ {
46
+ "from": "e3",
47
+ "to": "f4",
48
+ "path": [
49
+ "e3",
50
+ "f4"
51
+ ],
52
+ "captures": []
53
+ },
54
+ {
55
+ "from": "g3",
56
+ "to": "f4",
57
+ "path": [
58
+ "g3",
59
+ "f4"
60
+ ],
61
+ "captures": []
62
+ },
63
+ {
64
+ "from": "g3",
65
+ "to": "h4",
66
+ "path": [
67
+ "g3",
68
+ "h4"
69
+ ],
70
+ "captures": []
71
+ }
72
+ ],
73
+ "moveLog": [],
74
+ "lastMove": null,
75
+ "lastMoveThought": "",
76
+ "updatedAt": "2026-07-18T00:00:00.000Z"
77
+ }
@@ -0,0 +1,167 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "version": "1.2.0",
4
+ "releasedAt": "2026-08-12T00:00:00Z",
5
+ "meesterManaged": false,
6
+ "mode": "solo",
7
+ "leadLabel": "Opponent",
8
+ "leanProfile": true,
9
+ "tabVisibility": {
10
+ "overview": false,
11
+ "tasks": false,
12
+ "approvals": false,
13
+ "workspace": false,
14
+ "artifacts": false,
15
+ "map": false
16
+ },
17
+ "params": {
18
+ "type": "object",
19
+ "properties": {
20
+ "personality": {
21
+ "type": "string",
22
+ "title": "Table-talk style",
23
+ "description": "How your opponent talks between moves.",
24
+ "enum": [
25
+ "peppy",
26
+ "cheeky",
27
+ "zen",
28
+ "gracious"
29
+ ],
30
+ "default": "peppy"
31
+ },
32
+ "playStyle": {
33
+ "type": "string",
34
+ "title": "How should the Damspeler play?",
35
+ "description": "An opponent keeps their strategy to themselves; an instructor offers light teaching notes.",
36
+ "enum": [
37
+ "Opponent",
38
+ "Instructor"
39
+ ],
40
+ "default": "Opponent"
41
+ }
42
+ },
43
+ "required": [
44
+ "personality",
45
+ "playStyle"
46
+ ]
47
+ },
48
+ "nameTemplate": "Checkers vs the Damspeler",
49
+ "aboutTemplate": "about.md",
50
+ "missionTemplate": "mission.md",
51
+ "gezels": [
52
+ {
53
+ "templateId": "checkers-player",
54
+ "voorman": true
55
+ }
56
+ ],
57
+ "scripts": {
58
+ "game-store": "import { type InferredInput, defineScript, gezel } from '@bendyline/gezel-sdk';\n\nexport const meta = defineScript({\n name: 'game-store',\n description:\n \"Read and update the checkers game (game.json). Call action:'board' to see the position, the side to move, and every legal move; action:'ai_move' to play your move (from/to, optional moveThought); action:'state' for a light summary; action:'new_game' to reset, optionally with forceCaptures:false. An illegal ai_move fails with the full legal-move list — pick one of those and call again.\",\n inputs: {\n action: {\n type: 'string',\n description: \"One of 'state', 'board', 'user_move', 'ai_move', or 'new_game'.\",\n required: true,\n },\n from: { type: 'string', description: \"Origin square, e.g. 'c3'.\" },\n to: { type: 'string', description: \"Destination square (a jump sequence's final square).\" },\n moveThought: { type: 'string', description: 'A short line of table talk to show on the board (max 140 chars).' },\n forceCaptures: {\n type: 'boolean',\n description: \"For new_game: true for standard forced captures; false to keep ordinary moves legal when a capture is available.\",\n },\n },\n outputs: {\n board: { type: 'string', description: 'Legacy human-readable ASCII board with coordinates.' },\n modelState: {\n type: 'object',\n description:\n 'Authoritative model-facing state: orientation, square lists, capture availability, and structured legal moves.',\n },\n turn: { type: 'string', description: \"Whose move: 'user' or 'ai'.\" },\n status: { type: 'string', description: 'playing, user_won, or ai_won.' },\n lastMove: { type: 'string', description: \"The move just played, e.g. 'c3-d4' or 'b6xd4'.\" },\n legalMoves: { type: 'string', description: 'Rendered legal moves for the side to move.' },\n captureRule: { type: 'string', description: \"Either 'forced' or 'optional'.\" },\n summary: { type: 'string', description: 'Human-readable game summary.' },\n },\n requires: ['workspace.read', 'workspace.write'],\n} as const);\n\nconst input = gezel.input as InferredInput<typeof meta>;\n\n// ── checkers-rules-start ──\n// Pure, annotation-free rules engine (American/English draughts, 8x8).\n// Squares: 64-char row-major string, index = y*8+x, y0 = rank 8, x0 = file a.\n// 'r'/'R' = red (the user, moving toward rank 8 / decreasing y);\n// 'b'/'B' = black (the AI, moving toward rank 1 / increasing y); '.' empty.\n// Kept self-contained between these sentinels so unit tests execute the\n// shipped bytes directly (new Function) on every platform.\n\nvar FILES = 'abcdefgh';\n\nfunction sqName(x, y) {\n return FILES[x] + String(8 - y);\n}\n\nfunction parseSq(name) {\n if (typeof name !== 'string' || name.length !== 2) return null;\n var x = FILES.indexOf(name[0].toLowerCase());\n var rank = Number(name[1]);\n if (x < 0 || !(rank >= 1 && rank <= 8)) return null;\n return { x: x, y: 8 - rank };\n}\n\nfunction pieceAt(squares, x, y) {\n return squares[y * 8 + x];\n}\n\nfunction withPiece(squares, x, y, ch) {\n var i = y * 8 + x;\n return squares.slice(0, i) + ch + squares.slice(i + 1);\n}\n\nfunction sideOf(ch) {\n if (ch === 'r' || ch === 'R') return 'user';\n if (ch === 'b' || ch === 'B') return 'ai';\n return null;\n}\n\nfunction isKing(ch) {\n return ch === 'R' || ch === 'B';\n}\n\nfunction kingRowY(side) {\n return side === 'user' ? 0 : 7;\n}\n\nfunction dirsFor(ch) {\n if (isKing(ch)) {\n return [\n { dx: -1, dy: -1 },\n { dx: 1, dy: -1 },\n { dx: -1, dy: 1 },\n { dx: 1, dy: 1 },\n ];\n }\n var dy = ch === 'r' ? -1 : 1;\n return [\n { dx: -1, dy: dy },\n { dx: 1, dy: dy },\n ];\n}\n\nfunction inBounds(x, y) {\n return x >= 0 && x < 8 && y >= 0 && y < 8;\n}\n\nfunction buildInitial() {\n var out = '';\n for (var y = 0; y < 8; y += 1) {\n for (var x = 0; x < 8; x += 1) {\n var dark = (x + y) % 2 === 1;\n if (dark && y <= 2) out += 'b';\n else if (dark && y >= 5) out += 'r';\n else out += '.';\n }\n }\n return out;\n}\n\n// All complete jump sequences for the piece at (x, y). A sequence keeps\n// jumping with the same piece while another capture is available; a man\n// that reaches the king row STOPS there (standard rule — the move ends on\n// crowning). Each result: { from, to, path, captures } (square names).\nfunction jumpSequencesFrom(squares, x, y, ch) {\n var side = sideOf(ch);\n var results = [];\n\n function explore(sq, cx, cy, curCh, path, captures) {\n var extended = false;\n var dirs = dirsFor(curCh);\n for (var i = 0; i < dirs.length; i += 1) {\n var mx = cx + dirs[i].dx;\n var my = cy + dirs[i].dy;\n var lx = cx + dirs[i].dx * 2;\n var ly = cy + dirs[i].dy * 2;\n if (!inBounds(lx, ly)) continue;\n var mid = pieceAt(sq, mx, my);\n if (sideOf(mid) === null || sideOf(mid) === side) continue;\n if (pieceAt(sq, lx, ly) !== '.') continue;\n\n var nextCh = curCh;\n var crowned = false;\n if (!isKing(curCh) && ly === kingRowY(side)) {\n nextCh = side === 'user' ? 'R' : 'B';\n crowned = true;\n }\n var nextSq = withPiece(withPiece(withPiece(sq, cx, cy, '.'), mx, my, '.'), lx, ly, nextCh);\n var nextPath = path.concat([sqName(lx, ly)]);\n var nextCaptures = captures.concat([sqName(mx, my)]);\n extended = true;\n if (crowned) {\n results.push({\n from: path[0],\n to: sqName(lx, ly),\n path: nextPath,\n captures: nextCaptures,\n });\n } else {\n explore(nextSq, lx, ly, nextCh, nextPath, nextCaptures);\n }\n }\n if (!extended && captures.length > 0) {\n results.push({ from: path[0], to: path[path.length - 1], path: path, captures: captures });\n }\n }\n\n explore(squares, x, y, ch, [sqName(x, y)], []);\n return results;\n}\n\n// Every legal move for `side`. With standard forced captures, any jump\n// masks quiet moves. In the optional-capture variant, both remain legal.\n// Simple moves are single diagonal steps.\nfunction legalMovesFor(squares, side, forceCaptures) {\n var jumps = [];\n var steps = [];\n for (var y = 0; y < 8; y += 1) {\n for (var x = 0; x < 8; x += 1) {\n var ch = pieceAt(squares, x, y);\n if (sideOf(ch) !== side) continue;\n var seqs = jumpSequencesFrom(squares, x, y, ch);\n for (var j = 0; j < seqs.length; j += 1) jumps.push(seqs[j]);\n var dirs = dirsFor(ch);\n for (var i = 0; i < dirs.length; i += 1) {\n var nx = x + dirs[i].dx;\n var ny = y + dirs[i].dy;\n if (!inBounds(nx, ny) || pieceAt(squares, nx, ny) !== '.') continue;\n steps.push({\n from: sqName(x, y),\n to: sqName(nx, ny),\n path: [sqName(x, y), sqName(nx, ny)],\n captures: [],\n });\n }\n }\n }\n return forceCaptures !== false && jumps.length > 0 ? jumps : jumps.concat(steps);\n}\n\n// Apply a legal sequence, returning the new squares string. Crowns a man\n// that ends on the king row (mid-sequence crowning already ended the\n// sequence in jumpSequencesFrom, so end-of-path crowning is complete).\nfunction applySequence(squares, seq) {\n var start = parseSq(seq.from);\n var end = parseSq(seq.to);\n var ch = pieceAt(squares, start.x, start.y);\n var side = sideOf(ch);\n var next = withPiece(squares, start.x, start.y, '.');\n for (var i = 0; i < seq.captures.length; i += 1) {\n var cap = parseSq(seq.captures[i]);\n next = withPiece(next, cap.x, cap.y, '.');\n }\n var finalCh = ch;\n if (!isKing(ch) && end.y === kingRowY(side)) finalCh = side === 'user' ? 'R' : 'B';\n return withPiece(next, end.x, end.y, finalCh);\n}\n\n// The side to move with no legal move has lost (covers zero pieces too).\nfunction statusAfter(squares, sideToMove, forceCaptures) {\n if (legalMovesFor(squares, sideToMove, forceCaptures).length > 0) return 'playing';\n return sideToMove === 'user' ? 'ai_won' : 'user_won';\n}\n\nfunction describeMove(seq) {\n var joiner = seq.captures.length > 0 ? 'x' : '-';\n var text = seq.path.join(joiner);\n if (seq.captures.length > 0) text += ' (captures ' + seq.captures.join(', ') + ')';\n return text;\n}\n\nfunction renderAscii(squares) {\n var lines = [' a b c d e f g h'];\n for (var y = 0; y < 8; y += 1) {\n var row = String(8 - y);\n for (var x = 0; x < 8; x += 1) {\n row += ' ' + pieceAt(squares, x, y);\n }\n lines.push(row);\n }\n lines.push('r/R = red (your opponent, moving up). b/B = black (you, moving down). Capitals are kings.');\n return lines.join('\\n');\n}\n\nfunction renderPieces(squares) {\n var redMen = [];\n var redKings = [];\n var blackMen = [];\n var blackKings = [];\n for (var y = 0; y < 8; y += 1) {\n for (var x = 0; x < 8; x += 1) {\n var ch = pieceAt(squares, x, y);\n var name = sqName(x, y);\n if (ch === 'r') redMen.push(name);\n else if (ch === 'R') redKings.push(name);\n else if (ch === 'b') blackMen.push(name);\n else if (ch === 'B') blackKings.push(name);\n }\n }\n return { redMen: redMen, redKings: redKings, blackMen: blackMen, blackKings: blackKings };\n}\n// ── checkers-rules-end ──\n\nconst STATE_FILE = 'game.json';\nconst STAGE_SIDES = { user: 'red', ai: 'black' };\n\ninterface GameState {\n version: number;\n squares: string;\n turn: 'user' | 'ai';\n status: 'playing' | 'user_won' | 'ai_won' | 'draw';\n userColor: string;\n forceCaptures: boolean;\n legalMoves: Array<{ from: string; to: string; path: string[]; captures: string[] }>;\n moveLog: Array<{\n by: 'user' | 'ai';\n from: string;\n to: string;\n captures: string[];\n at: string;\n moveThought?: string;\n }>;\n lastMove: { by: 'user' | 'ai'; from: string; to: string; captures: string[] } | null;\n lastMoveThought: string;\n updatedAt: string;\n}\n\nfunction freshState(forceCaptures = true): GameState {\n const squares = buildInitial();\n return {\n version: 1,\n squares,\n turn: 'user',\n status: 'playing',\n userColor: 'red',\n forceCaptures,\n legalMoves: legalMovesFor(squares, 'user', forceCaptures),\n moveLog: [],\n lastMove: null,\n lastMoveThought: '',\n updatedAt: new Date().toISOString(),\n };\n}\n\nasync function load(): Promise<GameState> {\n let raw: string;\n try {\n raw = await gezel.fs.read(STATE_FILE);\n } catch {\n return freshState();\n }\n const parsed = JSON.parse(raw) as Partial<GameState>;\n if (typeof parsed.squares !== 'string' || parsed.squares.length !== 64) {\n throw new Error(`${STATE_FILE} is corrupt (squares missing); call action:'new_game' to reset.`);\n }\n return { ...freshState(parsed.forceCaptures !== false), ...parsed, forceCaptures: parsed.forceCaptures !== false } as GameState;\n}\n\nasync function persist(state: GameState): Promise<GameState> {\n state.legalMoves = state.status === 'playing' ? legalMovesFor(state.squares, state.turn, state.forceCaptures) : [];\n state.updatedAt = new Date().toISOString();\n await gezel.fs.write(STATE_FILE, `${JSON.stringify(state, null, 2)}\\n`);\n return state;\n}\n\nfunction summarize(state: GameState): string {\n const counts = { r: 0, b: 0 };\n for (const ch of state.squares) {\n if (ch === 'r' || ch === 'R') counts.r += 1;\n if (ch === 'b' || ch === 'B') counts.b += 1;\n }\n if (state.status === 'user_won') return `Game over — the user (red) won. Red ${counts.r} pieces, black ${counts.b}.`;\n if (state.status === 'ai_won') return `Game over — you (black) won. Red ${counts.r} pieces, black ${counts.b}.`;\n return `Move ${state.moveLog.length + 1}: ${STAGE_SIDES[state.turn]} (${state.turn}) to move. Red ${counts.r} pieces, black ${counts.b}. Captures are ${state.forceCaptures ? 'forced' : 'optional'}.`;\n}\n\nfunction renderLegal(state: GameState): string {\n if (state.status !== 'playing') return '(game over)';\n return state.legalMoves.map(describeMove).join(', ') || '(none)';\n}\n\nfunction renderModelState(state: GameState) {\n return {\n status: state.status,\n sideToMove: STAGE_SIDES[state.turn],\n coordinateFormat: 'file-rank (for example e3)',\n orientation: { redForwardRankDelta: 1, blackForwardRankDelta: -1 },\n pieces: renderPieces(state.squares),\n capturePolicy: state.forceCaptures ? 'mandatory-if-available' : 'optional',\n capturesAvailable: state.legalMoves.some((move) => move.captures.length > 0),\n legalMoves: state.legalMoves.map((move) => ({\n from: move.from,\n to: move.to,\n path: move.path,\n captures: move.captures,\n type: move.captures.length > 0 ? 'capture' : 'step',\n })),\n };\n}\n\nfunction applyTurn(state: GameState, by: 'user' | 'ai', moveThought?: string): GameState {\n if (state.status !== 'playing') {\n throw new Error(`The game is over (${state.status}). Call action:'new_game' to start another.`);\n }\n if (state.turn !== by) {\n throw new Error(\n by === 'ai'\n ? \"It's not your turn — the user has not moved yet.\"\n : \"It's not your turn — the AI is still to move.\",\n );\n }\n if (!input.from || !input.to) throw new Error(\"Pass 'from' and 'to' squares, e.g. from:'c3' to:'d4'.\");\n const seq = state.legalMoves.find(\n (m) => m.from === input.from!.toLowerCase() && m.to === input.to!.toLowerCase(),\n );\n if (!seq) {\n throw new Error(\n `Illegal move ${input.from}-${input.to}. Legal moves for ${STAGE_SIDES[by]}: ${renderLegal(state)}`,\n );\n }\n state.squares = applySequence(state.squares, seq);\n const nextTurn = by === 'user' ? 'ai' : 'user';\n state.status = statusAfter(state.squares, nextTurn, state.forceCaptures) as GameState['status'];\n state.turn = nextTurn;\n const entry = {\n by,\n from: seq.from,\n to: seq.to,\n captures: seq.captures,\n at: new Date().toISOString(),\n ...(moveThought ? { moveThought } : {}),\n };\n state.moveLog.push(entry);\n state.lastMove = { by, from: seq.from, to: seq.to, captures: seq.captures };\n if (by === 'ai' && moveThought) state.lastMoveThought = moveThought;\n return state;\n}\n\nlet state = await load();\nlet lastMoveText = state.lastMove\n ? describeMove({ ...state.lastMove, path: [state.lastMove.from, state.lastMove.to] })\n : '(none)';\n\nif (input.action === 'new_game') {\n state = await persist(freshState(input.forceCaptures !== false));\n lastMoveText = '(none)';\n} else if (input.action === 'user_move' || input.action === 'ai_move') {\n const by = input.action === 'user_move' ? 'user' : 'ai';\n const moveThought = input.moveThought ? input.moveThought.trim().slice(0, 140) : undefined;\n const before = state.legalMoves.find(\n (m) => m.from === (input.from ?? '').toLowerCase() && m.to === (input.to ?? '').toLowerCase(),\n );\n applyTurn(state, by, moveThought);\n state = await persist(state);\n lastMoveText = before ? describeMove(before) : `${input.from}-${input.to}`;\n} else if (input.action !== 'state' && input.action !== 'board') {\n throw new Error(`Unknown action '${input.action}'. Use state, board, user_move, ai_move, or new_game.`);\n}\n\ngezel.output({\n board:\n input.action === 'state'\n ? `${renderAscii(state.squares).split('\\n').slice(0, 9).join('\\n')}`\n : `${renderAscii(state.squares)}\\nCapture rule: ${state.forceCaptures ? 'forced' : 'optional'}.\\nTurn: ${STAGE_SIDES[state.turn]} (${state.turn}). Legal moves: ${renderLegal(state)}`,\n modelState: renderModelState(state),\n turn: state.turn,\n status: state.status,\n lastMove: lastMoveText,\n legalMoves: renderLegal(state),\n captureRule: state.forceCaptures ? 'forced' : 'optional',\n summary: summarize(state),\n});\n"
59
+ },
60
+ "tools": [
61
+ {
62
+ "name": "get_board",
63
+ "description": "See the authoritative structured checkers position, whose turn it is, capture availability, and every legal move. A legacy ASCII board is included for human readability.",
64
+ "script": "game-store",
65
+ "inputs": {
66
+ "type": "object",
67
+ "properties": {}
68
+ },
69
+ "bind": {
70
+ "action": "board"
71
+ }
72
+ },
73
+ {
74
+ "name": "make_move",
75
+ "description": "Play your (black's) move. Pass the from/to squares of a legal move; include a short moveThought to show on the board.",
76
+ "script": "game-store",
77
+ "inputs": {
78
+ "type": "object",
79
+ "properties": {
80
+ "from": {
81
+ "type": "string",
82
+ "description": "Origin square, e.g. 'b6'."
83
+ },
84
+ "to": {
85
+ "type": "string",
86
+ "description": "Destination square (a jump's final landing square)."
87
+ },
88
+ "moveThought": {
89
+ "type": "string",
90
+ "description": "A short line of table talk (max 140 chars)."
91
+ }
92
+ },
93
+ "required": [
94
+ "from",
95
+ "to"
96
+ ]
97
+ },
98
+ "bind": {
99
+ "action": "ai_move"
100
+ }
101
+ },
102
+ {
103
+ "name": "user_move",
104
+ "description": "The user's move, played from the board page.",
105
+ "script": "game-store",
106
+ "inputs": {
107
+ "type": "object",
108
+ "properties": {
109
+ "from": {
110
+ "type": "string"
111
+ },
112
+ "to": {
113
+ "type": "string"
114
+ }
115
+ },
116
+ "required": [
117
+ "from",
118
+ "to"
119
+ ]
120
+ },
121
+ "bind": {
122
+ "action": "user_move"
123
+ },
124
+ "reaction": {
125
+ "gezel": "checkers-player",
126
+ "hideSeed": true,
127
+ "prompt": "Your opponent played {{output.lastMove}}. The game engine returned this authoritative state:\n```json\n{{output.modelState}}\n```\nEvery `legalMoves` entry is legal. Its `type` and `captures` fields explicitly say whether it is a jump. Do not redraw the position as a grid, re-derive legality, recalculate capture availability, recount pieces, or question the stated orientation. Spend any thinking only on which listed move is strongest.\nStyle: {{playStyle}}. In Opponent style, do not volunteer advice or reveal the user's available moves, captures, threats, or tactics; comment only about your own move or the atmosphere. In Instructor style, offer at most one brief teaching observation without choosing the user's move. Answer strategy questions when asked.\nAct on the state above. If the game is over, do not move — one gracious line and offer new_game. Otherwise it is your turn: choose one listed move and call make_move with its `from` and `to` strings, including a short {{personality}} moveThought that follows the selected style. If make_move rejects it, choose one move from the legal list in the error and try again. After moving, reply with ONE short line of table talk that follows the selected style."
128
+ }
129
+ },
130
+ {
131
+ "name": "new_game",
132
+ "description": "Reset the board to the opening position (user moves first).",
133
+ "script": "game-store",
134
+ "inputs": {
135
+ "type": "object",
136
+ "properties": {
137
+ "forceCaptures": {
138
+ "type": "boolean",
139
+ "description": "Whether captures are required when available. Defaults to true."
140
+ }
141
+ }
142
+ },
143
+ "bind": {
144
+ "action": "new_game"
145
+ }
146
+ }
147
+ ],
148
+ "craftbooks": [],
149
+ "schedules": [],
150
+ "pages": {
151
+ "entry": "board/index.html",
152
+ "api": 1,
153
+ "reads": [
154
+ {
155
+ "source": "workspace",
156
+ "path": "game.json"
157
+ }
158
+ ],
159
+ "tools": [
160
+ "user_move",
161
+ "new_game"
162
+ ]
163
+ },
164
+ "workspaceSeed": [
165
+ "game.json"
166
+ ]
167
+ }
@@ -0,0 +1,9 @@
1
+ - Keep a game always ready: the board opens playable, and a new game with
2
+ required or optional captures is always close at hand.
3
+ - The Damspeler answers every user move with a legal move of their own — no
4
+ stalled turns.
5
+ - Table talk stays {{personality}} and kind: tease the position, never the
6
+ person.
7
+ - Match the chosen {{playStyle}} role: an Opponent does not volunteer hints or
8
+ reveal the user's replies; an Instructor teaches lightly without taking over.
9
+ - Wins and losses both end warmly, with the offer of a rematch.