@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.
- package/CLAUDE.md +27 -8
- package/README.md +25 -2
- package/bin/ask.mjs +16 -145
- package/bin/attach.mjs +2 -0
- package/bin/cli.mjs +22 -0
- package/bin/inject.mjs +2 -14
- package/bin/record.mjs +412 -0
- package/bin/share.mjs +63 -0
- package/package.json +1 -1
- package/src/attach-daemon.mjs +18 -0
- package/src/badge-auto.mjs +38 -1
- package/src/cdp-client.mjs +7 -1
- package/src/chrome-binary.mjs +28 -0
- package/src/cli-help.mjs +1 -0
- package/src/cli-registry.mjs +97 -3
- package/src/init.mjs +170 -16
- package/src/panel-driver.mjs +283 -0
- package/src/session-events.mjs +1 -0
- package/src/upgrade.mjs +87 -1
- package/src/video/README.md +29 -0
- package/src/video/assets/foldspace-mark.svg +27 -0
- package/src/video/assets/logo-dark.svg +68 -0
- package/src/video/assets/logo-light.svg +56 -0
- package/src/video/brand.mjs +46 -0
- package/src/video/compose.mjs +476 -0
- package/src/video/composer/composer.mjs +554 -0
- package/src/video/composer/index.html +13 -0
- package/src/video/fonts/OFL.txt +93 -0
- package/src/video/fonts/Poppins-ExtraBold.woff2 +0 -0
- package/src/video/fonts/Poppins-Regular.woff2 +0 -0
- package/src/video/fonts/Poppins-SemiBold.woff2 +0 -0
- package/src/video/recorder.mjs +331 -0
- package/src/video/scenes.mjs +253 -0
- package/src/video/share.mjs +284 -0
- package/src/video/timeline.mjs +726 -0
- package/src/video/vendor/mp4-muxer.LICENSE +21 -0
- package/src/video/vendor/mp4-muxer.min.mjs +5 -0
- package/templates/agent-starter/README.md +5 -0
- package/templates/agent-starter/claude-settings.json +1 -0
- package/templates/agent-starter/gitignore +3 -0
- 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,
|
|
@@ -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 |
|