@foldspace_npm/harness 0.1.31 → 0.1.33

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 (41) 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/bin/share.mjs +63 -0
  9. package/package.json +1 -1
  10. package/src/attach-daemon.mjs +18 -0
  11. package/src/badge-auto.mjs +38 -1
  12. package/src/cdp-client.mjs +7 -1
  13. package/src/chrome-binary.mjs +28 -0
  14. package/src/cli-help.mjs +1 -0
  15. package/src/cli-registry.mjs +97 -3
  16. package/src/init.mjs +170 -16
  17. package/src/panel-driver.mjs +283 -0
  18. package/src/session-events.mjs +1 -0
  19. package/src/upgrade.mjs +87 -1
  20. package/src/video/README.md +29 -0
  21. package/src/video/assets/foldspace-mark.svg +27 -0
  22. package/src/video/assets/logo-dark.svg +68 -0
  23. package/src/video/assets/logo-light.svg +56 -0
  24. package/src/video/brand.mjs +46 -0
  25. package/src/video/compose.mjs +476 -0
  26. package/src/video/composer/composer.mjs +554 -0
  27. package/src/video/composer/index.html +13 -0
  28. package/src/video/fonts/OFL.txt +93 -0
  29. package/src/video/fonts/Poppins-ExtraBold.woff2 +0 -0
  30. package/src/video/fonts/Poppins-Regular.woff2 +0 -0
  31. package/src/video/fonts/Poppins-SemiBold.woff2 +0 -0
  32. package/src/video/recorder.mjs +331 -0
  33. package/src/video/scenes.mjs +253 -0
  34. package/src/video/share.mjs +284 -0
  35. package/src/video/timeline.mjs +726 -0
  36. package/src/video/vendor/mp4-muxer.LICENSE +21 -0
  37. package/src/video/vendor/mp4-muxer.min.mjs +5 -0
  38. package/templates/agent-starter/README.md +5 -0
  39. package/templates/agent-starter/claude-settings.json +1 -0
  40. package/templates/agent-starter/gitignore +3 -0
  41. package/templates/agent-starter/skills/share-a-video/SKILL.md +271 -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,271 @@
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, and nothing is uploaded unless the human asks
26
+ for a share link (step 7).
27
+ - **Sharing it is their call** - whether, and where. Like a screen recording
28
+ they made themselves.
29
+ - Never create demo data for the video, never blur or crop parts of it, and
30
+ never record with test mode on.
31
+
32
+ ## When to offer it
33
+
34
+ In the second wave: after the first experience works and the human has tried
35
+ it - *"a short video of it to share"*, asked as its own question next to the
36
+ second-wave choice (a choice holds four options). Not before: there is nothing
37
+ to show yet. If they ask earlier, finish the first experience first, then come
38
+ back to it.
39
+
40
+ ## 1. Pick the scenes
41
+
42
+ The video shows the agent's key use cases: the experiences this agent really
43
+ has. One scene for each. Not a list of questions picked at random.
44
+
45
+ - **What counts:** what was built and works today - each card, list or lookup
46
+ the agent has - and what it answers well about the product. Each question
47
+ proven with `npx foldspace ask` today.
48
+ - **One scene per use case.** Two questions that show the same thing are one
49
+ use case: keep the better one.
50
+ - **Most valuable first** - what their users would most want to see. Between
51
+ two close ones, the more visual first: a chart, a card of their numbers,
52
+ then lists. Forms and navigation last, or not at all.
53
+ - **Only scenes that show something** - never one that saves, sends or
54
+ deletes.
55
+ - Do not build new actions just for the video.
56
+
57
+ Four or five make a video of about 40 s; more runs long. **Right after the
58
+ first experience there is often only one or two.** Say so and ask: a shorter
59
+ video now with what there is (recommended), or another experience first.
60
+
61
+ For each scene, agree three things:
62
+
63
+ - **`question`** - as a user would ask it. At most 120 characters; it is
64
+ drawn small, in quotes, above the line.
65
+ - **`capabilities`** - none to three of Foldspace's features: `Action`,
66
+ `Agentic UI`, `Navigation`, `Tandem`, `Human in the loop`, `Voice`. Only
67
+ what the scene showed, or for `Voice` what is switched on (below); the most
68
+ visible first. A plain answer has none.
69
+ - **`line`** - the caption's big words: what the user gets, not how it works.
70
+ Short and benefit-led, about their outcome; one or two words on the
71
+ highlight - the payoff - between asterisks. The product's own name is
72
+ fine; no Foldspace words and no action or tool names. At most two lines
73
+ and 70 characters (the asterisks do not count); `\n` breaks the line
74
+ where you want it, otherwise it wraps evenly by itself. In this voice:
75
+ - `Answers from your *own data,*\nas UI in the conversation.`
76
+ - `It takes them *there.*`
77
+ - `It fills it in. *They click Save.*`
78
+ - `The deals at risk, *ranked,*\nstraight from your CRM.`
79
+
80
+ Not `Calls getDealsAtRisk and renders a table card`: that is the
81
+ mechanics, and nobody watching cares.
82
+
83
+ ### Capabilities: only what `ask` showed
84
+
85
+ Never claim a capability the scene does not show. Name one only when the
86
+ scene's own `npx foldspace ask` showed it. What `ask` reports: `reply`, `cards` (the text
87
+ in each card), `hasCard`, `handler.execute` and `handler.render` with the
88
+ action names in `handler.lines` (only while `npx foldspace attach --daemon`
89
+ runs; otherwise they read "not tracked here (published code)"), `screenshot`
90
+ (the panel), `screenshotFull` (the whole window), and `url` - the page's
91
+ address when the ask began, not after.
92
+
93
+ | Capability | Name it when | `ask` alone cannot tell |
94
+ |---|---|---|
95
+ | `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 |
96
+ | `Agentic UI` | A card was drawn: `hasCard` is true (`handler.render` `succeeded` with attach) | - |
97
+ | `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 |
98
+ | `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` |
99
+ | `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 |
100
+ | `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 |
101
+
102
+ A plain answer - text only, no card, no action - gets no pill but `Voice` (when voice is on): otherwise `"capabilities": []`.
103
+ Not sure about one? Leave it out: one true pill is better than two.
104
+
105
+ Show the list - each scene's question, capabilities and line, in order -
106
+ and ask one choice: record these (recommended), or change something. Agree
107
+ any change in their words.
108
+
109
+ ## 2. Write video/scenes.json
110
+
111
+ ```json
112
+ {
113
+ "product": "Northwind CRM",
114
+ "scenes": [
115
+ {
116
+ "question": "How is my pipeline doing this quarter?",
117
+ "capabilities": ["Agentic UI"],
118
+ "line": "This quarter's pipeline, *by stage,*\nright in the conversation."
119
+ },
120
+ {
121
+ "question": "Which deals are at risk this week?",
122
+ "capabilities": ["Action", "Agentic UI"],
123
+ "line": "The deals at risk, *ranked,*\nstraight from your CRM."
124
+ }
125
+ ]
126
+ }
127
+ ```
128
+
129
+ - `product` as they call it: the opening reads "Meet the Northwind CRM
130
+ Product Agent".
131
+ - `capabilities`: up to three, spelled as above (any case works), drawn in
132
+ that order; `[]` for none. `record` adds "2 OF 4" beside them.
133
+ - `line`: `*...*` puts words on the blue highlight - at most two highlights,
134
+ none across the `\n`. A line too long for two lines at full size is drawn
135
+ smaller, and `record` warns: shorten it.
136
+ - `endCard` (default `true`) is the closing card: "An agent that does the
137
+ work.", Built with Foldspace, foldspace.ai. `false` - or `--no-end-card` -
138
+ leaves it out.
139
+ - `record` checks the file first and names any field that is wrong.
140
+
141
+ ## 3. Before recording, say so
142
+
143
+ > I'm recording the video now - it takes a few minutes. Please leave the
144
+ > Chrome window alone until I say it's done: keep it where you can see it -
145
+ > not minimized, not behind another window - and don't type or click in it.
146
+ > The video will show real data from your account; whether to share it is up
147
+ > to you.
148
+
149
+ The window stays the size it is: the whole of it goes in the video, scaled to
150
+ fit, never cropped.
151
+
152
+ ## 4. Record
153
+
154
+ ```bash
155
+ npx foldspace record # 1920x1080
156
+ npx foldspace record --preset square # 1080x1080, only if they ask for square
157
+ ```
158
+
159
+ It runs 2-4 minutes. Give the command a 10-minute timeout (in Claude Code,
160
+ `timeout: 600000`) - the 2-minute default cuts it off half-way. Stopped
161
+ half-way, it puts the pill and the corner tag back and says `interrupted`; if
162
+ the corner tag is still missing after that, `npx foldspace badge label`.
163
+
164
+ For each scene it starts a new conversation (the panel's New Chat button),
165
+ types the question, waits for the answer to settle and keeps a few seconds of
166
+ it. It records with Chrome's own capture: no screen-recording permission, and
167
+ the window is never brought to the front. The pill and the corner tag are
168
+ hidden while it records and come back after. Then it makes the video in a
169
+ hidden Chrome: long waits shortened (the thinking is kept), short dissolves
170
+ between scenes, each caption fading in once its answer has settled.
171
+
172
+ Each question is a real conversation, so it shows in their Foldspace
173
+ dashboard.
174
+
175
+ It prints one JSON summary: `video`, `poster`, `durationS`, `sizeMB`,
176
+ `scenes` (each with its `lastFrame`), `recordDir`.
177
+
178
+ ## 5. Look before you hand it over
179
+
180
+ If `ok` is false, find its `code` in the table below. If true, read the poster
181
+ (scene 1 with its caption) and every scene's `lastFrame` - the answer as it
182
+ ended: each answer complete, none an error or "that action isn't available",
183
+ and each one shows what its capabilities and line say. A bad one: offer
184
+ another question for that use case, prove it, record again. If there is a
185
+ `note` that it ran long, offer to drop the last scene.
186
+
187
+ ## 6. Hand it over
188
+
189
+ - `npx foldspace badge ready`, and say the Chrome window is theirs again.
190
+ - Send the MP4 and the poster (`SendUserFile` where you have it; otherwise
191
+ give the paths).
192
+ - Say plainly what is in it and whose call it is: *"It shows real data from
193
+ your account - [what is on screen, for example your pipeline numbers, your
194
+ name, and the names of the deals and contacts in the list]. Whether to share
195
+ it, and where, is your call."*
196
+ - Suggest the post: two or three short lines in their voice - the use cases,
197
+ what the agent does for their users, and that it was built with Foldspace.
198
+ They edit it.
199
+ - Never post it for them, and never upload it anywhere on your own: where it
200
+ goes is their call.
201
+
202
+ **Sharing:** upload the MP4 itself to LinkedIn or X - that is also how it
203
+ plays inline in the feed. Or a Foldspace share link (step 7): a page anyone
204
+ with the link can open to watch it, with a "Build yours" button, and an email
205
+ to them when it is watched.
206
+
207
+ ## 7. A Foldspace share link - only when they ask for one
208
+
209
+ Offer it as one option next to "send me the file", never as the default.
210
+ The link is public: anyone who has it can watch, with no sign-in. So before
211
+ anything leaves this computer, ask once, as a choice: *"A share link is public
212
+ to anyone who has it, and the video shows [what is on screen]. Create one?"*
213
+ Only on a yes:
214
+
215
+ 1. `npx foldspace share inspect` (the newest `video/*.mp4` and its poster, or
216
+ pass the path). It uploads nothing. If it says the MP4 is not fast-start,
217
+ that file did not come from `record`: say so and stop.
218
+ 2. The Foldspace tool `create_video_share` with a title in their words (the
219
+ product and what the agent does, up to 200 characters), and `video_bytes`
220
+ and `poster_bytes` from step 1.
221
+ 3. Run the upload command it returns, exactly - it carries one-time upload
222
+ targets for 30 minutes. Pass `upload` the same video path and `--poster`
223
+ you gave `inspect`, if any, so it uploads the files `inspect` sized. Never
224
+ paste the targets into chat, a file or a commit. In Codex (or any editor
225
+ that runs commands in a sandbox), this command needs the network: run it
226
+ outside the sandbox through the editor's own approval, with one plain line
227
+ on why.
228
+ 4. When it prints `"status": "uploaded"`, the tool `publish_video_share` with
229
+ the `share_id`. It returns the link: give it to them as it is.
230
+ 5. Tell them in one line that they get an email at the first view and a daily
231
+ summary on days it is watched, and that you can show the numbers
232
+ (`get_video_share_stats`) or delete the link (`delete_video_share`) whenever
233
+ they ask.
234
+
235
+ The tools refuse with a reason you pass on as it is: an EU account (share
236
+ links are not offered there yet - send the file instead), a cap (shares per
237
+ day, or a trial's monthly limit), or a sign-in by API key rather than as a
238
+ person. The CLI never signs in to Foldspace and never reaches the Foldspace
239
+ tools itself: steps 2 and 4 are tool calls you make.
240
+
241
+ The video holds real data, so it is never saved with the code: `record` writes
242
+ `video/.gitignore` (`*.mp4`, `*.jpg`) in every project. `video/scenes.json`
243
+ stays in git.
244
+
245
+ ## Change a caption without recording again
246
+
247
+ Edit `video/scenes.json` - a scene's `capabilities` or `line` (the words,
248
+ the highlight, where it breaks), the `product` name, `endCard` - then make
249
+ the video again from the same recording:
250
+
251
+ ```bash
252
+ npx foldspace record --from <recordDir from the summary>
253
+ npx foldspace record --from <recordDir> --preset square # a square one, same recording
254
+ ```
255
+
256
+ A changed question needs a new recording. The three latest finished
257
+ recordings are kept in `.foldspace-dev/record/`.
258
+
259
+ ## When it stops
260
+
261
+ | `code` | What to do |
262
+ |---|---|
263
+ | `bad_scenes`, `no_scenes` | Write or fix `video/scenes.json`; `errors` names each field |
264
+ | `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` |
265
+ | `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` |
266
+ | `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 |
267
+ | `no_reply`, `not_settled`, `not_sent` | Prove that question with `npx foldspace ask`, or pick another, then record again |
268
+ | `no_frames`, `hidden` | The window was minimized or covered: ask them to keep it in view, then record again |
269
+ | `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` |
270
+ | `recording_incomplete`, `scenes_changed` | That recording cannot make this video (a scene never finished, or a question changed): record again |
271
+ | 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 |