dreamcontext 0.5.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +7 -0
  3. package/README.md +77 -12
  4. package/agents/sleep-product.md +29 -8
  5. package/agents/sleep-tasks.md +44 -9
  6. package/dist/agents/sleep-product.md +29 -8
  7. package/dist/agents/sleep-tasks.md +44 -9
  8. package/dist/dashboard/assets/{BrainCanvas3D-1HMiA14F.js → BrainCanvas3D-LLqVeXtb.js} +1 -1
  9. package/dist/dashboard/assets/{_baseUniq-C7pfRmXz.js → _baseUniq-DW0uA0ty.js} +1 -1
  10. package/dist/dashboard/assets/{arc-B5x_GF9Q.js → arc-fVUGtlbL.js} +1 -1
  11. package/dist/dashboard/assets/{architectureDiagram-Q4EWVU46-CWmjaqno.js → architectureDiagram-Q4EWVU46-Dz2PKBsS.js} +1 -1
  12. package/dist/dashboard/assets/{blockDiagram-DXYQGD6D-B7SyUIdV.js → blockDiagram-DXYQGD6D-ByWKxzhL.js} +1 -1
  13. package/dist/dashboard/assets/{c4Diagram-AHTNJAMY-OeJBWXHU.js → c4Diagram-AHTNJAMY-dpHsVM3D.js} +1 -1
  14. package/dist/dashboard/assets/channel-zrLwggBX.js +1 -0
  15. package/dist/dashboard/assets/{chunk-4BX2VUAB-2Oop1B0M.js → chunk-4BX2VUAB-HEXb6Yg5.js} +1 -1
  16. package/dist/dashboard/assets/{chunk-4TB4RGXK-Bh7qSu_X.js → chunk-4TB4RGXK-DeVy5g6H.js} +1 -1
  17. package/dist/dashboard/assets/{chunk-55IACEB6-gNeHRO_i.js → chunk-55IACEB6-DGy3ZgDZ.js} +1 -1
  18. package/dist/dashboard/assets/{chunk-EDXVE4YY-BRniAH53.js → chunk-EDXVE4YY-CYJehjz4.js} +1 -1
  19. package/dist/dashboard/assets/{chunk-FMBD7UC4-B5r8U2gJ.js → chunk-FMBD7UC4-DKTOmvgZ.js} +1 -1
  20. package/dist/dashboard/assets/{chunk-OYMX7WX6-Dd1c17fm.js → chunk-OYMX7WX6-mmtWyiDA.js} +1 -1
  21. package/dist/dashboard/assets/{chunk-QZHKN3VN-BIw-OdTn.js → chunk-QZHKN3VN-DSdV0iwY.js} +1 -1
  22. package/dist/dashboard/assets/{chunk-YZCP3GAM-Dl9MlwCW.js → chunk-YZCP3GAM-DbY-KoDn.js} +1 -1
  23. package/dist/dashboard/assets/classDiagram-6PBFFD2Q-BvejNiwH.js +1 -0
  24. package/dist/dashboard/assets/classDiagram-v2-HSJHXN6E-BvejNiwH.js +1 -0
  25. package/dist/dashboard/assets/clone-BFVdml6g.js +1 -0
  26. package/dist/dashboard/assets/{cose-bilkent-S5V4N54A-DlrROH5K.js → cose-bilkent-S5V4N54A-p0t1DY88.js} +1 -1
  27. package/dist/dashboard/assets/{dagre-KV5264BT-B0GpPXKL.js → dagre-KV5264BT-DYE0WzHM.js} +1 -1
  28. package/dist/dashboard/assets/{diagram-5BDNPKRD-BBYcDW2Z.js → diagram-5BDNPKRD-D9EiQCOP.js} +1 -1
  29. package/dist/dashboard/assets/{diagram-G4DWMVQ6-BxHxugdp.js → diagram-G4DWMVQ6-poDXSAfu.js} +1 -1
  30. package/dist/dashboard/assets/{diagram-MMDJMWI5-C9Jc19Wo.js → diagram-MMDJMWI5-CHZ7wgN1.js} +1 -1
  31. package/dist/dashboard/assets/{diagram-TYMM5635-Vmo5GAAU.js → diagram-TYMM5635-DVk6fYG7.js} +1 -1
  32. package/dist/dashboard/assets/{erDiagram-SMLLAGMA-pskr9x-V.js → erDiagram-SMLLAGMA-D2sqkGin.js} +1 -1
  33. package/dist/dashboard/assets/{flowDiagram-DWJPFMVM-B1D7Ky0W.js → flowDiagram-DWJPFMVM-DrMlKBYA.js} +1 -1
  34. package/dist/dashboard/assets/{ganttDiagram-T4ZO3ILL-BVJZZV1M.js → ganttDiagram-T4ZO3ILL-Cf4FbEFp.js} +1 -1
  35. package/dist/dashboard/assets/{gitGraphDiagram-UUTBAWPF-BiOGN6We.js → gitGraphDiagram-UUTBAWPF-BsbOq1C9.js} +1 -1
  36. package/dist/dashboard/assets/{graph-D58deEXr.js → graph-CUxxgCtS.js} +1 -1
  37. package/dist/dashboard/assets/index-CqSkXBSu.css +1 -0
  38. package/dist/dashboard/assets/index-flRpQtDj.js +476 -0
  39. package/dist/dashboard/assets/{infoDiagram-42DDH7IO-DgJYg1X1.js → infoDiagram-42DDH7IO-DTkMnZiD.js} +1 -1
  40. package/dist/dashboard/assets/{ishikawaDiagram-UXIWVN3A-arJgr-8P.js → ishikawaDiagram-UXIWVN3A-CahQ348K.js} +1 -1
  41. package/dist/dashboard/assets/{journeyDiagram-VCZTEJTY-B3DCvb8p.js → journeyDiagram-VCZTEJTY-BDY2Nslc.js} +1 -1
  42. package/dist/dashboard/assets/{kanban-definition-6JOO6SKY-B--dN6Ma.js → kanban-definition-6JOO6SKY-CgrwoTjI.js} +1 -1
  43. package/dist/dashboard/assets/{layout-z2qEbZWC.js → layout-IaUxkFkm.js} +1 -1
  44. package/dist/dashboard/assets/{linear-BQu1dwrM.js → linear-shc0iNFn.js} +1 -1
  45. package/dist/dashboard/assets/{min-DkF5b2sh.js → min-BIL7YgTN.js} +1 -1
  46. package/dist/dashboard/assets/{mindmap-definition-QFDTVHPH-Bw04koyq.js → mindmap-definition-QFDTVHPH-GmO0JRtA.js} +1 -1
  47. package/dist/dashboard/assets/{pieDiagram-DEJITSTG-vNqz64o8.js → pieDiagram-DEJITSTG-Bfm_toYA.js} +1 -1
  48. package/dist/dashboard/assets/{quadrantDiagram-34T5L4WZ-TuVP5Gi6.js → quadrantDiagram-34T5L4WZ-DIjTM3lm.js} +1 -1
  49. package/dist/dashboard/assets/{requirementDiagram-MS252O5E-D3mS8_ke.js → requirementDiagram-MS252O5E-B9AEoGYh.js} +1 -1
  50. package/dist/dashboard/assets/{sankeyDiagram-XADWPNL6-BmVRers6.js → sankeyDiagram-XADWPNL6-C_O5XLXm.js} +1 -1
  51. package/dist/dashboard/assets/{sequenceDiagram-FGHM5R23-CTYCgk5P.js → sequenceDiagram-FGHM5R23-CHSxSvmJ.js} +1 -1
  52. package/dist/dashboard/assets/{stateDiagram-FHFEXIEX-DNbP2aCg.js → stateDiagram-FHFEXIEX-B0URrhHw.js} +1 -1
  53. package/dist/dashboard/assets/stateDiagram-v2-QKLJ7IA2-FdAb9MBo.js +1 -0
  54. package/dist/dashboard/assets/{timeline-definition-GMOUNBTQ-DKHJd4z8.js → timeline-definition-GMOUNBTQ-CycU2gVC.js} +1 -1
  55. package/dist/dashboard/assets/{vennDiagram-DHZGUBPP-ChotNMNq.js → vennDiagram-DHZGUBPP-DcU2536G.js} +1 -1
  56. package/dist/dashboard/assets/{wardley-RL74JXVD-Dmo2_ksP.js → wardley-RL74JXVD-BWqqaX3S.js} +1 -1
  57. package/dist/dashboard/assets/{wardleyDiagram-NUSXRM2D-Z6Wbiv2H.js → wardleyDiagram-NUSXRM2D-CUnIHJgd.js} +1 -1
  58. package/dist/dashboard/assets/{xychartDiagram-5P7HB3ND-BJa0AHtN.js → xychartDiagram-5P7HB3ND-b9fcOYTj.js} +1 -1
  59. package/dist/dashboard/index.html +2 -2
  60. package/dist/dashboard/media/README.md +29 -0
  61. package/dist/dashboard/media/brain-hero.png +0 -0
  62. package/dist/dashboard/media/brain.mp4 +0 -0
  63. package/dist/dashboard/media/brain.webm +0 -0
  64. package/dist/dashboard/media/shot-disabled.png +0 -0
  65. package/dist/dashboard/media/shot-enabled.png +0 -0
  66. package/dist/index.js +2280 -1568
  67. package/dist/skill-packs/catalog.json +44 -0
  68. package/dist/skill-packs/excalidraw/SKILL.md +123 -0
  69. package/dist/skill-packs/excalidraw/examples/hello.spec.json +14 -0
  70. package/dist/skill-packs/excalidraw/examples/sample.png +0 -0
  71. package/dist/skill-packs/excalidraw/examples/style_board.js +31 -0
  72. package/dist/skill-packs/excalidraw/package.json +5 -0
  73. package/dist/skill-packs/excalidraw/reference/format.md +93 -0
  74. package/dist/skill-packs/excalidraw/scripts/build_excalidraw.js +357 -0
  75. package/dist/skill-packs/excalidraw/scripts/lib/fractional-indexing.LICENSE +121 -0
  76. package/dist/skill-packs/excalidraw/scripts/lib/fractional-indexing.js +311 -0
  77. package/dist/skill-packs/excalidraw/scripts/lib/imagesize.js +65 -0
  78. package/dist/skill-packs/excalidraw/scripts/lib/style.js +130 -0
  79. package/dist/skill-packs/video-watching/SKILL.md +151 -0
  80. package/dist/skill-packs/video-watching/scripts/build_frame_index.py +95 -0
  81. package/dist/skill-packs/video-watching/scripts/gap_fill.py +45 -0
  82. package/dist/skill-packs/video-watching/scripts/transcribe.sh +136 -0
  83. package/package.json +4 -2
  84. package/skill/SKILL.md +5 -0
  85. package/skill-packs/catalog.json +44 -0
  86. package/skill-packs/excalidraw/SKILL.md +123 -0
  87. package/skill-packs/excalidraw/examples/hello.spec.json +14 -0
  88. package/skill-packs/excalidraw/examples/sample.png +0 -0
  89. package/skill-packs/excalidraw/examples/style_board.js +31 -0
  90. package/skill-packs/excalidraw/package.json +5 -0
  91. package/skill-packs/excalidraw/reference/format.md +93 -0
  92. package/skill-packs/excalidraw/scripts/build_excalidraw.js +357 -0
  93. package/skill-packs/excalidraw/scripts/lib/fractional-indexing.LICENSE +121 -0
  94. package/skill-packs/excalidraw/scripts/lib/fractional-indexing.js +311 -0
  95. package/skill-packs/excalidraw/scripts/lib/imagesize.js +65 -0
  96. package/skill-packs/excalidraw/scripts/lib/style.js +130 -0
  97. package/skill-packs/video-watching/SKILL.md +151 -0
  98. package/skill-packs/video-watching/scripts/build_frame_index.py +95 -0
  99. package/skill-packs/video-watching/scripts/gap_fill.py +45 -0
  100. package/skill-packs/video-watching/scripts/transcribe.sh +136 -0
  101. package/dist/dashboard/assets/channel-CbuTKKsM.js +0 -1
  102. package/dist/dashboard/assets/classDiagram-6PBFFD2Q-HXl3A_sM.js +0 -1
  103. package/dist/dashboard/assets/classDiagram-v2-HSJHXN6E-HXl3A_sM.js +0 -1
  104. package/dist/dashboard/assets/clone-NKHOtdi8.js +0 -1
  105. package/dist/dashboard/assets/index-DK_2-eWY.css +0 -1
  106. package/dist/dashboard/assets/index-Ds-JXWtr.js +0 -476
  107. package/dist/dashboard/assets/stateDiagram-v2-QKLJ7IA2-CfN6kjo8.js +0 -1
@@ -0,0 +1,311 @@
1
+ // License: CC0 (no rights reserved).
2
+
3
+ // This is based on https://observablehq.com/@dgreensp/implementing-fractional-indexing
4
+
5
+ const BASE_62_DIGITS =
6
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
7
+
8
+ // `a` may be empty string, `b` is null or non-empty string.
9
+ // `a < b` lexicographically if `b` is non-null.
10
+ // no trailing zeros allowed.
11
+ // digits is a string such as '0123456789' for base 10. Digits must be in
12
+ // ascending character code order!
13
+ /**
14
+ * @param {string} a
15
+ * @param {string | null | undefined} b
16
+ * @param {string} digits
17
+ * @returns {string}
18
+ */
19
+ function midpoint(a, b, digits) {
20
+ const zero = digits[0];
21
+ if (b != null && a >= b) {
22
+ throw new Error(a + " >= " + b);
23
+ }
24
+ if (a.slice(-1) === zero || (b && b.slice(-1) === zero)) {
25
+ throw new Error("trailing zero");
26
+ }
27
+ if (b) {
28
+ // remove longest common prefix. pad `a` with 0s as we
29
+ // go. note that we don't need to pad `b`, because it can't
30
+ // end before `a` while traversing the common prefix.
31
+ let n = 0;
32
+ while ((a[n] || zero) === b[n]) {
33
+ n++;
34
+ }
35
+ if (n > 0) {
36
+ return b.slice(0, n) + midpoint(a.slice(n), b.slice(n), digits);
37
+ }
38
+ }
39
+ // first digits (or lack of digit) are different
40
+ const digitA = a ? digits.indexOf(a[0]) : 0;
41
+ const digitB = b != null ? digits.indexOf(b[0]) : digits.length;
42
+ if (digitB - digitA > 1) {
43
+ const midDigit = Math.round(0.5 * (digitA + digitB));
44
+ return digits[midDigit];
45
+ } else {
46
+ // first digits are consecutive
47
+ if (b && b.length > 1) {
48
+ return b.slice(0, 1);
49
+ } else {
50
+ // `b` is null or has length 1 (a single digit).
51
+ // the first digit of `a` is the previous digit to `b`,
52
+ // or 9 if `b` is null.
53
+ // given, for example, midpoint('49', '5'), return
54
+ // '4' + midpoint('9', null), which will become
55
+ // '4' + '9' + midpoint('', null), which is '495'
56
+ return digits[digitA] + midpoint(a.slice(1), null, digits);
57
+ }
58
+ }
59
+ }
60
+
61
+ /**
62
+ * @param {string} int
63
+ * @return {void}
64
+ */
65
+
66
+ function validateInteger(int) {
67
+ if (int.length !== getIntegerLength(int[0])) {
68
+ throw new Error("invalid integer part of order key: " + int);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * @param {string} head
74
+ * @return {number}
75
+ */
76
+
77
+ function getIntegerLength(head) {
78
+ if (head >= "a" && head <= "z") {
79
+ return head.charCodeAt(0) - "a".charCodeAt(0) + 2;
80
+ } else if (head >= "A" && head <= "Z") {
81
+ return "Z".charCodeAt(0) - head.charCodeAt(0) + 2;
82
+ } else {
83
+ throw new Error("invalid order key head: " + head);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * @param {string} key
89
+ * @return {string}
90
+ */
91
+
92
+ function getIntegerPart(key) {
93
+ const integerPartLength = getIntegerLength(key[0]);
94
+ if (integerPartLength > key.length) {
95
+ throw new Error("invalid order key: " + key);
96
+ }
97
+ return key.slice(0, integerPartLength);
98
+ }
99
+
100
+ /**
101
+ * @param {string} key
102
+ * @param {string} digits
103
+ * @return {void}
104
+ */
105
+
106
+ function validateOrderKey(key, digits) {
107
+ if (key === "A" + digits[0].repeat(26)) {
108
+ throw new Error("invalid order key: " + key);
109
+ }
110
+ // getIntegerPart will throw if the first character is bad,
111
+ // or the key is too short. we'd call it to check these things
112
+ // even if we didn't need the result
113
+ const i = getIntegerPart(key);
114
+ const f = key.slice(i.length);
115
+ if (f.slice(-1) === digits[0]) {
116
+ throw new Error("invalid order key: " + key);
117
+ }
118
+ }
119
+
120
+ // note that this may return null, as there is a largest integer
121
+ /**
122
+ * @param {string} x
123
+ * @param {string} digits
124
+ * @return {string | null}
125
+ */
126
+ function incrementInteger(x, digits) {
127
+ validateInteger(x);
128
+ const [head, ...digs] = x.split("");
129
+ let carry = true;
130
+ for (let i = digs.length - 1; carry && i >= 0; i--) {
131
+ const d = digits.indexOf(digs[i]) + 1;
132
+ if (d === digits.length) {
133
+ digs[i] = digits[0];
134
+ } else {
135
+ digs[i] = digits[d];
136
+ carry = false;
137
+ }
138
+ }
139
+ if (carry) {
140
+ if (head === "Z") {
141
+ return "a" + digits[0];
142
+ }
143
+ if (head === "z") {
144
+ return null;
145
+ }
146
+ const h = String.fromCharCode(head.charCodeAt(0) + 1);
147
+ if (h > "a") {
148
+ digs.push(digits[0]);
149
+ } else {
150
+ digs.pop();
151
+ }
152
+ return h + digs.join("");
153
+ } else {
154
+ return head + digs.join("");
155
+ }
156
+ }
157
+
158
+ // note that this may return null, as there is a smallest integer
159
+ /**
160
+ * @param {string} x
161
+ * @param {string} digits
162
+ * @return {string | null}
163
+ */
164
+
165
+ function decrementInteger(x, digits) {
166
+ validateInteger(x);
167
+ const [head, ...digs] = x.split("");
168
+ let borrow = true;
169
+ for (let i = digs.length - 1; borrow && i >= 0; i--) {
170
+ const d = digits.indexOf(digs[i]) - 1;
171
+ if (d === -1) {
172
+ digs[i] = digits.slice(-1);
173
+ } else {
174
+ digs[i] = digits[d];
175
+ borrow = false;
176
+ }
177
+ }
178
+ if (borrow) {
179
+ if (head === "a") {
180
+ return "Z" + digits.slice(-1);
181
+ }
182
+ if (head === "A") {
183
+ return null;
184
+ }
185
+ const h = String.fromCharCode(head.charCodeAt(0) - 1);
186
+ if (h < "Z") {
187
+ digs.push(digits.slice(-1));
188
+ } else {
189
+ digs.pop();
190
+ }
191
+ return h + digs.join("");
192
+ } else {
193
+ return head + digs.join("");
194
+ }
195
+ }
196
+
197
+ // `a` is an order key or null (START).
198
+ // `b` is an order key or null (END).
199
+ // `a < b` lexicographically if both are non-null.
200
+ // digits is a string such as '0123456789' for base 10. Digits must be in
201
+ // ascending character code order!
202
+ /**
203
+ * @param {string | null | undefined} a
204
+ * @param {string | null | undefined} b
205
+ * @param {string=} digits
206
+ * @return {string}
207
+ */
208
+ function generateKeyBetween(a, b, digits = BASE_62_DIGITS) {
209
+ if (a != null) {
210
+ validateOrderKey(a, digits);
211
+ }
212
+ if (b != null) {
213
+ validateOrderKey(b, digits);
214
+ }
215
+ if (a != null && b != null && a >= b) {
216
+ throw new Error(a + " >= " + b);
217
+ }
218
+ if (a == null) {
219
+ if (b == null) {
220
+ return "a" + digits[0];
221
+ }
222
+
223
+ const ib = getIntegerPart(b);
224
+ const fb = b.slice(ib.length);
225
+ if (ib === "A" + digits[0].repeat(26)) {
226
+ return ib + midpoint("", fb, digits);
227
+ }
228
+ if (ib < b) {
229
+ return ib;
230
+ }
231
+ const res = decrementInteger(ib, digits);
232
+ if (res == null) {
233
+ throw new Error("cannot decrement any more");
234
+ }
235
+ return res;
236
+ }
237
+
238
+ if (b == null) {
239
+ const ia = getIntegerPart(a);
240
+ const fa = a.slice(ia.length);
241
+ const i = incrementInteger(ia, digits);
242
+ return i == null ? ia + midpoint(fa, null, digits) : i;
243
+ }
244
+
245
+ const ia = getIntegerPart(a);
246
+ const fa = a.slice(ia.length);
247
+ const ib = getIntegerPart(b);
248
+ const fb = b.slice(ib.length);
249
+ if (ia === ib) {
250
+ return ia + midpoint(fa, fb, digits);
251
+ }
252
+ const i = incrementInteger(ia, digits);
253
+ if (i == null) {
254
+ throw new Error("cannot increment any more");
255
+ }
256
+ if (i < b) {
257
+ return i;
258
+ }
259
+ return ia + midpoint(fa, null, digits);
260
+ }
261
+
262
+ /**
263
+ * same preconditions as generateKeysBetween.
264
+ * n >= 0.
265
+ * Returns an array of n distinct keys in sorted order.
266
+ * If a and b are both null, returns [a0, a1, ...]
267
+ * If one or the other is null, returns consecutive "integer"
268
+ * keys. Otherwise, returns relatively short keys between
269
+ * a and b.
270
+ * @param {string | null | undefined} a
271
+ * @param {string | null | undefined} b
272
+ * @param {number} n
273
+ * @param {string} digits
274
+ * @return {string[]}
275
+ */
276
+ function generateNKeysBetween(a, b, n, digits = BASE_62_DIGITS) {
277
+ if (n === 0) {
278
+ return [];
279
+ }
280
+ if (n === 1) {
281
+ return [generateKeyBetween(a, b, digits)];
282
+ }
283
+ if (b == null) {
284
+ let c = generateKeyBetween(a, b, digits);
285
+ const result = [c];
286
+ for (let i = 0; i < n - 1; i++) {
287
+ c = generateKeyBetween(c, b, digits);
288
+ result.push(c);
289
+ }
290
+ return result;
291
+ }
292
+ if (a == null) {
293
+ let c = generateKeyBetween(a, b, digits);
294
+ const result = [c];
295
+ for (let i = 0; i < n - 1; i++) {
296
+ c = generateKeyBetween(a, c, digits);
297
+ result.push(c);
298
+ }
299
+ result.reverse();
300
+ return result;
301
+ }
302
+ const mid = Math.floor(n / 2);
303
+ const c = generateKeyBetween(a, b, digits);
304
+ return [
305
+ ...generateNKeysBetween(a, c, mid, digits),
306
+ c,
307
+ ...generateNKeysBetween(c, b, n - mid - 1, digits),
308
+ ];
309
+ }
310
+
311
+ module.exports = { BASE_62_DIGITS, generateKeyBetween, generateNKeysBetween };
@@ -0,0 +1,65 @@
1
+ // Minimal, dependency-free image dimension reader. Supports PNG, JPEG, GIF, WebP, BMP.
2
+ // Returns { width, height, mime } or throws.
3
+ const fs = require('fs');
4
+
5
+ function fromBuffer(buf) {
6
+ // PNG: \x89PNG\r\n\x1a\n, IHDR width/height at bytes 16..24 (big-endian)
7
+ if (buf.length >= 24 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) {
8
+ return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20), mime: 'image/png' };
9
+ }
10
+ // GIF: 'GIF87a' / 'GIF89a', width/height little-endian at bytes 6..10
11
+ if (buf.length >= 10 && buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46) {
12
+ return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8), mime: 'image/gif' };
13
+ }
14
+ // BMP
15
+ if (buf.length >= 26 && buf[0] === 0x42 && buf[1] === 0x4d) {
16
+ return { width: buf.readInt32LE(18), height: Math.abs(buf.readInt32LE(22)), mime: 'image/bmp' };
17
+ }
18
+ // WebP: RIFF....WEBP
19
+ if (buf.length >= 30 && buf.toString('ascii', 0, 4) === 'RIFF' && buf.toString('ascii', 8, 12) === 'WEBP') {
20
+ const fmt = buf.toString('ascii', 12, 16);
21
+ if (fmt === 'VP8 ') {
22
+ return { width: buf.readUInt16LE(26) & 0x3fff, height: buf.readUInt16LE(28) & 0x3fff, mime: 'image/webp' };
23
+ } else if (fmt === 'VP8L') {
24
+ const b = buf.readUInt32LE(21);
25
+ return { width: (b & 0x3fff) + 1, height: ((b >> 14) & 0x3fff) + 1, mime: 'image/webp' };
26
+ } else if (fmt === 'VP8X') {
27
+ const w = 1 + (buf[24] | (buf[25] << 8) | (buf[26] << 16));
28
+ const h = 1 + (buf[27] | (buf[28] << 8) | (buf[29] << 16));
29
+ return { width: w, height: h, mime: 'image/webp' };
30
+ }
31
+ }
32
+ // JPEG: scan SOFn markers
33
+ if (buf.length >= 4 && buf[0] === 0xff && buf[1] === 0xd8) {
34
+ let off = 2;
35
+ while (off < buf.length) {
36
+ if (buf[off] !== 0xff) { off++; continue; }
37
+ const marker = buf[off + 1];
38
+ // SOF0..SOF15 except DHT(c4) DAC(cc) RSTn — these carry frame dims
39
+ if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
40
+ const height = buf.readUInt16BE(off + 5);
41
+ const width = buf.readUInt16BE(off + 7);
42
+ return { width, height, mime: 'image/jpeg' };
43
+ }
44
+ const len = buf.readUInt16BE(off + 2);
45
+ off += 2 + len;
46
+ }
47
+ }
48
+ throw new Error('unsupported or unrecognized image format');
49
+ }
50
+
51
+ function imageSize(filePath) {
52
+ // Read enough bytes for any header (JPEG may need more; read up to 256KB)
53
+ const fd = fs.openSync(filePath, 'r');
54
+ try {
55
+ const stat = fs.fstatSync(fd);
56
+ const n = Math.min(stat.size, 262144);
57
+ const buf = Buffer.alloc(n);
58
+ fs.readSync(fd, buf, 0, n, 0);
59
+ return fromBuffer(buf);
60
+ } finally {
61
+ fs.closeSync(fd);
62
+ }
63
+ }
64
+
65
+ module.exports = { imageSize, fromBuffer };
@@ -0,0 +1,130 @@
1
+ // House style learned from the vault's presentation boards (MemoryOS High Level, Personalization
2
+ // Module, Research). Excalifont (fontFamily 5), solid fills, always-rounded rects, 2px #1e1e1e ink,
3
+ // Excalidraw's native pastel swatches used semantically. These helpers return ElementSpec[] for
4
+ // build_excalidraw.js's buildExcalidraw({ elements }).
5
+
6
+ const INK = '#1e1e1e';
7
+
8
+ // semantic palette: { fill, stroke } — matches the vault's swatches and their meaning
9
+ const PALETTE = {
10
+ green: { fill: '#b2f2bb', stroke: '#2f9e44' }, // benefit / positive / go
11
+ paleGreen: { fill: '#ebfbee', stroke: '#2f9e44' },
12
+ red: { fill: '#ffc9c9', stroke: '#e03131' }, // problem / pain / risk
13
+ blue: { fill: '#a5d8ff', stroke: '#1971c2' }, // system / neutral / flow node
14
+ paleBlue: { fill: '#e7f5ff', stroke: '#1971c2' },
15
+ purple: { fill: '#d0bfff', stroke: '#6741d9' }, // core service / special
16
+ palePurple: { fill: '#f3f0ff', stroke: '#6741d9' },
17
+ yellow: { fill: '#ffec99', stroke: '#f08c00' }, // processing / attention
18
+ mint: { fill: '#96f2d7', stroke: '#0c8599' }, // result / output
19
+ gray: { fill: '#e9ecef', stroke: '#868e96' }, // muted / inactive
20
+ };
21
+ function pal(c) { return PALETTE[c] || { fill: c, stroke: INK }; }
22
+
23
+ let _gc = 0;
24
+ const newGroup = (hint) => `g${++_gc}-${hint || ''}`;
25
+ const lines = (t) => String(t).split('\n').length;
26
+ // word-wrapped line count at a given width — keeps card labels vertically centered when they wrap
27
+ function wrapLineCount(text, fontSize, width) {
28
+ const perLine = Math.max(1, Math.floor(width / (fontSize * 0.52)));
29
+ let total = 0;
30
+ for (const ln of String(text).split('\n')) {
31
+ const words = ln.split(/\s+/).filter(Boolean);
32
+ if (!words.length) { total += 1; continue; }
33
+ let cur = 0, lc = 1;
34
+ for (const w of words) { const add = (cur ? 1 : 0) + w.length; if (cur + add > perLine && cur > 0) { lc++; cur = w.length; } else cur += add; }
35
+ total += lc;
36
+ }
37
+ return total;
38
+ }
39
+
40
+ // rounded card: filled rect + centered label, grouped so they move together
41
+ function card(o) {
42
+ const { x, y, w = 260, h = 92, text = '', color = 'blue', fontSize = 20, stroke = INK, strokeWidth = 2, textColor = INK, roundness = true } = o;
43
+ const group = o.group || newGroup('card');
44
+ const p = pal(color);
45
+ const textH = wrapLineCount(text, fontSize, w) * fontSize * 1.25;
46
+ const ty = y + Math.max(6, (h - textH) / 2);
47
+ return [
48
+ { type: 'rectangle', x, y, width: w, height: h, strokeColor: stroke, backgroundColor: p.fill, fillStyle: 'solid', strokeWidth, roundness, group },
49
+ { type: 'text', x, y: ty, text, fontSize, color: textColor, width: w, align: 'center', fontFamily: 5, group },
50
+ ];
51
+ }
52
+ // small node (flow step) — card with tighter defaults
53
+ function node(o) { return card(Object.assign({ w: 200, h: 64, fontSize: 18 }, o)); }
54
+
55
+ // big plain section header (no box), like "Configuration" / "Flow"
56
+ function sectionTitle(o) {
57
+ const { x, y, text, fontSize = 44, color = INK } = o;
58
+ return [{ type: 'text', x, y, text, fontSize, color, fontFamily: 5, width: Math.max(200, String(text).length * fontSize * 0.6) }];
59
+ }
60
+
61
+ // labeled connector arrow between two points (centers of cards, etc.)
62
+ function connector(o) {
63
+ const { from, to, label, double = false, strokeColor = INK, strokeWidth = 2, dashed = false, labelColor = '#495057', fontSize = 16 } = o;
64
+ const out = [{ type: 'arrow', points: [from, to], strokeColor, strokeWidth, strokeStyle: dashed ? 'dashed' : 'solid', endArrow: 'arrow', startArrow: double ? 'arrow' : null }];
65
+ if (label) {
66
+ const mx = (from[0] + to[0]) / 2, my = (from[1] + to[1]) / 2;
67
+ const w = Math.max(40, String(label).length * fontSize * 0.62);
68
+ out.push({ type: 'text', x: mx - w / 2, y: my - fontSize - 4, text: label, fontSize, color: labelColor, width: w, align: 'center', fontFamily: 5 });
69
+ }
70
+ return out;
71
+ }
72
+
73
+ // side-annotation: short arrow from a node edge to a spec note (the "90sec / deepseek..." pattern)
74
+ function annotate(o) {
75
+ const { from, to, text, fontSize = 16, color = '#495057' } = o;
76
+ return [
77
+ { type: 'arrow', points: [from, to], strokeColor: color, strokeWidth: 1.5, endArrow: 'arrow' },
78
+ { type: 'text', x: to[0] + 6, y: to[1] - fontSize * 0.6, text, fontSize, color, width: Math.max(80, String(text).split('\n')[0].length * fontSize * 0.62), align: 'left', fontFamily: 5 },
79
+ ];
80
+ }
81
+
82
+ // vertical stack of cards (benefit/risk columns)
83
+ function column(o) {
84
+ const { x, y, w = 280, h = 70, gap = 16, fontSize = 18, color = 'green', items = [] } = o;
85
+ const out = [];
86
+ items.forEach((it, i) => {
87
+ const t = typeof it === 'string' ? it : it.text;
88
+ const c = (typeof it === 'object' && it.color) || color;
89
+ out.push(...card({ x, y: y + i * (h + gap), w, h, text: t, color: c, fontSize }));
90
+ });
91
+ return out;
92
+ }
93
+
94
+ // hub-and-spoke: central node + satellites with arrows (the "Personalization Layer" + APIs pattern)
95
+ function hub(o) {
96
+ const { cx, cy, label, color = 'purple', rx = 64, ry = 44, radius = 170, spokes = [], spokeColor = 'blue', spokeRx = 30, spokeRy = 30, startAngle = -Math.PI / 2 } = o;
97
+ const group = newGroup('hub');
98
+ const out = [];
99
+ const p = pal(color);
100
+ out.push({ type: 'ellipse', x: cx - rx, y: cy - ry, width: rx * 2, height: ry * 2, strokeColor: p.stroke, backgroundColor: p.fill, fillStyle: 'solid', strokeWidth: 2 });
101
+ out.push({ type: 'text', x: cx - rx, y: cy - 10, text: label, fontSize: 16, color: INK, width: rx * 2, align: 'center', fontFamily: 5 });
102
+ const n = spokes.length;
103
+ spokes.forEach((s, i) => {
104
+ const a = startAngle + (i / Math.max(1, n)) * Math.PI * 2;
105
+ const sx = cx + Math.cos(a) * radius, sy = cy + Math.sin(a) * radius;
106
+ const c = (typeof s === 'object' && s.color) || spokeColor;
107
+ const t = typeof s === 'string' ? s : s.label;
108
+ const sp = pal(c);
109
+ out.push({ type: 'arrow', points: [[cx + Math.cos(a) * rx, cy + Math.sin(a) * ry], [sx - Math.cos(a) * spokeRx, sy - Math.sin(a) * spokeRy]], strokeColor: INK, strokeWidth: 2, endArrow: 'arrow' });
110
+ out.push({ type: 'ellipse', x: sx - spokeRx, y: sy - spokeRy, width: spokeRx * 2, height: spokeRy * 2, strokeColor: sp.stroke, backgroundColor: sp.fill, fillStyle: 'solid', strokeWidth: 2 });
111
+ out.push({ type: 'text', x: sx - spokeRx, y: sy - 8, text: t, fontSize: 14, color: INK, width: spokeRx * 2, align: 'center', fontFamily: 5 });
112
+ });
113
+ return out;
114
+ }
115
+
116
+ // bullet list as a single left-aligned block (their "Configuration" spec list pattern)
117
+ function bullets(o) {
118
+ const { x, y, items = [], fontSize = 20, color = INK, bullet = '• ', width } = o;
119
+ const text = items.map((i) => bullet + i).join('\n');
120
+ return [{ type: 'text', x, y, text, fontSize, color, fontFamily: 5, width: width || Math.max(200, Math.max(...items.map((i) => i.length)) * fontSize * 0.6) }];
121
+ }
122
+
123
+ // center helpers for wiring connectors to card edges
124
+ const center = (x, y, w, h) => [x + w / 2, y + h / 2];
125
+ const rightOf = (x, y, w, h) => [x + w, y + h / 2];
126
+ const leftOf = (x, y, w, h) => [x, y + h / 2];
127
+ const bottomOf = (x, y, w, h) => [x + w / 2, y + h];
128
+ const topOf = (x, y, w, h) => [x + w / 2, y];
129
+
130
+ module.exports = { INK, PALETTE, pal, card, node, sectionTitle, connector, annotate, column, hub, bullets, center, rightOf, leftOf, bottomOf, topOf };
@@ -0,0 +1,151 @@
1
+ ---
2
+ name: video-watching
3
+ description: >-
4
+ Watch a video the way a human would — turn it into a time-mapped transcript
5
+ with the on-screen visuals (slides, UI, diagrams, text) described inline, then
6
+ reason about it. Triggers: "watch this video", "şu videoyu izle", "videoyu
7
+ analiz et", "transkript çıkar", "video transcript", "bu videoda ne var", or any
8
+ time the user hands over a video file/link. Produces a curated
9
+ <video>.transcript.md NEXT TO the source video, then continues per use-case
10
+ (marketing teardown, FTE analysis, knowledge capture, …).
11
+ alwaysApply: false
12
+ ruleType: "Tool Workflow"
13
+ version: "1.0"
14
+ ---
15
+
16
+ # Video Watching
17
+
18
+ Turn a video into something an AI can fully reason about: a **time-mapped
19
+ transcript** with **what's on screen described inline at the same timestamps**.
20
+ The output is one self-contained file dropped **next to the source video**, so any
21
+ model (our cloud, this session, anything) can read the whole video without the
22
+ binary.
23
+
24
+ The clever part: **transcript first, then YOU decide which frames to look at.**
25
+ You don't blindly OCR every frame. You read the transcript, see where the speaker
26
+ references something visual ("as you can see here", "this screen", "the chart"),
27
+ look up that moment in `frames.json`, and open only those frames.
28
+
29
+ ## The loop (do these in order)
30
+
31
+ ### 0. Get the video
32
+ Ask for the path or link if not given. Local files work today. Remote links
33
+ (YouTube etc.) need `yt-dlp` (`brew install yt-dlp`) — the engine auto-downloads
34
+ if it's installed.
35
+
36
+ ### 1. Run the engine (mechanical — one command)
37
+ ```bash
38
+ .claude/skills/video-watching/scripts/transcribe.sh "/abs/path/to/video.mov"
39
+ # language auto-detects — no need to pass it. Override only if auto mislabels a
40
+ # short/ambiguous clip: ./transcribe.sh "/abs/path/clip.mp4" tr
41
+ ```
42
+ This writes everything into `<video_dir>/<slug>.media/`:
43
+ - `transcript.srt` / `.json` / `.txt` — timestamped transcript (large-v3-turbo)
44
+ - `frames/anchor_first.jpg` / `anchor_last.jpg` — first + last frame, always captured
45
+ (short cut-heavy creatives carry the hook and CTA here; scene-detect misses both)
46
+ - `frames/scene_*.jpg` — frames at each scene change (slides/UI transitions)
47
+ - `frames/gap_*.jpg` — fill frames so no stretch > 10s goes unsampled (catches
48
+ static-but-important sections scene-detect misses: app demos, slides, CTAs)
49
+ - `frames.json` — **the index you read**: `[{file, t, at, type}]`, sorted by time,
50
+ near-duplicate timestamps collapsed (anchors always kept)
51
+
52
+ `audio.wav` is auto-deleted after transcription (it's a ~1.9MB/min whisper-only
53
+ intermediate). The whole `*.media/` dir is gitignored — it stays next to the video
54
+ as scratch but never enters the repo. Only `<slug>.transcript.md` is the keeper;
55
+ delete a `.media/` folder anytime to reclaim disk (re-running regenerates it).
56
+
57
+ ### 2. Read the transcript
58
+ Read `transcript.srt`. It carries the spoken content with `[mm:ss]` timing. Form a
59
+ first-pass understanding of structure and topic.
60
+
61
+ ### 3. Decide which frames you NEED — then view them
62
+ Read `frames.json`. For each moment where understanding depends on the visual —
63
+ the speaker points at something, a slide/UI/diagram/number is on screen, or the
64
+ transcript is ambiguous without the picture — pick the frame whose `at` is closest
65
+ to that moment and **open it with the Read tool** (it renders images). Don't open
66
+ all of them; open the ones that carry information. Talking-head stretches with
67
+ nothing on screen need no frame.
68
+
69
+ > Heuristic: the two `anchor` frames (first/last) almost always matter — the hook
70
+ > and the CTA. Every `scene` frame is a candidate (the picture changed for a
71
+ > reason). `gap` frames cover static stretches scene-detect skipped — often the
72
+ > most informative part (an app demo or slide that doesn't "move"), so check them.
73
+
74
+ **Need a frame the index doesn't have? Grab it on demand.** Scene-detect fires on
75
+ motion, not on meaning — on fast-cut video the most informative moment often sits
76
+ between the indexed frames. When the VO points at something ("look at this
77
+ screen", a number, a result) and no indexed frame lands there, fetch that exact
78
+ second yourself:
79
+ ```bash
80
+ ffmpeg -y -loglevel error -ss <seconds> -i "<video>" -frames:v 1 -q:v 3 \
81
+ "<slug>.media/frames/grab_<mmss>.jpg"
82
+ ```
83
+ This is the whole point of transcript-first: the frame set is yours to extend, not
84
+ a fixed dump you're stuck with.
85
+
86
+ ### 4. Write the curated transcript NEXT TO the video
87
+ Write `<video_dir>/<slug>.transcript.md` — the deliverable. One timeline, voice and
88
+ visuals interleaved, every line timestamped:
89
+
90
+ ```markdown
91
+ # <Video Title>
92
+ source: <path or URL> · duration: <mm:ss> · transcribed: large-v3-turbo
93
+
94
+ ## Summary
95
+ 2–4 sentences: what this video is and what it's for.
96
+
97
+ ## Timeline
98
+ - **[00:00]** <what's said>
99
+ - **[00:06]** 🖼 <what's on screen — slide title, UI state, chart, on-screen text>
100
+ - **[00:12]** <what's said> … 🖼 <visual if relevant>
101
+ ...
102
+
103
+ ## Visual notes
104
+ - **[00:06]** Slide: "Funnel → Billing → Gateway → PSP" (4 layers)
105
+ - ...
106
+ ```
107
+ Rules: never invent content not in transcript or frames; mark anything uncertain;
108
+ keep the source path in the header so the file stands alone.
109
+
110
+ ### 5. Hand off to the use-case
111
+ Once the `.transcript.md` exists you know the video cold. Tell the user it's ready
112
+ and ask what they want from it — the answer is domain-specific. Triage to the
113
+ relevant dreamcontext skill (per the skill-triage rule) and load it:
114
+ - **Marketing video** → teardown, hook/CTA analysis, "we could do X" (→ `growth` / `meta-marketing` skills)
115
+ - **First-time-experience / app demo** → reconstruct the FTE flow, friction points (→ `design` / `onboarding-design`)
116
+ - **Knowledge / training** → if it should persist for the project, hand the transcript to `dreamcontext knowledge` so it becomes durable context
117
+ Don't guess the use-case — let the user direct it.
118
+
119
+ ## What this skill is NOT
120
+ - Not a knowledge-base writer or ingestion pipeline. This skill produces the
121
+ transcript artifact; persisting it (chunking, embedding, ingest into a project's
122
+ long-term memory) is a separate, downstream concern — keep that boundary clean.
123
+ - Not an auto-summarizer that skips the frames. The visual pass is the point —
124
+ that's what separates "watched the video" from "read the subtitles".
125
+
126
+ ## Setup (once per machine)
127
+ ```bash
128
+ brew install whisper-cpp ffmpeg # yt-dlp too, only for remote links
129
+ # model: the engine prefers large-v3-turbo, falls back to large-v3 then medium.
130
+ # pull turbo if missing:
131
+ # curl -L -o ~/.cache/whisper.cpp/models/ggml-large-v3-turbo.bin \
132
+ # https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin
133
+ ```
134
+ `build_frame_index.py` is stdlib-only — no venv needed. Language **auto-detects**
135
+ (whisper); only pass a code (`tr`, `en`) to force it on a short/ambiguous clip.
136
+
137
+ ## Files
138
+ ```
139
+ .claude/skills/video-watching/
140
+ ├── SKILL.md ← you are here
141
+ └── scripts/
142
+ ├── transcribe.sh ← video → transcript + frames + frames.json (the engine)
143
+ ├── gap_fill.py ← timestamps to fill scene-detect gaps (> MAX_GAP)
144
+ └── build_frame_index.py ← frames/*.jpg → frames.json (pts_time index)
145
+ ```
146
+
147
+ ## Tuning
148
+ - Too few frames on a slide-heavy video? `SCENE_THRESHOLD=0.1 ./transcribe.sh …`
149
+ - Long static lecture over-sampled? Raise `MAX_GAP=20 ./transcribe.sh …` (default 10s).
150
+ - Capturing too many near-identical frames? `DEDUPE_SEC=1.0 ./transcribe.sh …`
151
+ - Force a model: `WHISPER_MODEL=/abs/ggml-large-v3.bin ./transcribe.sh …`