@foldspace_npm/harness 0.1.31 → 0.1.32

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 (39) hide show
  1. package/CLAUDE.md +27 -8
  2. package/README.md +25 -2
  3. package/bin/ask.mjs +16 -145
  4. package/bin/attach.mjs +2 -0
  5. package/bin/cli.mjs +22 -0
  6. package/bin/inject.mjs +2 -14
  7. package/bin/record.mjs +412 -0
  8. package/package.json +1 -1
  9. package/src/attach-daemon.mjs +18 -0
  10. package/src/badge-auto.mjs +38 -1
  11. package/src/cdp-client.mjs +7 -1
  12. package/src/chrome-binary.mjs +28 -0
  13. package/src/cli-help.mjs +1 -0
  14. package/src/cli-registry.mjs +53 -3
  15. package/src/init.mjs +170 -16
  16. package/src/panel-driver.mjs +283 -0
  17. package/src/session-events.mjs +1 -0
  18. package/src/upgrade.mjs +87 -1
  19. package/src/video/README.md +29 -0
  20. package/src/video/assets/foldspace-mark.svg +27 -0
  21. package/src/video/assets/logo-dark.svg +68 -0
  22. package/src/video/assets/logo-light.svg +56 -0
  23. package/src/video/brand.mjs +46 -0
  24. package/src/video/compose.mjs +476 -0
  25. package/src/video/composer/composer.mjs +554 -0
  26. package/src/video/composer/index.html +13 -0
  27. package/src/video/fonts/OFL.txt +93 -0
  28. package/src/video/fonts/Poppins-ExtraBold.woff2 +0 -0
  29. package/src/video/fonts/Poppins-Regular.woff2 +0 -0
  30. package/src/video/fonts/Poppins-SemiBold.woff2 +0 -0
  31. package/src/video/recorder.mjs +331 -0
  32. package/src/video/scenes.mjs +253 -0
  33. package/src/video/timeline.mjs +726 -0
  34. package/src/video/vendor/mp4-muxer.LICENSE +21 -0
  35. package/src/video/vendor/mp4-muxer.min.mjs +5 -0
  36. package/templates/agent-starter/README.md +5 -0
  37. package/templates/agent-starter/claude-settings.json +1 -0
  38. package/templates/agent-starter/gitignore +3 -0
  39. package/templates/agent-starter/skills/share-a-video/SKILL.md +235 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Vanilagy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ /*! mp4-muxer 5.2.2 | MIT License | Copyright (c) 2023 Vanilagy | https://github.com/Vanilagy/mp4-muxer */
2
+ var we=(e,t,s)=>{if(!t.has(e))throw TypeError("Cannot "+s)},i=(e,t,s)=>(we(e,t,"read from private field"),s?s.call(e):t.get(e)),d=(e,t,s)=>{if(t.has(e))throw TypeError("Cannot add the same private member more than once");t instanceof WeakSet?t.add(e):t.set(e,s)},y=(e,t,s,r)=>(we(e,t,"write to private field"),r?r.call(e,s):t.set(e,s),s),st=(e,t,s,r)=>({set _(a){y(e,t,a,s)},get _(){return i(e,t,r)}}),m=(e,t,s)=>(we(e,t,"access private method"),s),p=new Uint8Array(8),z=new DataView(p.buffer),C=e=>[(e%256+256)%256],c=e=>(z.setUint16(0,e,!1),[p[0],p[1]]),rt=e=>(z.setInt16(0,e,!1),[p[0],p[1]]),Le=e=>(z.setUint32(0,e,!1),[p[1],p[2],p[3]]),n=e=>(z.setUint32(0,e,!1),[p[0],p[1],p[2],p[3]]),at=e=>(z.setInt32(0,e,!1),[p[0],p[1],p[2],p[3]]),W=e=>(z.setUint32(0,Math.floor(e/2**32),!1),z.setUint32(4,e,!1),[p[0],p[1],p[2],p[3],p[4],p[5],p[6],p[7]]),ve=e=>(z.setInt16(0,2**8*e,!1),[p[0],p[1]]),A=e=>(z.setInt32(0,2**16*e,!1),[p[0],p[1],p[2],p[3]]),ge=e=>(z.setInt32(0,2**30*e,!1),[p[0],p[1],p[2],p[3]]),E=(e,t=!1)=>{let s=Array(e.length).fill(null).map((r,a)=>e.charCodeAt(a));return t&&s.push(0),s},re=e=>e&&e[e.length-1],Ce=e=>{let t;for(let s of e)(!t||s.presentationTimestamp>t.presentationTimestamp)&&(t=s);return t},O=(e,t,s=!0)=>{let r=e*t;return s?Math.round(r):r},Ve=e=>{let t=e*(Math.PI/180),s=Math.cos(t),r=Math.sin(t);return[s,r,0,-r,s,0,0,0,1]},Pe=Ve(0),je=e=>[A(e[0]),A(e[1]),ge(e[2]),A(e[3]),A(e[4]),ge(e[5]),A(e[6]),A(e[7]),ge(e[8])],G=e=>!e||typeof e!="object"?e:Array.isArray(e)?e.map(G):Object.fromEntries(Object.entries(e).map(([t,s])=>[t,G(s)])),P=e=>e>=0&&e<2**32,g=(e,t,s)=>({type:e,contents:t&&new Uint8Array(t.flat(10)),children:s}),w=(e,t,s,r,a)=>g(e,[C(t),Le(s),r??[]],a),nt=e=>{let t=512;return e.fragmented?g("ftyp",[E("iso5"),n(t),E("iso5"),E("iso6"),E("mp41")]):g("ftyp",[E("isom"),n(t),E("isom"),e.holdsAvc?E("avc1"):[],E("mp41")])},ye=e=>({type:"mdat",largeSize:e}),ot=e=>({type:"free",size:e}),ae=(e,t,s=!1)=>g("moov",null,[ht(t,e),...e.map(r=>lt(r,t)),s?Lt(e):null]),ht=(e,t)=>{let s=O(Math.max(0,...t.filter(h=>h.samples.length>0).map(h=>{const f=Ce(h.samples);return f.presentationTimestamp+f.duration})),ze),r=Math.max(...t.map(h=>h.id))+1,a=!P(e)||!P(s),o=a?W:n;return w("mvhd",+a,0,[o(e),o(e),n(ze),o(s),A(1),ve(1),Array(10).fill(0),je(Pe),Array(24).fill(0),n(r)])},lt=(e,t)=>g("trak",null,[ft(e,t),ut(e,t)]),ft=(e,t)=>{let s=Ce(e.samples),r=O(s?s.presentationTimestamp+s.duration:0,ze),a=!P(t)||!P(r),o=a?W:n,h;return e.info.type==="video"?h=typeof e.info.rotation=="number"?Ve(e.info.rotation):e.info.rotation:h=Pe,w("tkhd",+a,3,[o(t),o(t),n(e.id),n(0),o(r),Array(8).fill(0),c(0),c(0),ve(e.info.type==="audio"?1:0),c(0),je(h),A(e.info.type==="video"?e.info.width:0),A(e.info.type==="video"?e.info.height:0)])},ut=(e,t)=>g("mdia",null,[dt(e,t),mt(e.info.type==="video"?"vide":"soun"),pt(e)]),dt=(e,t)=>{let s=Ce(e.samples),r=O(s?s.presentationTimestamp+s.duration:0,e.timescale),a=!P(t)||!P(r),o=a?W:n;return w("mdhd",+a,0,[o(t),o(t),n(e.timescale),o(r),c(21956),c(0)])},mt=e=>w("hdlr",0,0,[E("mhlr"),E(e),n(0),n(0),n(0),E("mp4-muxer-hdlr",!0)]),pt=e=>g("minf",null,[e.info.type==="video"?ct():wt(),vt(),yt(e)]),ct=()=>w("vmhd",0,1,[c(0),c(0),c(0),c(0)]),wt=()=>w("smhd",0,0,[c(0),c(0)]),vt=()=>g("dinf",null,[gt()]),gt=()=>w("dref",0,0,[n(1)],[Ct()]),Ct=()=>w("url ",0,1),yt=e=>{const t=e.compositionTimeOffsetTable.length>1||e.compositionTimeOffsetTable.some(s=>s.sampleCompositionTimeOffset!==0);return g("stbl",null,[Tt(e),Ut(e),xt(e),Wt(e),Ft(e),Nt(e),t?Rt(e):null])},Tt=e=>w("stsd",0,0,[n(1)],[e.info.type==="video"?St(Kt[e.info.codec],e):It(Yt[e.info.codec],e)]),St=(e,t)=>g(e,[Array(6).fill(0),c(1),c(0),c(0),Array(12).fill(0),c(t.info.width),c(t.info.height),n(4718592),n(4718592),n(0),c(1),Array(32).fill(0),c(24),rt(65535)],[Qt[t.info.codec](t),t.info.decoderConfig.colorSpace?Et(t):null]),bt={bt709:1,bt470bg:5,smpte170m:6},kt={bt709:1,smpte170m:6,"iec61966-2-1":13},_t={rgb:0,bt709:1,bt470bg:5,smpte170m:6},Et=e=>g("colr",[E("nclx"),c(bt[e.info.decoderConfig.colorSpace.primaries]),c(kt[e.info.decoderConfig.colorSpace.transfer]),c(_t[e.info.decoderConfig.colorSpace.matrix]),C((e.info.decoderConfig.colorSpace.fullRange?1:0)<<7)]),zt=e=>e.info.decoderConfig&&g("avcC",[...new Uint8Array(e.info.decoderConfig.description)]),At=e=>e.info.decoderConfig&&g("hvcC",[...new Uint8Array(e.info.decoderConfig.description)]),Ot=e=>{if(!e.info.decoderConfig)return null;let t=e.info.decoderConfig;if(!t.colorSpace)throw new Error("'colorSpace' is required in the decoder config for VP9.");let s=t.codec.split("."),r=Number(s[1]),a=Number(s[2]),f=(Number(s[3])<<4)+(0<<1)+Number(t.colorSpace.fullRange);return w("vpcC",1,0,[C(r),C(a),C(f),C(2),C(2),C(2),c(0)])},Bt=()=>{let s=(1<<7)+1;return g("av1C",[s,0,0,0])},It=(e,t)=>g(e,[Array(6).fill(0),c(1),c(0),c(0),n(0),c(t.info.numberOfChannels),c(16),c(0),c(0),A(t.info.sampleRate)],[Jt[t.info.codec](t)]),Mt=e=>{let t=new Uint8Array(e.info.decoderConfig.description);return w("esds",0,0,[n(58753152),C(32+t.byteLength),c(1),C(0),n(75530368),C(18+t.byteLength),C(64),C(21),Le(0),n(130071),n(130071),n(92307584),C(t.byteLength),...t,n(109084800),C(1),C(2)])},Dt=e=>{let t=3840,s=0;const r=e.info.decoderConfig?.description;if(r){if(r.byteLength<18)throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.");const a=ArrayBuffer.isView(r)?new DataView(r.buffer,r.byteOffset,r.byteLength):new DataView(r);t=a.getUint16(10,!0),s=a.getInt16(14,!0)}return g("dOps",[C(0),C(e.info.numberOfChannels),c(t),n(e.info.sampleRate),ve(s),C(0)])},Ut=e=>w("stts",0,0,[n(e.timeToSampleTable.length),e.timeToSampleTable.map(t=>[n(t.sampleCount),n(t.sampleDelta)])]),xt=e=>{if(e.samples.every(s=>s.type==="key"))return null;let t=[...e.samples.entries()].filter(([,s])=>s.type==="key");return w("stss",0,0,[n(t.length),t.map(([s])=>n(s+1))])},Wt=e=>w("stsc",0,0,[n(e.compactlyCodedChunkTable.length),e.compactlyCodedChunkTable.map(t=>[n(t.firstChunk),n(t.samplesPerChunk),n(1)])]),Ft=e=>w("stsz",0,0,[n(0),n(e.samples.length),e.samples.map(t=>n(t.size))]),Nt=e=>e.finalizedChunks.length>0&&re(e.finalizedChunks).offset>=2**32?w("co64",0,0,[n(e.finalizedChunks.length),e.finalizedChunks.map(t=>W(t.offset))]):w("stco",0,0,[n(e.finalizedChunks.length),e.finalizedChunks.map(t=>n(t.offset))]),Rt=e=>w("ctts",0,0,[n(e.compositionTimeOffsetTable.length),e.compositionTimeOffsetTable.map(t=>[n(t.sampleCount),n(t.sampleCompositionTimeOffset)])]),Lt=e=>g("mvex",null,e.map(Vt)),Vt=e=>w("trex",0,0,[n(e.id),n(1),n(0),n(0),n(0)]),$e=(e,t)=>g("moof",null,[Pt(e),...t.map(jt)]),Pt=e=>w("mfhd",0,0,[n(e)]),He=e=>{let t=0,s=0,r=0,a=0,o=e.type==="delta";return s|=+o,o?t|=1:t|=2,t<<24|s<<16|r<<8|a},jt=e=>g("traf",null,[$t(e),Ht(e),qt(e)]),$t=e=>{let t=0;t|=8,t|=16,t|=32,t|=131072;let s=e.currentChunk.samples[1]??e.currentChunk.samples[0],r={duration:s.timescaleUnitsToNextSample,size:s.size,flags:He(s)};return w("tfhd",0,t,[n(e.id),n(r.duration),n(r.size),n(r.flags)])},Ht=e=>w("tfdt",1,0,[W(O(e.currentChunk.startTimestamp,e.timescale))]),qt=e=>{let t=e.currentChunk.samples.map(I=>I.timescaleUnitsToNextSample),s=e.currentChunk.samples.map(I=>I.size),r=e.currentChunk.samples.map(He),a=e.currentChunk.samples.map(I=>O(I.presentationTimestamp-I.decodeTimestamp,e.timescale)),o=new Set(t),h=new Set(s),f=new Set(r),v=new Set(a),S=f.size===2&&r[0]!==r[1],U=o.size>1,X=h.size>1,Ne=!S&&f.size>1,Re=v.size>1||[...v].some(I=>I!==0),x=0;return x|=1,x|=4*+S,x|=256*+U,x|=512*+X,x|=1024*+Ne,x|=2048*+Re,w("trun",1,x,[n(e.currentChunk.samples.length),n(e.currentChunk.offset-e.currentChunk.moofOffset||0),S?n(r[0]):[],e.currentChunk.samples.map((I,se)=>[U?n(t[se]):[],X?n(s[se]):[],Ne?n(r[se]):[],Re?at(a[se]):[]])])},Xt=e=>g("mfra",null,[...e.map(Gt),Zt()]),Gt=(e,t)=>w("tfra",1,0,[n(e.id),n(63),n(e.finalizedChunks.length),e.finalizedChunks.map(r=>[W(O(r.startTimestamp,e.timescale)),W(r.moofOffset),n(t+1),n(1),n(1)])]),Zt=()=>w("mfro",0,0,[n(0)]),Kt={avc:"avc1",hevc:"hvc1",vp9:"vp09",av1:"av01"},Qt={avc:zt,hevc:At,vp9:Ot,av1:Bt},Yt={aac:"mp4a",opus:"Opus"},Jt={aac:Mt,opus:Dt},ei=Symbol("isTarget"),ne=class{},qe=class extends ne{constructor(){super(...arguments),this.buffer=null}},Te=class extends ne{constructor(e){if(super(),this.options=e,typeof e!="object")throw new TypeError("StreamTarget requires an options object to be passed to its constructor.");if(e.onData){if(typeof e.onData!="function")throw new TypeError("options.onData, when provided, must be a function.");if(e.onData.length<2)throw new TypeError("options.onData, when provided, must be a function that takes in at least two arguments (data and position). Ignoring the position argument, which specifies the byte offset at which the data is to be written, can lead to broken outputs.")}if(e.chunked!==void 0&&typeof e.chunked!="boolean")throw new TypeError("options.chunked, when provided, must be a boolean.");if(e.chunkSize!==void 0&&(!Number.isInteger(e.chunkSize)||e.chunkSize<1024))throw new TypeError("options.chunkSize, when provided, must be an integer and not smaller than 1024.")}},Xe=class extends ne{constructor(e,t){if(super(),this.stream=e,this.options=t,!(e instanceof FileSystemWritableFileStream))throw new TypeError("FileSystemWritableFileStreamTarget requires a FileSystemWritableFileStream instance.");if(t!==void 0&&typeof t!="object")throw new TypeError("FileSystemWritableFileStreamTarget's options, when provided, must be an object.");if(t&&t.chunkSize!==void 0&&(!Number.isInteger(t.chunkSize)||t.chunkSize<=0))throw new TypeError("options.chunkSize, when provided, must be a positive integer")}},F,j,Ge=class{constructor(){this.pos=0,d(this,F,new Uint8Array(8)),d(this,j,new DataView(i(this,F).buffer)),this.offsets=new WeakMap}seek(e){this.pos=e}writeU32(e){i(this,j).setUint32(0,e,!1),this.write(i(this,F).subarray(0,4))}writeU64(e){i(this,j).setUint32(0,Math.floor(e/2**32),!1),i(this,j).setUint32(4,e,!1),this.write(i(this,F).subarray(0,8))}writeAscii(e){for(let t=0;t<e.length;t++)i(this,j).setUint8(t%8,e.charCodeAt(t)),t%8===7&&this.write(i(this,F));e.length%8!==0&&this.write(i(this,F).subarray(0,e.length%8))}writeBox(e){if(this.offsets.set(e,this.pos),e.contents&&!e.children)this.writeBoxHeader(e,e.size??e.contents.byteLength+8),this.write(e.contents);else{let t=this.pos;if(this.writeBoxHeader(e,0),e.contents&&this.write(e.contents),e.children)for(let a of e.children)a&&this.writeBox(a);let s=this.pos,r=e.size??s-t;this.seek(t),this.writeBoxHeader(e,r),this.seek(s)}}writeBoxHeader(e,t){this.writeU32(e.largeSize?1:t),this.writeAscii(e.type),e.largeSize&&this.writeU64(t)}measureBoxHeader(e){return 8+(e.largeSize?8:0)}patchBox(e){let t=this.pos;this.seek(this.offsets.get(e)),this.writeBox(e),this.seek(t)}measureBox(e){if(e.contents&&!e.children)return this.measureBoxHeader(e)+e.contents.byteLength;{let t=this.measureBoxHeader(e);if(e.contents&&(t+=e.contents.byteLength),e.children)for(let s of e.children)s&&(t+=this.measureBox(s));return t}}};F=new WeakMap,j=new WeakMap;var oe,N,Z,K,he,Se,ti=class extends Ge{constructor(e){super(),d(this,he),d(this,oe,void 0),d(this,N,new ArrayBuffer(2**16)),d(this,Z,new Uint8Array(i(this,N))),d(this,K,0),y(this,oe,e)}write(e){m(this,he,Se).call(this,this.pos+e.byteLength),i(this,Z).set(e,this.pos),this.pos+=e.byteLength,y(this,K,Math.max(i(this,K),this.pos))}finalize(){m(this,he,Se).call(this,this.pos),i(this,oe).buffer=i(this,N).slice(0,Math.max(i(this,K),this.pos))}};oe=new WeakMap,N=new WeakMap,Z=new WeakMap,K=new WeakMap,he=new WeakSet,Se=function(e){let t=i(this,N).byteLength;for(;t<e;)t*=2;if(t===i(this,N).byteLength)return;let s=new ArrayBuffer(t),r=new Uint8Array(s);r.set(i(this,Z),0),y(this,N,s),y(this,Z,r)};var ii=2**24,si=2,Q,R,Y,M,k,le,be,ke,Ze,_e,Ke,J,fe,Ee=class extends Ge{constructor(e){super(),d(this,le),d(this,ke),d(this,_e),d(this,J),d(this,Q,void 0),d(this,R,[]),d(this,Y,void 0),d(this,M,void 0),d(this,k,[]),y(this,Q,e),y(this,Y,e.options?.chunked??!1),y(this,M,e.options?.chunkSize??ii)}write(e){i(this,R).push({data:e.slice(),start:this.pos}),this.pos+=e.byteLength}flush(){if(i(this,R).length===0)return;let e=[],t=[...i(this,R)].sort((s,r)=>s.start-r.start);e.push({start:t[0].start,size:t[0].data.byteLength});for(let s=1;s<t.length;s++){let r=e[e.length-1],a=t[s];a.start<=r.start+r.size?r.size=Math.max(r.size,a.start+a.data.byteLength-r.start):e.push({start:a.start,size:a.data.byteLength})}for(let s of e){s.data=new Uint8Array(s.size);for(let r of i(this,R))s.start<=r.start&&r.start<s.start+s.size&&s.data.set(r.data,r.start-s.start);i(this,Y)?(m(this,le,be).call(this,s.data,s.start),m(this,J,fe).call(this)):i(this,Q).options.onData?.(s.data,s.start)}i(this,R).length=0}finalize(){i(this,Y)&&m(this,J,fe).call(this,!0)}};Q=new WeakMap,R=new WeakMap,Y=new WeakMap,M=new WeakMap,k=new WeakMap,le=new WeakSet,be=function(e,t){let s=i(this,k).findIndex(f=>f.start<=t&&t<f.start+i(this,M));s===-1&&(s=m(this,_e,Ke).call(this,t));let r=i(this,k)[s],a=t-r.start,o=e.subarray(0,Math.min(i(this,M)-a,e.byteLength));r.data.set(o,a);let h={start:a,end:a+o.byteLength};if(m(this,ke,Ze).call(this,r,h),r.written[0].start===0&&r.written[0].end===i(this,M)&&(r.shouldFlush=!0),i(this,k).length>si){for(let f=0;f<i(this,k).length-1;f++)i(this,k)[f].shouldFlush=!0;m(this,J,fe).call(this)}o.byteLength<e.byteLength&&m(this,le,be).call(this,e.subarray(o.byteLength),t+o.byteLength)},ke=new WeakSet,Ze=function(e,t){let s=0,r=e.written.length-1,a=-1;for(;s<=r;){let o=Math.floor(s+(r-s+1)/2);e.written[o].start<=t.start?(s=o+1,a=o):r=o-1}for(e.written.splice(a+1,0,t),(a===-1||e.written[a].end<t.start)&&a++;a<e.written.length-1&&e.written[a].end>=e.written[a+1].start;)e.written[a].end=Math.max(e.written[a].end,e.written[a+1].end),e.written.splice(a+1,1)},_e=new WeakSet,Ke=function(e){let s={start:Math.floor(e/i(this,M))*i(this,M),data:new Uint8Array(i(this,M)),written:[],shouldFlush:!1};return i(this,k).push(s),i(this,k).sort((r,a)=>r.start-a.start),i(this,k).indexOf(s)},J=new WeakSet,fe=function(e=!1){for(let t=0;t<i(this,k).length;t++){let s=i(this,k)[t];if(!(!s.shouldFlush&&!e)){for(let r of s.written)i(this,Q).options.onData?.(s.data.subarray(r.start,r.end),s.start+r.start);i(this,k).splice(t--,1)}}};var ri=class extends Ee{constructor(e){super(new Te({onData:(t,s)=>e.stream.write({type:"write",data:t,position:s}),chunked:!0,chunkSize:e.options?.chunkSize}))}},ze=1e3,ai=["avc","hevc","vp9","av1"],ni=["aac","opus"],oi=2082844800,hi=["strict","offset","cross-track-offset"],l,u,ue,_,b,T,$,H,Ae,L,V,ee,Oe,Qe,Be,Ye,Ie,Je,Me,et,De,tt,de,Ue,B,D,xe,it,te,me,pe,We,q,ie,ce,Fe,li=class{constructor(e){if(d(this,Oe),d(this,Be),d(this,Ie),d(this,Me),d(this,De),d(this,de),d(this,B),d(this,xe),d(this,te),d(this,pe),d(this,q),d(this,ce),d(this,l,void 0),d(this,u,void 0),d(this,ue,void 0),d(this,_,void 0),d(this,b,null),d(this,T,null),d(this,$,Math.floor(Date.now()/1e3)+oi),d(this,H,[]),d(this,Ae,1),d(this,L,[]),d(this,V,[]),d(this,ee,!1),m(this,Oe,Qe).call(this,e),e.video=G(e.video),e.audio=G(e.audio),e.fastStart=G(e.fastStart),this.target=e.target,y(this,l,{firstTimestampBehavior:"strict",...e}),e.target instanceof qe)y(this,u,new ti(e.target));else if(e.target instanceof Te)y(this,u,new Ee(e.target));else if(e.target instanceof Xe)y(this,u,new ri(e.target));else throw new Error(`Invalid target: ${e.target}`);m(this,Me,et).call(this),m(this,Be,Ye).call(this)}addVideoChunk(e,t,s,r){if(!(e instanceof EncodedVideoChunk))throw new TypeError("addVideoChunk's first argument (sample) must be of type EncodedVideoChunk.");if(t&&typeof t!="object")throw new TypeError("addVideoChunk's second argument (meta), when provided, must be an object.");if(s!==void 0&&(!Number.isFinite(s)||s<0))throw new TypeError("addVideoChunk's third argument (timestamp), when provided, must be a non-negative real number.");if(r!==void 0&&!Number.isFinite(r))throw new TypeError("addVideoChunk's fourth argument (compositionTimeOffset), when provided, must be a real number.");let a=new Uint8Array(e.byteLength);e.copyTo(a),this.addVideoChunkRaw(a,e.type,s??e.timestamp,e.duration,t,r)}addVideoChunkRaw(e,t,s,r,a,o){if(!(e instanceof Uint8Array))throw new TypeError("addVideoChunkRaw's first argument (data) must be an instance of Uint8Array.");if(t!=="key"&&t!=="delta")throw new TypeError("addVideoChunkRaw's second argument (type) must be either 'key' or 'delta'.");if(!Number.isFinite(s)||s<0)throw new TypeError("addVideoChunkRaw's third argument (timestamp) must be a non-negative real number.");if(!Number.isFinite(r)||r<0)throw new TypeError("addVideoChunkRaw's fourth argument (duration) must be a non-negative real number.");if(a&&typeof a!="object")throw new TypeError("addVideoChunkRaw's fifth argument (meta), when provided, must be an object.");if(o!==void 0&&!Number.isFinite(o))throw new TypeError("addVideoChunkRaw's sixth argument (compositionTimeOffset), when provided, must be a real number.");if(m(this,ce,Fe).call(this),!i(this,l).video)throw new Error("No video track declared.");if(typeof i(this,l).fastStart=="object"&&i(this,b).samples.length===i(this,l).fastStart.expectedVideoChunks)throw new Error(`Cannot add more video chunks than specified in 'fastStart' (${i(this,l).fastStart.expectedVideoChunks}).`);let h=m(this,de,Ue).call(this,i(this,b),e,t,s,r,a,o);if(i(this,l).fastStart==="fragmented"&&i(this,T)){for(;i(this,V).length>0&&i(this,V)[0].decodeTimestamp<=h.decodeTimestamp;){let f=i(this,V).shift();m(this,B,D).call(this,i(this,T),f)}h.decodeTimestamp<=i(this,T).lastDecodeTimestamp?m(this,B,D).call(this,i(this,b),h):i(this,L).push(h)}else m(this,B,D).call(this,i(this,b),h)}addAudioChunk(e,t,s){if(!(e instanceof EncodedAudioChunk))throw new TypeError("addAudioChunk's first argument (sample) must be of type EncodedAudioChunk.");if(t&&typeof t!="object")throw new TypeError("addAudioChunk's second argument (meta), when provided, must be an object.");if(s!==void 0&&(!Number.isFinite(s)||s<0))throw new TypeError("addAudioChunk's third argument (timestamp), when provided, must be a non-negative real number.");let r=new Uint8Array(e.byteLength);e.copyTo(r),this.addAudioChunkRaw(r,e.type,s??e.timestamp,e.duration,t)}addAudioChunkRaw(e,t,s,r,a){if(!(e instanceof Uint8Array))throw new TypeError("addAudioChunkRaw's first argument (data) must be an instance of Uint8Array.");if(t!=="key"&&t!=="delta")throw new TypeError("addAudioChunkRaw's second argument (type) must be either 'key' or 'delta'.");if(!Number.isFinite(s)||s<0)throw new TypeError("addAudioChunkRaw's third argument (timestamp) must be a non-negative real number.");if(!Number.isFinite(r)||r<0)throw new TypeError("addAudioChunkRaw's fourth argument (duration) must be a non-negative real number.");if(a&&typeof a!="object")throw new TypeError("addAudioChunkRaw's fifth argument (meta), when provided, must be an object.");if(m(this,ce,Fe).call(this),!i(this,l).audio)throw new Error("No audio track declared.");if(typeof i(this,l).fastStart=="object"&&i(this,T).samples.length===i(this,l).fastStart.expectedAudioChunks)throw new Error(`Cannot add more audio chunks than specified in 'fastStart' (${i(this,l).fastStart.expectedAudioChunks}).`);let o=m(this,de,Ue).call(this,i(this,T),e,t,s,r,a);if(i(this,l).fastStart==="fragmented"&&i(this,b)){for(;i(this,L).length>0&&i(this,L)[0].decodeTimestamp<=o.decodeTimestamp;){let h=i(this,L).shift();m(this,B,D).call(this,i(this,b),h)}o.decodeTimestamp<=i(this,b).lastDecodeTimestamp?m(this,B,D).call(this,i(this,T),o):i(this,V).push(o)}else m(this,B,D).call(this,i(this,T),o)}finalize(){if(i(this,ee))throw new Error("Cannot finalize a muxer more than once.");if(i(this,l).fastStart==="fragmented"){for(let t of i(this,L))m(this,B,D).call(this,i(this,b),t);for(let t of i(this,V))m(this,B,D).call(this,i(this,T),t);m(this,pe,We).call(this,!1)}else i(this,b)&&m(this,te,me).call(this,i(this,b)),i(this,T)&&m(this,te,me).call(this,i(this,T));let e=[i(this,b),i(this,T)].filter(Boolean);if(i(this,l).fastStart==="in-memory"){let t;for(let r=0;r<2;r++){let a=ae(e,i(this,$)),o=i(this,u).measureBox(a);t=i(this,u).measureBox(i(this,_));let h=i(this,u).pos+o+t;for(let f of i(this,H)){f.offset=h;for(let{data:v}of f.samples)h+=v.byteLength,t+=v.byteLength}if(h<2**32)break;t>=2**32&&(i(this,_).largeSize=!0)}let s=ae(e,i(this,$));i(this,u).writeBox(s),i(this,_).size=t,i(this,u).writeBox(i(this,_));for(let r of i(this,H))for(let a of r.samples)i(this,u).write(a.data),a.data=null}else if(i(this,l).fastStart==="fragmented"){let t=i(this,u).pos,s=Xt(e);i(this,u).writeBox(s);let r=i(this,u).pos-t;i(this,u).seek(i(this,u).pos-4),i(this,u).writeU32(r)}else{let t=i(this,u).offsets.get(i(this,_)),s=i(this,u).pos-t;i(this,_).size=s,i(this,_).largeSize=s>=2**32,i(this,u).patchBox(i(this,_));let r=ae(e,i(this,$));if(typeof i(this,l).fastStart=="object"){i(this,u).seek(i(this,ue)),i(this,u).writeBox(r);let a=t-i(this,u).pos;i(this,u).writeBox(ot(a))}else i(this,u).writeBox(r)}m(this,q,ie).call(this),i(this,u).finalize(),y(this,ee,!0)}};l=new WeakMap,u=new WeakMap,ue=new WeakMap,_=new WeakMap,b=new WeakMap,T=new WeakMap,$=new WeakMap,H=new WeakMap,Ae=new WeakMap,L=new WeakMap,V=new WeakMap,ee=new WeakMap,Oe=new WeakSet,Qe=function(e){if(typeof e!="object")throw new TypeError("The muxer requires an options object to be passed to its constructor.");if(!(e.target instanceof ne))throw new TypeError("The target must be provided and an instance of Target.");if(e.video){if(!ai.includes(e.video.codec))throw new TypeError(`Unsupported video codec: ${e.video.codec}`);if(!Number.isInteger(e.video.width)||e.video.width<=0)throw new TypeError(`Invalid video width: ${e.video.width}. Must be a positive integer.`);if(!Number.isInteger(e.video.height)||e.video.height<=0)throw new TypeError(`Invalid video height: ${e.video.height}. Must be a positive integer.`);const t=e.video.rotation;if(typeof t=="number"&&![0,90,180,270].includes(t))throw new TypeError(`Invalid video rotation: ${t}. Has to be 0, 90, 180 or 270.`);if(Array.isArray(t)&&(t.length!==9||t.some(s=>typeof s!="number")))throw new TypeError(`Invalid video transformation matrix: ${t.join()}`);if(e.video.frameRate!==void 0&&(!Number.isInteger(e.video.frameRate)||e.video.frameRate<=0))throw new TypeError(`Invalid video frame rate: ${e.video.frameRate}. Must be a positive integer.`)}if(e.audio){if(!ni.includes(e.audio.codec))throw new TypeError(`Unsupported audio codec: ${e.audio.codec}`);if(!Number.isInteger(e.audio.numberOfChannels)||e.audio.numberOfChannels<=0)throw new TypeError(`Invalid number of audio channels: ${e.audio.numberOfChannels}. Must be a positive integer.`);if(!Number.isInteger(e.audio.sampleRate)||e.audio.sampleRate<=0)throw new TypeError(`Invalid audio sample rate: ${e.audio.sampleRate}. Must be a positive integer.`)}if(e.firstTimestampBehavior&&!hi.includes(e.firstTimestampBehavior))throw new TypeError(`Invalid first timestamp behavior: ${e.firstTimestampBehavior}`);if(typeof e.fastStart=="object"){if(e.video){if(e.fastStart.expectedVideoChunks===void 0)throw new TypeError("'fastStart' is an object but is missing property 'expectedVideoChunks'.");if(!Number.isInteger(e.fastStart.expectedVideoChunks)||e.fastStart.expectedVideoChunks<0)throw new TypeError("'expectedVideoChunks' must be a non-negative integer.")}if(e.audio){if(e.fastStart.expectedAudioChunks===void 0)throw new TypeError("'fastStart' is an object but is missing property 'expectedAudioChunks'.");if(!Number.isInteger(e.fastStart.expectedAudioChunks)||e.fastStart.expectedAudioChunks<0)throw new TypeError("'expectedAudioChunks' must be a non-negative integer.")}}else if(![!1,"in-memory","fragmented"].includes(e.fastStart))throw new TypeError("'fastStart' option must be false, 'in-memory', 'fragmented' or an object.");if(e.minFragmentDuration!==void 0&&(!Number.isFinite(e.minFragmentDuration)||e.minFragmentDuration<0))throw new TypeError("'minFragmentDuration' must be a non-negative number.")},Be=new WeakSet,Ye=function(){if(i(this,u).writeBox(nt({holdsAvc:i(this,l).video?.codec==="avc",fragmented:i(this,l).fastStart==="fragmented"})),y(this,ue,i(this,u).pos),i(this,l).fastStart==="in-memory")y(this,_,ye(!1));else if(i(this,l).fastStart!=="fragmented"){if(typeof i(this,l).fastStart=="object"){let e=m(this,Ie,Je).call(this);i(this,u).seek(i(this,u).pos+e)}y(this,_,ye(!0)),i(this,u).writeBox(i(this,_))}m(this,q,ie).call(this)},Ie=new WeakSet,Je=function(){if(typeof i(this,l).fastStart!="object")return;let e=0,t=[i(this,l).fastStart.expectedVideoChunks,i(this,l).fastStart.expectedAudioChunks];for(let s of t)s&&(e+=8*Math.ceil(2/3*s),e+=4*s,e+=12*Math.ceil(2/3*s),e+=4*s,e+=8*s);return e+=4096,e},Me=new WeakSet,et=function(){if(i(this,l).video&&y(this,b,{id:1,info:{type:"video",codec:i(this,l).video.codec,width:i(this,l).video.width,height:i(this,l).video.height,rotation:i(this,l).video.rotation??0,decoderConfig:null},timescale:i(this,l).video.frameRate??57600,samples:[],finalizedChunks:[],currentChunk:null,firstDecodeTimestamp:void 0,lastDecodeTimestamp:-1,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,compactlyCodedChunkTable:[]}),i(this,l).audio&&(y(this,T,{id:i(this,l).video?2:1,info:{type:"audio",codec:i(this,l).audio.codec,numberOfChannels:i(this,l).audio.numberOfChannels,sampleRate:i(this,l).audio.sampleRate,decoderConfig:null},timescale:i(this,l).audio.sampleRate,samples:[],finalizedChunks:[],currentChunk:null,firstDecodeTimestamp:void 0,lastDecodeTimestamp:-1,timeToSampleTable:[],compositionTimeOffsetTable:[],lastTimescaleUnits:null,lastSample:null,compactlyCodedChunkTable:[]}),i(this,l).audio.codec==="aac")){let e=m(this,De,tt).call(this,2,i(this,l).audio.sampleRate,i(this,l).audio.numberOfChannels);i(this,T).info.decoderConfig={codec:i(this,l).audio.codec,description:e,numberOfChannels:i(this,l).audio.numberOfChannels,sampleRate:i(this,l).audio.sampleRate}}},De=new WeakSet,tt=function(e,t,s){let a=[96e3,88200,64e3,48e3,44100,32e3,24e3,22050,16e3,12e3,11025,8e3,7350].indexOf(t),o=s,h="";h+=e.toString(2).padStart(5,"0"),h+=a.toString(2).padStart(4,"0"),a===15&&(h+=t.toString(2).padStart(24,"0")),h+=o.toString(2).padStart(4,"0");let f=Math.ceil(h.length/8)*8;h=h.padEnd(f,"0");let v=new Uint8Array(h.length/8);for(let S=0;S<h.length;S+=8)v[S/8]=parseInt(h.slice(S,S+8),2);return v},de=new WeakSet,Ue=function(e,t,s,r,a,o,h){let f=r/1e6,v=(r-(h??0))/1e6,S=a/1e6,U=m(this,xe,it).call(this,f,v,e);return f=U.presentationTimestamp,v=U.decodeTimestamp,o?.decoderConfig&&(e.info.decoderConfig===null?e.info.decoderConfig=o.decoderConfig:Object.assign(e.info.decoderConfig,o.decoderConfig)),{presentationTimestamp:f,decodeTimestamp:v,duration:S,data:t,size:t.byteLength,type:s,timescaleUnitsToNextSample:O(S,e.timescale)}},B=new WeakSet,D=function(e,t){i(this,l).fastStart!=="fragmented"&&e.samples.push(t);const s=O(t.presentationTimestamp-t.decodeTimestamp,e.timescale);if(e.lastTimescaleUnits!==null){let a=O(t.decodeTimestamp,e.timescale,!1),o=Math.round(a-e.lastTimescaleUnits);if(e.lastTimescaleUnits+=o,e.lastSample.timescaleUnitsToNextSample=o,i(this,l).fastStart!=="fragmented"){let h=re(e.timeToSampleTable);h.sampleCount===1?(h.sampleDelta=o,h.sampleCount++):h.sampleDelta===o?h.sampleCount++:(h.sampleCount--,e.timeToSampleTable.push({sampleCount:2,sampleDelta:o}));const f=re(e.compositionTimeOffsetTable);f.sampleCompositionTimeOffset===s?f.sampleCount++:e.compositionTimeOffsetTable.push({sampleCount:1,sampleCompositionTimeOffset:s})}}else e.lastTimescaleUnits=0,i(this,l).fastStart!=="fragmented"&&(e.timeToSampleTable.push({sampleCount:1,sampleDelta:O(t.duration,e.timescale)}),e.compositionTimeOffsetTable.push({sampleCount:1,sampleCompositionTimeOffset:s}));e.lastSample=t;let r=!1;if(!e.currentChunk)r=!0;else{let a=t.presentationTimestamp-e.currentChunk.startTimestamp;if(i(this,l).fastStart==="fragmented"){let o=i(this,b)??i(this,T);const h=i(this,l).minFragmentDuration??1;e===o&&t.type==="key"&&a>=h&&(r=!0,m(this,pe,We).call(this))}else r=a>=.5}r&&(e.currentChunk&&m(this,te,me).call(this,e),e.currentChunk={startTimestamp:t.presentationTimestamp,samples:[]}),e.currentChunk.samples.push(t)},xe=new WeakSet,it=function(e,t,s){const r=i(this,l).firstTimestampBehavior==="strict",a=s.lastDecodeTimestamp===-1;if(r&&a&&t!==0)throw new Error(`The first chunk for your media track must have a timestamp of 0 (received DTS=${t}).Non-zero first timestamps are often caused by directly piping frames or audio data from a MediaStreamTrack into the encoder. Their timestamps are typically relative to the age of thedocument, which is probably what you want.
3
+
4
+ If you want to offset all timestamps of a track such that the first one is zero, set firstTimestampBehavior: 'offset' in the options.
5
+ `);if(i(this,l).firstTimestampBehavior==="offset"||i(this,l).firstTimestampBehavior==="cross-track-offset"){s.firstDecodeTimestamp===void 0&&(s.firstDecodeTimestamp=t);let h;i(this,l).firstTimestampBehavior==="offset"?h=s.firstDecodeTimestamp:h=Math.min(i(this,b)?.firstDecodeTimestamp??1/0,i(this,T)?.firstDecodeTimestamp??1/0),t-=h,e-=h}if(t<s.lastDecodeTimestamp)throw new Error(`Timestamps must be monotonically increasing (DTS went from ${s.lastDecodeTimestamp*1e6} to ${t*1e6}).`);return s.lastDecodeTimestamp=t,{presentationTimestamp:e,decodeTimestamp:t}},te=new WeakSet,me=function(e){if(i(this,l).fastStart==="fragmented")throw new Error("Can't finalize individual chunks if 'fastStart' is set to 'fragmented'.");if(e.currentChunk){if(e.finalizedChunks.push(e.currentChunk),i(this,H).push(e.currentChunk),(e.compactlyCodedChunkTable.length===0||re(e.compactlyCodedChunkTable).samplesPerChunk!==e.currentChunk.samples.length)&&e.compactlyCodedChunkTable.push({firstChunk:e.finalizedChunks.length,samplesPerChunk:e.currentChunk.samples.length}),i(this,l).fastStart==="in-memory"){e.currentChunk.offset=0;return}e.currentChunk.offset=i(this,u).pos;for(let t of e.currentChunk.samples)i(this,u).write(t.data),t.data=null;m(this,q,ie).call(this)}},pe=new WeakSet,We=function(e=!0){if(i(this,l).fastStart!=="fragmented")throw new Error("Can't finalize a fragment unless 'fastStart' is set to 'fragmented'.");let t=[i(this,b),i(this,T)].filter(f=>f&&f.currentChunk);if(t.length===0)return;let s=st(this,Ae)._++;if(s===1){let f=ae(t,i(this,$),!0);i(this,u).writeBox(f)}let r=i(this,u).pos,a=$e(s,t);i(this,u).writeBox(a);{let f=ye(!1),v=0;for(let U of t)for(let X of U.currentChunk.samples)v+=X.size;let S=i(this,u).measureBox(f)+v;S>=2**32&&(f.largeSize=!0,S=i(this,u).measureBox(f)+v),f.size=S,i(this,u).writeBox(f)}for(let f of t){f.currentChunk.offset=i(this,u).pos,f.currentChunk.moofOffset=r;for(let v of f.currentChunk.samples)i(this,u).write(v.data),v.data=null}let o=i(this,u).pos;i(this,u).seek(i(this,u).offsets.get(a));let h=$e(s,t);i(this,u).writeBox(h),i(this,u).seek(o);for(let f of t)f.finalizedChunks.push(f.currentChunk),i(this,H).push(f.currentChunk),f.currentChunk=null;e&&m(this,q,ie).call(this)},q=new WeakSet,ie=function(){i(this,u)instanceof Ee&&i(this,u).flush()},ce=new WeakSet,Fe=function(){if(i(this,ee))throw new Error("Cannot add new video or audio chunks after the file has been finalized.")};export{qe as ArrayBufferTarget,Xe as FileSystemWritableFileStreamTarget,li as Muxer,Te as StreamTarget};
@@ -29,6 +29,10 @@ agent/
29
29
  utils.ts configures the harness runtime; the only import surface for actions
30
30
  docs/
31
31
  app-profile.md what is known about this app, and how each fact was established
32
+ video/
33
+ scenes.json the questions and captions of a share video (written when you make one); the videos stay out of git
34
+ .claude/
35
+ skills/share-a-video/ how to make a short video of the agent to share
32
36
  foldspace.dev.json
33
37
  CLAUDE.md imports platform instructions from the installed harness
34
38
  ```
@@ -48,6 +52,7 @@ npm run dev # rebuild dist/index.js on changes
48
52
  npm run inject # open the test window; the human signs in there
49
53
  npx foldspace observe wait-login # returns when they are signed in
50
54
  npx foldspace attach --daemon # load this build into the test window
55
+ npx foldspace record # a short video of the agent working, to share
51
56
  ```
52
57
 
53
58
  Run `inject` before `attach`. `npx foldspace observe` reads the test window,
@@ -11,6 +11,7 @@
11
11
  "Bash(npx foldspace ask:*)",
12
12
  "Bash(npx foldspace badge:*)",
13
13
  "Bash(npx foldspace published:*)",
14
+ "Bash(npx foldspace record:*)",
14
15
  "Bash(npm install:*)",
15
16
  "Bash(npm run build:*)",
16
17
  "Bash(npm run typecheck:*)",
@@ -4,3 +4,6 @@ dist/
4
4
  .DS_Store
5
5
  *.log
6
6
  *.zip
7
+ # A share video shows real data from the account: kept out of git, the human decides where it goes.
8
+ video/*.mp4
9
+ video/*.jpg
@@ -0,0 +1,235 @@
1
+ ---
2
+ name: share-a-video
3
+ description: Make a short video of this agent at work in the product - a real recording of its panel showing the agent's key use cases, with captions and title cards in Foldspace's colours - for the human to share (LinkedIn, X, their team). Use when the human wants a video, demo clip, screen recording or something to post about their agent - not when they want the agent itself to show a video in a card. Offer it in the second wave, once their first experience works and they have tried it.
4
+ ---
5
+
6
+ # Share a video of the agent
7
+
8
+ A video of about 40 seconds that shows what this agent does, made by
9
+ `npx foldspace record`. It opens on "Meet the <product> Product Agent".
10
+ Then the agent's key use cases, one scene each, recorded for real in the
11
+ test window: the question typed key by key, "Thinking...", the card loading,
12
+ the answer streaming in, and a caption - the Foldspace features the scene
13
+ shows as pills ("Action", "Agentic UI"), "2 OF 4", the question in small
14
+ type, and the scene's line in large type: what the user gets, its payoff on a
15
+ blue highlight. A closing card ends it.
16
+
17
+ The look is Foldspace's, as its own product video draws it: its blue on
18
+ black, Poppins, the official logo, small. Not the app's colours. There is
19
+ nothing to set.
20
+
21
+ ## What it shows: say it plainly
22
+
23
+ - **Real data from their account.** The signed-in app as it is: their records,
24
+ their numbers, their name wherever the app or the panel shows it. Nothing is
25
+ blurred, nothing is made up, nothing is uploaded.
26
+ - **Sharing it is their call** - whether, and where. Like a screen recording
27
+ they made themselves.
28
+ - Never create demo data for the video, never blur or crop parts of it, and
29
+ never record with test mode on.
30
+
31
+ ## When to offer it
32
+
33
+ In the second wave: after the first experience works and the human has tried
34
+ it - *"a short video of it to share"*, asked as its own question next to the
35
+ second-wave choice (a choice holds four options). Not before: there is nothing
36
+ to show yet. If they ask earlier, finish the first experience first, then come
37
+ back to it.
38
+
39
+ ## 1. Pick the scenes
40
+
41
+ The video shows the agent's key use cases: the experiences this agent really
42
+ has. One scene for each. Not a list of questions picked at random.
43
+
44
+ - **What counts:** what was built and works today - each card, list or lookup
45
+ the agent has - and what it answers well about the product. Each question
46
+ proven with `npx foldspace ask` today.
47
+ - **One scene per use case.** Two questions that show the same thing are one
48
+ use case: keep the better one.
49
+ - **Most valuable first** - what their users would most want to see. Between
50
+ two close ones, the more visual first: a chart, a card of their numbers,
51
+ then lists. Forms and navigation last, or not at all.
52
+ - **Only scenes that show something** - never one that saves, sends or
53
+ deletes.
54
+ - Do not build new actions just for the video.
55
+
56
+ Four or five make a video of about 40 s; more runs long. **Right after the
57
+ first experience there is often only one or two.** Say so and ask: a shorter
58
+ video now with what there is (recommended), or another experience first.
59
+
60
+ For each scene, agree three things:
61
+
62
+ - **`question`** - as a user would ask it. At most 120 characters; it is
63
+ drawn small, in quotes, above the line.
64
+ - **`capabilities`** - none to three of Foldspace's features: `Action`,
65
+ `Agentic UI`, `Navigation`, `Tandem`, `Human in the loop`, `Voice`. Only
66
+ what the scene showed, or for `Voice` what is switched on (below); the most
67
+ visible first. A plain answer has none.
68
+ - **`line`** - the caption's big words: what the user gets, not how it works.
69
+ Short and benefit-led, about their outcome; one or two words on the
70
+ highlight - the payoff - between asterisks. The product's own name is
71
+ fine; no Foldspace words and no action or tool names. At most two lines
72
+ and 70 characters (the asterisks do not count); `\n` breaks the line
73
+ where you want it, otherwise it wraps evenly by itself. In this voice:
74
+ - `Answers from your *own data,*\nas UI in the conversation.`
75
+ - `It takes them *there.*`
76
+ - `It fills it in. *They click Save.*`
77
+ - `The deals at risk, *ranked,*\nstraight from your CRM.`
78
+
79
+ Not `Calls getDealsAtRisk and renders a table card`: that is the
80
+ mechanics, and nobody watching cares.
81
+
82
+ ### Capabilities: only what `ask` showed
83
+
84
+ Never claim a capability the scene does not show. Name one only when the
85
+ scene's own `npx foldspace ask` showed it. What `ask` reports: `reply`, `cards` (the text
86
+ in each card), `hasCard`, `handler.execute` and `handler.render` with the
87
+ action names in `handler.lines` (only while `npx foldspace attach --daemon`
88
+ runs; otherwise they read "not tracked here (published code)"), `screenshot`
89
+ (the panel), `screenshotFull` (the whole window), and `url` - the page's
90
+ address when the ask began, not after.
91
+
92
+ | Capability | Name it when | `ask` alone cannot tell |
93
+ |---|---|---|
94
+ | `Action` | An action ran: `handler.execute` is `succeeded` and `handler.lines` names the action - and that action's code reaches the product's API | Without attach: execute is not tracked. Run `npx foldspace attach --daemon` and ask again |
95
+ | `Agentic UI` | A card was drawn: `hasCard` is true (`handler.render` `succeeded` with attach) | - |
96
+ | `Navigation` | The agent took the user to another page | `ask` does not report it: `url` is the page before the question. Run `npx foldspace observe pages` before and after the ask and compare the address, or compare the screenshots |
97
+ | `Tandem` | The agent filled or changed the app's own UI - a form, a filter - for the user to review | Nothing in the JSON says so. Run `npx foldspace observe screenshot` before the ask and compare it with `screenshotFull` |
98
+ | `Human in the loop` | The agent waits for the user: a card asks them to confirm or fill something in (an input card) | Read `cards` and the panel `screenshot`: a button to confirm, or fields to fill |
99
+ | `Voice` | Voice is on for this agent, so its users can ask that question out loud: Agent Studio → Style & Behavior → Voice (Speech to Text or real-time voice), or `enableVoiceChat: true` where this project starts the agent. The recording itself types | Look for `enableVoiceChat` in the project's start-up code; otherwise ask the human whether voice is on in Agent Studio. Never assume it |
100
+
101
+ A plain answer - text only, no card, no action - gets no pill but `Voice` (when voice is on): otherwise `"capabilities": []`.
102
+ Not sure about one? Leave it out: one true pill is better than two.
103
+
104
+ Show the list - each scene's question, capabilities and line, in order -
105
+ and ask one choice: record these (recommended), or change something. Agree
106
+ any change in their words.
107
+
108
+ ## 2. Write video/scenes.json
109
+
110
+ ```json
111
+ {
112
+ "product": "Northwind CRM",
113
+ "scenes": [
114
+ {
115
+ "question": "How is my pipeline doing this quarter?",
116
+ "capabilities": ["Agentic UI"],
117
+ "line": "This quarter's pipeline, *by stage,*\nright in the conversation."
118
+ },
119
+ {
120
+ "question": "Which deals are at risk this week?",
121
+ "capabilities": ["Action", "Agentic UI"],
122
+ "line": "The deals at risk, *ranked,*\nstraight from your CRM."
123
+ }
124
+ ]
125
+ }
126
+ ```
127
+
128
+ - `product` as they call it: the opening reads "Meet the Northwind CRM
129
+ Product Agent".
130
+ - `capabilities`: up to three, spelled as above (any case works), drawn in
131
+ that order; `[]` for none. `record` adds "2 OF 4" beside them.
132
+ - `line`: `*...*` puts words on the blue highlight - at most two highlights,
133
+ none across the `\n`. A line too long for two lines at full size is drawn
134
+ smaller, and `record` warns: shorten it.
135
+ - `endCard` (default `true`) is the closing card: "An agent that does the
136
+ work.", Built with Foldspace, foldspace.ai. `false` - or `--no-end-card` -
137
+ leaves it out.
138
+ - `record` checks the file first and names any field that is wrong.
139
+
140
+ ## 3. Before recording, say so
141
+
142
+ > I'm recording the video now - it takes a few minutes. Please leave the
143
+ > Chrome window alone until I say it's done: keep it where you can see it -
144
+ > not minimized, not behind another window - and don't type or click in it.
145
+ > The video will show real data from your account; whether to share it is up
146
+ > to you.
147
+
148
+ The window stays the size it is: the whole of it goes in the video, scaled to
149
+ fit, never cropped.
150
+
151
+ ## 4. Record
152
+
153
+ ```bash
154
+ npx foldspace record # 1920x1080
155
+ npx foldspace record --preset square # 1080x1080, only if they ask for square
156
+ ```
157
+
158
+ It runs 2-4 minutes. Give the command a 10-minute timeout (in Claude Code,
159
+ `timeout: 600000`) - the 2-minute default cuts it off half-way. Stopped
160
+ half-way, it puts the pill and the corner tag back and says `interrupted`; if
161
+ the corner tag is still missing after that, `npx foldspace badge label`.
162
+
163
+ For each scene it starts a new conversation (the panel's New Chat button),
164
+ types the question, waits for the answer to settle and keeps a few seconds of
165
+ it. It records with Chrome's own capture: no screen-recording permission, and
166
+ the window is never brought to the front. The pill and the corner tag are
167
+ hidden while it records and come back after. Then it makes the video in a
168
+ hidden Chrome: long waits shortened (the thinking is kept), short dissolves
169
+ between scenes, each caption fading in once its answer has settled.
170
+
171
+ Each question is a real conversation, so it shows in their Foldspace
172
+ dashboard.
173
+
174
+ It prints one JSON summary: `video`, `poster`, `durationS`, `sizeMB`,
175
+ `scenes` (each with its `lastFrame`), `recordDir`.
176
+
177
+ ## 5. Look before you hand it over
178
+
179
+ If `ok` is false, find its `code` in the table below. If true, read the poster
180
+ (scene 1 with its caption) and every scene's `lastFrame` - the answer as it
181
+ ended: each answer complete, none an error or "that action isn't available",
182
+ and each one shows what its capabilities and line say. A bad one: offer
183
+ another question for that use case, prove it, record again. If there is a
184
+ `note` that it ran long, offer to drop the last scene.
185
+
186
+ ## 6. Hand it over
187
+
188
+ - `npx foldspace badge ready`, and say the Chrome window is theirs again.
189
+ - Send the MP4 and the poster (`SendUserFile` where you have it; otherwise
190
+ give the paths).
191
+ - Say plainly what is in it and whose call it is: *"It shows real data from
192
+ your account - [what is on screen, for example your pipeline numbers, your
193
+ name, and the names of the deals and contacts in the list]. Whether to share
194
+ it, and where, is your call."*
195
+ - Suggest the post: two or three short lines in their voice - the use cases,
196
+ what the agent does for their users, and that it was built with Foldspace.
197
+ They edit it.
198
+ - Never post it for them, and never upload it anywhere on your own: where it
199
+ goes is their call.
200
+
201
+ **Sharing:** upload the MP4 itself to LinkedIn or X - that is also how it
202
+ plays inline in the feed. A Foldspace share link (a page anyone can open to
203
+ watch it) is coming; until then, the file.
204
+
205
+ The video holds real data, so it is never saved with the code: `record` writes
206
+ `video/.gitignore` (`*.mp4`, `*.jpg`) in every project. `video/scenes.json`
207
+ stays in git.
208
+
209
+ ## Change a caption without recording again
210
+
211
+ Edit `video/scenes.json` - a scene's `capabilities` or `line` (the words,
212
+ the highlight, where it breaks), the `product` name, `endCard` - then make
213
+ the video again from the same recording:
214
+
215
+ ```bash
216
+ npx foldspace record --from <recordDir from the summary>
217
+ npx foldspace record --from <recordDir> --preset square # a square one, same recording
218
+ ```
219
+
220
+ A changed question needs a new recording. The three latest finished
221
+ recordings are kept in `.foldspace-dev/record/`.
222
+
223
+ ## When it stops
224
+
225
+ | `code` | What to do |
226
+ |---|---|
227
+ | `bad_scenes`, `no_scenes` | Write or fix `video/scenes.json`; `errors` names each field |
228
+ | `no_h264`, `no_chrome` | The video needs Google Chrome (some Chromium builds cannot make it): ask them to install it, or set `CHROME_PATH`. A kept recording then needs only `--from` |
229
+ | `test_mode` | attach runs with `--test-mode`; its banner would be in the video. Ask: the video's questions will show in their Foldspace dashboard, next to their users' conversations - record anyway? On a yes: `npx foldspace attach --stop`, attach again with the same flags minus `--test-mode`, record, then attach again with `--test-mode` |
230
+ | `no_panel`, `no_composer` | The agent is not showing in the test window. Do what `reason` says, check with `npx foldspace ask "<scene 1's question>"`, then record again |
231
+ | `no_reply`, `not_settled`, `not_sent` | Prove that question with `npx foldspace ask`, or pick another, then record again |
232
+ | `no_frames`, `hidden` | The window was minimized or covered: ask them to keep it in view, then record again |
233
+ | `interrupted` | It was stopped (Ctrl-C or a timeout). Run it again with the 10-minute timeout; `reason` says if a finished recording can be remade with `--from` |
234
+ | `recording_incomplete`, `scenes_changed` | That recording cannot make this video (a scene never finished, or a question changed): record again |
235
+ | anything else | `reason` says what happened. A finished recording is kept: once it is fixed, `npx foldspace record --from <recordDir>` makes the video without asking anything again |