@seshat.sh/cli 0.4.0 → 0.5.1

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/README.md CHANGED
@@ -66,9 +66,55 @@ Uploads show connection, reading and transfer progress on stderr, then keep a sp
66
66
 
67
67
  All commands show readable results and errors by default. Pass `--json` for structured results and errors, including in scripts and pipelines. Login instructions and errors use stderr. Return the upload’s `url` to the user. Reuse a request ID only for retries of the exact same file and metadata. Files may be up to 10 MiB and do not expire by default. HTML should be self-contained. Keep secrets out of shared artifacts.
68
68
 
69
- ## Local MCP
69
+ ## Upload several files, or a folder
70
70
 
71
- `seshat mcp` starts the existing stdio MCP client and reuses the stored profile. Configure `SESHAT_UPLOAD_ROOTS` as a JSON array of allowed directories to enable local file uploads. Without it, inline uploads work and file access is disabled. A hosted MCP endpoint is not provided yet.
71
+ Pass more than one file to publish each as its own artifact:
72
+
73
+ ```sh
74
+ seshat upload report.html notes.md chart.png
75
+ ```
76
+
77
+ `--title`, `--description`, `--content-type` and stdin apply to a single upload. With `--request-id`, each file gets that ID plus its position, so repeating the command adds nothing twice. One bad path does not stop the others; the command reports it and exits non-zero.
78
+
79
+ To keep files together under one link, gather them into a folder:
80
+
81
+ ```sh
82
+ seshat upload ./dist --folder "Launch site"
83
+ seshat upload ~/Desktop/logo.png ./exports/deck.pdf --folder "Launch kit"
84
+ seshat upload ./photos --into FOLDER_ID_OR_LINK --prefix images
85
+ ```
86
+
87
+ - `--folder NAME` creates a folder. `--into FOLDER` adds to one that exists; it takes a folder ID or its share link.
88
+ - A file is stored under its own name. A directory contributes its contents and keeps their layout, so `./dist/index.html` becomes `index.html`.
89
+ - `--prefix PATH` places everything from that command in a subfolder. Two sources that would land on the same path are refused before anything is sent.
90
+ - Hidden files and `node_modules` are skipped unless you pass `--include-hidden`. Symbolic links are never followed.
91
+ - A folder holds up to 100 files and 100 MiB, 10 MiB per file.
92
+ - Files already stored unchanged are not sent again. If an upload is interrupted, repeat the same command with the same `--request-id`, or use `--into` with the folder ID from the output.
93
+
94
+ ## Use with an AI assistant (MCP)
95
+
96
+ Assistants such as Claude Code, Claude Desktop, Cursor and Codex can publish to Seshat through MCP. You do not start anything yourself: the assistant runs `seshat mcp` as a local server when it needs it and signs in with a profile you approved.
97
+
98
+ ```sh
99
+ seshat agent setup --profile my-agent
100
+ claude mcp add --env SESHAT_PROFILE=my-agent --transport stdio seshat -- npx -y @seshat.sh/cli mcp
101
+ ```
102
+
103
+ For Claude Desktop or Cursor, add the same command to the assistant's MCP settings file:
104
+
105
+ ```json
106
+ {
107
+ "mcpServers": {
108
+ "seshat": {
109
+ "command": "npx",
110
+ "args": ["-y", "@seshat.sh/cli", "mcp"],
111
+ "env": { "SESHAT_PROFILE": "my-agent" }
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ Without further setup the assistant can publish what it writes. To let it upload existing files, also set `SESHAT_UPLOAD_ROOTS` to a JSON array of allowed directories; files outside them are refused. A hosted MCP endpoint is not provided yet.
72
118
 
73
119
  This package contains only the service client. Server implementation and deployment sources are not distributed with it.
74
120
 
@@ -0,0 +1,3 @@
1
+ import{z as o}from"zod";import{z as ue}from"zod";var C=ue.enum(["never","hour","day","week","month","year","custom"]);var S=10*1024*1024,U=100,I=100*1024*1024,Y=["artifacts:read","artifacts:write","artifacts:delete","tokens:manage"],j=o.enum(Y),d=o.string().regex(/^[a-f0-9]{48}$/),$=o.string().min(8).max(128).regex(/^[A-Za-z0-9_-]+$/),b=o.string().regex(/^seshat_[A-Za-z0-9_-]{43}$/),J=o.iso.datetime().transform(t=>new Date(t).toISOString());function D(t){return t.expirationPreset==null||(t.expirationPreset==="never"?t.expiresAt===null:typeof t.expiresAt=="string")}var w=o.object({title:o.string().trim().min(1).max(200).optional(),description:o.string().max(2e3).default(""),expiresAt:J.nullable().optional(),expirationPreset:C.nullable().optional(),password:o.string().min(12).max(128).nullable().optional(),generateMetadata:o.boolean().optional()}).strict().refine(D,"Expiration preset must match an explicit expiration value"),L=o.object({filename:o.string().min(1).max(200).regex(/^[^/\\\x00-\x1f\x7f]+$/),contentType:o.string().max(150).regex(/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/i),size:o.number().int().min(1).max(S)}).strict(),Z=o.object({title:w.shape.title.optional(),description:o.string().max(2e3).optional(),expiresAt:w.shape.expiresAt,expirationPreset:w.shape.expirationPreset,password:w.shape.password}).strict().refine(D,"Expiration preset must match an explicit expiration value").refine(t=>Object.values(t).some(e=>e!==void 0),"Provide at least one metadata field"),T=o.string().min(1).max(400).regex(/^[^\\\x00-\x1f\x7f]+$/).refine(t=>{let e=t.split("/");return e.length<=12&&e.every(r=>r.length>0&&r.length<=200&&r!=="."&&r!==".."&&!r.startsWith("~"))},"Use a relative path such as css/site.css"),G=o.object({kind:o.literal("folder"),name:L.shape.filename,title:w.shape.title,description:w.shape.description,expiresAt:w.shape.expiresAt,expirationPreset:w.shape.expirationPreset,password:w.shape.password}).strict().refine(D,"Expiration preset must match an explicit expiration value"),R=o.object({path:T,contentType:o.string(),size:o.number(),digest:o.string().regex(/^[a-f0-9]{64}$/)}),K=o.object({files:o.array(R)}),Q=o.object({slug:o.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).min(2).max(63),name:o.string().trim().min(1).max(100)}).strict(),y=o.object({name:o.string().trim().min(1).max(100),scopes:o.array(j).min(1).max(Y.length),expiresAt:J.optional()}).strict(),v=o.object({id:d,title:o.string(),description:o.string(),filename:o.string(),contentType:o.string(),size:o.number(),createdAt:o.string(),updatedAt:o.string().nullable().default(null),expiresAt:o.string().nullable(),expirationPreset:C.nullable().default(null),url:o.url(),downloadUrl:o.url(),passwordProtected:o.boolean().default(!1),kind:o.enum(["file","folder"]).default("file"),views:o.object({owner:o.number(),others:o.number()})}),V=o.object({artifacts:o.array(v),nextCursor:o.string().nullable()}),z=o.object({id:d,token:b,scopes:o.array(j),expiresAt:o.string().nullable()}),_=o.object({space:o.object({id:d,slug:o.string(),name:o.string()}),credential:z}),ee=o.object({spaceId:d,tokenId:d,scopes:y.shape.scopes,space:_.shape.space.optional()}),te=o.object({error:o.object({code:o.string(),message:o.string()})});import{z as u}from"zod";import{z as i}from"zod";var re="https://app.seshat.sh",A=i.url().refine(t=>{let e=new URL(t);return!e.username&&!e.password&&!e.search&&!e.hash&&e.pathname==="/"&&(e.protocol==="https:"||e.protocol==="http:"&&["localhost","127.0.0.1","[::1]"].includes(e.hostname))},"Use an HTTPS origin, or HTTP localhost for development").transform(t=>new URL(t).origin),O=i.string().min(1).max(64).regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/),Je=i.string().max(16).transform(t=>t.toUpperCase().replaceAll("-","").trim()).pipe(i.string().regex(/^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{8}$/)),M=i.string().trim().min(1).max(80).regex(/^[^\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]+$/),ge=i.strictObject({city:M.optional(),region:M.optional(),country:i.string().regex(/^[A-Z]{2}$/).optional()}).refine(t=>!!(t.city||t.region||t.country),"Location is unavailable"),q=i.strictObject({kind:i.enum(["laptop","desktop","phone","tablet","terminal"]),os:i.enum(["macos","windows","linux","android","ios","other"]),model:M.optional(),client:i.enum(["terminal","agent"])});var Ze=y.extend({tokenHash:i.string().regex(/^[a-f0-9]{64}$/),device:q.optional()}).strict().refine(t=>!t.expiresAt||Date.parse(t.expiresAt)>Date.now()&&Date.parse(t.expiresAt)<=Date.now()+366*864e5,"Choose a credential expiry within the next year"),Ge=i.strictObject({deviceCode:d}),Ke=i.strictObject({approve:i.boolean()}),F=i.object({id:d,slug:i.string(),name:i.string()}),Qe=i.object({deviceCode:d,userCode:i.string().regex(/^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{4}-[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{4}$/),verificationUrl:i.url(),expiresAt:i.iso.datetime(),interval:i.number().int().min(1).max(60)}),Ve=i.discriminatedUnion("status",[i.object({status:i.literal("pending"),interval:i.number().int().min(1).max(60)}),i.object({status:i.literal("denied")}),i.object({status:i.literal("approved"),id:d,space:F,scopes:y.shape.scopes,expiresAt:i.iso.datetime()})]),et=i.object({device:q.nullable().default(null),userCode:i.string(),name:i.string(),scopes:y.shape.scopes,status:i.enum(["pending","approved","denied"]),expiresAt:i.iso.datetime(),tokenExpiresAt:i.iso.datetime(),space:F});var tt=i.object({devices:i.array(i.object({id:d,name:i.string(),device:q.nullable().default(null),scopes:y.shape.scopes,createdAt:i.iso.datetime(),completedAt:i.iso.datetime().nullable(),lastUsedAt:i.iso.datetime().nullable(),lastIp:i.union([i.ipv4(),i.ipv6()]).nullable().default(null),lastLocation:ge.nullable().default(null),expiresAt:i.iso.datetime().nullable()}))});var we=u.object({endpoint:A,token:b}).strict();async function ye({form:t,onProgress:e}){let r=new Response(t),n=r.headers.get("content-type");if(!n)throw new Error("Could not encode the upload");let a=new Uint8Array(await r.arrayBuffer()),s=0;return e({loadedBytes:0,totalBytes:a.byteLength}),{body:new ReadableStream({pull(p){let c=Math.min(s+65536,a.byteLength);p.enqueue(a.subarray(s,c)),s=c,e({loadedBytes:s,totalBytes:a.byteLength}),s===a.byteLength&&p.close()}},{highWaterMark:0}),headers:{"content-type":n,"content-length":String(a.byteLength)},duplex:"half"}}var k=class extends Error{constructor(r,n,a){super(n);this.code=r;this.status=a}code;status},P=class{settings;constructor(e){this.settings=we.parse(e)}async request(e,r,n={}){let a=new Headers(n.headers);a.set("authorization",`Bearer ${this.settings.token}`),typeof n.body=="string"&&a.set("content-type","application/json");let s;try{s=await fetch(new URL(e,this.settings.endpoint),{...n,headers:a,redirect:"error",signal:n.signal?AbortSignal.any([n.signal,AbortSignal.timeout(6e4)]):AbortSignal.timeout(6e4)})}catch{throw new k("connection_failed","Request failed or timed out. Check SESHAT_ENDPOINT; retry uploads with the same --request-id")}let l=await s.json();if(!s.ok){let p=te.safeParse(l);throw new k(p.success?p.data.error.code:"request_failed",p.success?p.data.error.message:`HTTP ${s.status}`,s.status)}return r.parse(l)}async upload(e){let r=L.parse({filename:e.filename,contentType:e.contentType,size:e.bytes.byteLength}),n=w.parse(e.metadata),a=$.parse(e.requestId),s=new FormData;if(s.set("metadata",JSON.stringify(n)),s.set("file",new Blob([new Uint8Array(e.bytes)],{type:r.contentType}),r.filename),e.onProgress){let l=await ye({form:s,onProgress:e.onProgress}),p={...l,method:"POST",headers:{...l.headers,"idempotency-key":a}};return this.request("/v1/artifacts",v,p)}return this.request("/v1/artifacts",v,{method:"POST",body:s,headers:{"idempotency-key":a}})}list(e={}){let r=new URLSearchParams;return e.limit!==void 0&&r.set("limit",String(e.limit)),e.cursor!==void 0&&r.set("cursor",e.cursor),this.request(`/v1/artifacts?${r}`,V)}get(e){return this.request(`/v1/artifacts/${d.parse(e)}`,v)}update(e,r){return this.request(`/v1/artifacts/${d.parse(e)}`,v,{method:"PATCH",body:JSON.stringify(Z.parse(r))})}delete(e){return this.request(`/v1/artifacts/${d.parse(e)}`,u.object({deleted:u.literal(!0),id:d}),{method:"DELETE"})}whoami(e={}){return this.request("/v1/me",ee,{signal:e.signal})}completeConnection(){return this.request("/v1/auth/complete",u.object({completed:u.literal(!0)}),{method:"POST"})}revokeSelf(){return this.request("/v1/auth/logout",u.object({revoked:u.literal(!0),id:d}),{method:"POST"})}createFolder(e){return this.request("/v1/artifacts",v,{method:"POST",body:JSON.stringify(G.parse({...e.folder,kind:"folder"})),headers:{"idempotency-key":$.parse(e.requestId)}})}putFile(e,r){return this.request(`/v1/artifacts/${d.parse(e)}/files/${T.parse(r.path).split("/").map(encodeURIComponent).join("/")}`,R,{method:"PUT",body:new Blob([new Uint8Array(r.bytes)]),headers:{"content-type":r.contentType??"application/octet-stream"}})}listFiles(e){return this.request(`/v1/artifacts/${d.parse(e)}/files`,K)}deleteFile(e,r){return this.request(`/v1/artifacts/${d.parse(e)}/files/${T.parse(r).split("/").map(encodeURIComponent).join("/")}`,u.object({deleted:u.literal(!0),path:u.string()}),{method:"DELETE"})}createSpace(e){return this.request("/v1/admin/spaces",_,{method:"POST",body:JSON.stringify(Q.parse(e))})}createToken(e){return this.request("/v1/tokens",z,{method:"POST",body:JSON.stringify(y.parse(e))})}revokeToken(e){return this.request(`/v1/tokens/${d.parse(e)}`,u.object({revoked:u.literal(!0),id:d}),{method:"DELETE"})}};import{constants as oe}from"node:fs";import{lstat as xe,mkdir as Se,open as N,rename as be,unlink as ne}from"node:fs/promises";import{homedir as ve}from"node:os";import{join as E,resolve as ke}from"node:path";import{randomUUID as ie}from"node:crypto";import{setTimeout as Te}from"node:timers/promises";import{z as h}from"zod";var wt=h.enum(["keychain","file"]),ae=h.strictObject({endpoint:A,tokenId:d,space:F,scopes:h.array(j),expiresAt:h.iso.datetime(),pendingConnection:h.literal(!0).optional(),credential:h.discriminatedUnion("kind",[h.strictObject({kind:h.literal("keychain"),account:h.string().min(1)}),h.strictObject({kind:h.literal("file"),token:b})])}),Ee=h.strictObject({version:h.literal(1),profiles:h.record(O,ae)});function se(){return ke(process.env.SESHAT_CONFIG_DIR??E(process.env.XDG_CONFIG_HOME??E(ve(),".config"),"seshat"))}function ce(t){return t instanceof Error&&"code"in t&&t.code==="ENOENT"}async function le(t,e=!1){let r=await xe(t);if(r.isSymbolicLink()||(e?!r.isDirectory():!r.isFile()))throw new Error("Seshat credential paths must be real files and directories, not symlinks");if(process.platform!=="win32"&&((r.mode&63)!==0||process.getuid&&r.uid!==process.getuid()))throw new Error(`Credential path must belong to you and have ${e?"0700":"0600"} permissions: ${t}`)}async function pe(){let t=se();try{await le(t,!0);let e=E(t,"profiles.json"),r=await N(e,oe.O_RDONLY|(oe.O_NOFOLLOW??0));try{let n=await r.stat();if(!n.isFile()||n.size>65536||process.platform!=="win32"&&((n.mode&63)!==0||process.getuid&&n.uid!==process.getuid()))throw new Error("Credential file must be a small, owner-only regular file");try{let a=Ee.safeParse(JSON.parse(await r.readFile("utf8")));if(a.success)return a.data}catch{}throw new Error("Invalid Seshat profile file. Run seshat login again")}finally{await r.close()}}catch(e){if(ce(e))return{version:1,profiles:{}};throw e}}async function H(t,e){let r=se();await Se(r,{recursive:!0,mode:448}),await le(r,!0);let n=E(r,"profiles.lock"),a;for(let l=0;l<40;l++)try{a=await N(n,"wx",384);break}catch(p){if(!(p instanceof Error&&"code"in p&&p.code==="EEXIST"))throw p;await Te(50)}if(!a)throw new Error("Another Seshat process is updating profiles. Retry shortly; if it stopped unexpectedly, remove profiles.lock from your Seshat config directory");let s=E(r,`profiles-${ie()}.tmp`);try{e?.throwIfAborted();let l=t(await pe()),p=await N(s,"wx",384);try{let c=JSON.stringify(l)+`
2
+ `;if(Buffer.byteLength(c)>65536)throw new Error("Too many stored profiles; remove unused connections before adding another");await p.writeFile(c),await p.sync()}finally{await p.close()}e?.throwIfAborted(),await be(s,E(r,"profiles.json"))}finally{try{await ne(s).catch(l=>{if(!ce(l))throw l})}finally{await a.close(),await ne(n)}}}async function W(t){let{AsyncEntry:e}=await import("@napi-rs/keyring");return new e("dev.djamil.seshat.cli",t,{linux:{store:"secret-service"}})}async function B(t){t.credential.kind==="keychain"&&await(await W(t.credential.account)).deletePassword(AbortSignal.timeout(15e3))}async function yt(t){let e=O.parse(t.name),r=b.parse(t.token);if(t.storage==="file"&&process.platform==="win32")throw new Error("Use keychain storage with Windows Credential Manager");let n=ie();if(t.storage==="keychain")try{await(await W(n)).setPassword(r,t.signal?AbortSignal.any([t.signal,AbortSignal.timeout(15e3)]):AbortSignal.timeout(15e3))}catch{throw t.signal?.aborted?new Error("Connection cancelled"):new Error("The OS credential vault is unavailable. On a headless POSIX machine, rerun login with --storage file to use an owner-only credential file")}let a=ae.parse({pendingConnection:t.pendingConnection,endpoint:t.endpoint,tokenId:t.tokenId,space:t.space,scopes:t.scopes,expiresAt:t.expiresAt,credential:t.storage==="keychain"?{kind:"keychain",account:n}:{kind:"file",token:r}}),s;try{await H(l=>{if(s=Object.hasOwn(l.profiles,e)?l.profiles[e]:void 0,(s?.tokenId??null)!==t.expectedTokenId)throw new Error("This profile changed during login. Retry; the newer connection has been preserved");return{...l,profiles:{...l.profiles,[e]:a}}},t.signal)}catch(l){throw await B(a).catch(()=>{}),l}return s&&await B(s).catch(()=>{process.stderr.write(`The previous OS credential entry could not be removed. Revoke its token in Seshat Agent access.
3
+ `)}),a}function Ae(t){return O.parse(t??process.env.SESHAT_PROFILE??"default")}async function de(t){let e=await pe();return Object.hasOwn(e.profiles,t)?e.profiles[t]:void 0}async function Pe(t){if(t.credential.kind==="file")return t.credential.token;try{return b.parse(await(await W(t.credential.account)).getPassword(AbortSignal.timeout(15e3)))}catch{throw new Error("Stored credential is unavailable. Run seshat login again for this profile")}}async function xt(t,e){let r=await de(t);if(r){if(r.tokenId!==e)throw new Error("This profile changed during logout. The newer connection has been preserved");await B(r),await H(n=>{let a=Object.hasOwn(n.profiles,t)?n.profiles[t]:void 0;if(a&&a.tokenId!==e)throw new Error("This profile changed during logout. The newer connection has been preserved");let s={...n.profiles};return delete s[t],{...n,profiles:s}})}}async function je(t,e,r){e.pendingConnection&&(await r.completeConnection(),await H(n=>{let a=Object.hasOwn(n.profiles,t)?n.profiles[t]:void 0;if(a?.tokenId!==e.tokenId)throw new Error("This profile changed during connection. The newer connection has been preserved");let{pendingConnection:s,...l}=a;return{...n,profiles:{...n.profiles,[t]:l}}}))}async function St(t={}){let e=t.endpoint??process.env.SESHAT_ENDPOINT;if(process.env.SESHAT_TOKEN&&!t.profile&&!process.env.SESHAT_PROFILE)return new P({endpoint:e??re,token:process.env.SESHAT_TOKEN});let r=Ae(t.profile),n=await de(r);if(!n)throw new Error(`No credential for profile "${r}". Run seshat login --profile ${r}`);if(e&&A.parse(e)!==n.endpoint)throw new Error("This profile belongs to another endpoint. Log in with a different --profile; stored credentials are never sent to a different service");let a=new P({endpoint:n.endpoint,token:await Pe(n)});return await je(r,n,a),a}function bt(t,e){return{profile:t,endpoint:e.endpoint,tokenId:e.tokenId,space:e.space,scopes:e.scopes,expiresAt:e.expiresAt,storage:e.credential.kind}}var Oe={html:"text/html",htm:"text/html",md:"text/markdown",markdown:"text/markdown",json:"application/json",pdf:"application/pdf",txt:"text/plain",png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",avif:"image/avif",mp4:"video/mp4",m4v:"video/mp4",webm:"video/webm",mov:"video/quicktime",ogv:"video/ogg",mp3:"audio/mpeg",m4a:"audio/mp4",aac:"audio/aac",wav:"audio/wav",ogg:"audio/ogg",oga:"audio/ogg",opus:"audio/ogg",flac:"audio/flac"};function fe(t){return Oe[t.split(".").at(-1)?.toLowerCase()??""]}import{realpath as me,stat as Fe,readFile as Ce}from"node:fs/promises";import{basename as Ue,relative as Ie,isAbsolute as $e}from"node:path";async function X(t,e){if(!(await Promise.all(e.map(n=>me(n)))).some(n=>{let a=Ie(n,t);return a!==".."&&!a.startsWith("../")&&!$e(a)}))throw new Error("File is outside SESHAT_UPLOAD_ROOTS. Configure explicit directories for MCP file access")}async function jt(t,e){let r=await me(t);e&&await X(r,e);let n=await Fe(r);if(!n.isFile())throw new Error("Upload path must be a regular file");if(n.size<1||n.size>S)throw new Error(`File must contain 1 to ${S} bytes`);return{bytes:new Uint8Array(await Ce(r)),filename:Ue(r),contentType:fe(r)??"application/octet-stream"}}import{createHash as De}from"node:crypto";import{lstat as he,readdir as Le,readFile as Re,realpath as ze}from"node:fs/promises";import{basename as _e,join as Me}from"node:path";function Lt(t){let e=/(?:^|\/a\/)([a-f0-9]{48})(?:[/?#]|$)/.exec(t.trim())?.[1];if(!e)throw new Error("Provide a folder ID or its share link with --into");return d.parse(e)}async function Rt(t,e={}){let r=(e.prefix??"").replace(/^\/+|\/+$/g,""),n=new Map,a=(c,f,m)=>{let g=T.safeParse(r?`${r}/${c}`:c);if(!g.success)throw new Error(`${f} cannot be stored as ${c}. Use plain relative names without "..", "~" or backslashes`);let x=n.get(g.data);if(x)throw new Error(`${x.source} and ${f} would both become ${g.data}. Upload one of them with a different --prefix`);if(m<1||m>S)throw new Error(`${f} must contain 1 to ${S} bytes`);n.set(g.data,{path:g.data,source:f,size:m})},s=c=>!e.includeHidden&&(c.startsWith(".")||c==="node_modules"),l=async(c,f)=>{for(let m of(await Le(c,{withFileTypes:!0})).sort((g,x)=>g.name.localeCompare(x.name))){if(s(m.name))continue;let g=Me(c,m.name),x=f?`${f}/${m.name}`:m.name;m.isDirectory()?await l(g,x):m.isFile()&&a(x,g,(await he(g)).size)}};for(let c of t){let f=await ze(c);e.allowedRoots&&await X(f,e.allowedRoots);let m=await he(f);if(m.isDirectory())await l(f,"");else if(m.isFile())a(_e(f),f,m.size);else throw new Error(`${c} is not a file or a directory`)}let p=[...n.values()];if(!p.length)throw new Error("Nothing to upload. The paths contain no files");if(p.length>U)throw new Error(`A folder holds up to ${U} files; these paths contain ${p.length}`);if(p.reduce((c,f)=>c+f.size,0)>I)throw new Error(`A folder holds up to ${I/1024/1024} MiB`);return p}async function zt(t,e,r,n=()=>{}){let a=new Map;try{for(let c of(await t.listFiles(e)).files)a.set(c.path,c.digest)}catch(c){if(!(c instanceof k)||c.code!=="insufficient_scope")throw c}let s={uploaded:0,unchanged:0,failed:[]},l=[...r],p=0;return n({completed:p,total:r.length}),await Promise.all(Array.from({length:Math.min(4,r.length)},async()=>{for(let c=l.shift();c;c=l.shift()){try{let f=new Uint8Array(await Re(c.source));a.get(c.path)===De("sha256").update(f).digest("hex")?s.unchanged++:(await t.putFile(e,{path:c.path,bytes:f}),s.uploaded++)}catch(f){s.failed.push({path:c.path,code:f instanceof k?f.code:"read_failed",message:f instanceof Error?f.message:"Upload failed"})}n({completed:++p,total:r.length})}})),s}export{S as a,j as b,d as c,$ as d,w as e,Z as f,T as g,G as h,te as i,re as j,A as k,q as l,Qe as m,Ve as n,k as o,P as p,wt as q,yt as r,Ae as s,de as t,Pe as u,xt as v,je as w,St as x,bt as y,jt as z,Lt as A,Rt as B,zt as C};
package/dist/cli.js CHANGED
@@ -1,30 +1,30 @@
1
1
  #!/usr/bin/env node
2
- import{a as q,b as O,d as X,g as V,h as H,i as L,j as Z,k as Q,l as M,m as E,n as R,o as ee,p as te,q as T,r as N,s as K,t as ne,u as B,v as P,w as U,x as re}from"./chunk-FGC75MZR.js";import{parseArgs as _e}from"node:util";import{pathToFileURL as je}from"node:url";import{realpathSync as Fe}from"node:fs";import{z as F}from"zod";import{execFile as ye}from"node:child_process";import{readFile as se}from"node:fs/promises";import{promisify as ke}from"node:util";import{z as S}from"zod";var oe=ke(ye),ve=S.coerce.number().int().min(1).max(36),xe=S.object({SPHardwareDataType:S.array(S.object({machine_name:S.string().optional()}))}),Se=S.object({model:S.string().optional(),chassis:S.array(S.number()).nullable().optional()});function be(e){let s=e.platform==="darwin"?"macos":e.platform==="win32"?"windows":e.platform==="android"?"android":e.platform==="linux"?"linux":"other",r="terminal";return s==="android"||e.chassis===11?r="phone":e.chassis===30?r="tablet":/^MacBook/i.test(e.model??"")||[8,9,10,14,31,32].includes(e.chassis??0)?r="laptop":(/^(?:iMac|Mac (?:mini|Pro|Studio))/i.test(e.model??"")||[3,4,5,6,7,13,15,16,24].includes(e.chassis??0))&&(r="desktop"),{kind:r,os:s,...e.model?{model:e.model}:{},client:e.client}}async function ae(e){let s,r;try{if(process.platform==="darwin"){let{stdout:i}=await oe("/usr/sbin/system_profiler",["SPHardwareDataType","-detailLevel","mini","-json"],{timeout:2e3,maxBuffer:65536}),l=xe.safeParse(JSON.parse(i));l.success&&(s=l.data.SPHardwareDataType[0]?.machine_name)}else if(process.platform==="linux"){let i=await Promise.all([se("/sys/class/dmi/id/product_name","utf8").catch(()=>""),se("/sys/class/dmi/id/chassis_type","utf8").catch(()=>"")]);s=i[0]?.trim();let l=ve.safeParse(i[1]?.trim());l.success&&(r=l.data)}else if(process.platform==="win32"){let{stdout:i}=await oe("powershell.exe",["-NoProfile","-NonInteractive","-Command","[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new(); @{model=(Get-CimInstance Win32_ComputerSystem).Model;chassis=@((Get-CimInstance Win32_SystemEnclosure).ChassisTypes)} | ConvertTo-Json -Compress"],{timeout:2e3,maxBuffer:65536,windowsHide:!0}),l=Se.safeParse(JSON.parse(i));l.success&&(s=l.data.model,r=l.data.chassis?.[0])}}catch{}let a=Z.shape.model.safeParse(s);return be({platform:process.env.TERMUX_VERSION?"android":process.platform,model:a.success?a.data:void 0,chassis:r,client:e})}import{stripVTControlCharacters as Te}from"node:util";function I(e){return Te(e).replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g," ").trim()}function _(e){return e?new Date(e).toLocaleDateString(void 0,{month:"short",day:"numeric",year:"numeric"}):"Never"}function g(e,s=[]){let r=Math.max(10,...s.map(([a])=>a.length+1));return`
3
- ${I(e)}
4
- ${s.length?`
5
- `+s.map(([a,i])=>` ${a.padEnd(r)}${I(i)}`).join(`
2
+ import{A as pe,B as fe,C as ue,a as W,b as L,d as J,i as oe,j as G,k as Y,l as se,m as ae,n as X,o as R,p as U,q as ie,r as ce,s as $,t as M,u as V,v as de,w as Z,x as _,y as H,z as le}from"./chunk-73X6ZBVW.js";import{parseArgs as We}from"node:util";import{pathToFileURL as Je}from"node:url";import{realpathSync as Ge}from"node:fs";import{z as B}from"zod";import{execFile as Ce}from"node:child_process";import{readFile as me}from"node:fs/promises";import{promisify as Re}from"node:util";import{z as A}from"zod";var he=Re(Ce),$e=A.coerce.number().int().min(1).max(36),Pe=A.object({SPHardwareDataType:A.array(A.object({machine_name:A.string().optional()}))}),De=A.object({model:A.string().optional(),chassis:A.array(A.number()).nullable().optional()});function Ne(e){let o=e.platform==="darwin"?"macos":e.platform==="win32"?"windows":e.platform==="android"?"android":e.platform==="linux"?"linux":"other",n="terminal";return o==="android"||e.chassis===11?n="phone":e.chassis===30?n="tablet":/^MacBook/i.test(e.model??"")||[8,9,10,14,31,32].includes(e.chassis??0)?n="laptop":(/^(?:iMac|Mac (?:mini|Pro|Studio))/i.test(e.model??"")||[3,4,5,6,7,13,15,16,24].includes(e.chassis??0))&&(n="desktop"),{kind:n,os:o,...e.model?{model:e.model}:{},client:e.client}}async function ge(e){let o,n;try{if(process.platform==="darwin"){let{stdout:i}=await he("/usr/sbin/system_profiler",["SPHardwareDataType","-detailLevel","mini","-json"],{timeout:2e3,maxBuffer:65536}),p=Pe.safeParse(JSON.parse(i));p.success&&(o=p.data.SPHardwareDataType[0]?.machine_name)}else if(process.platform==="linux"){let i=await Promise.all([me("/sys/class/dmi/id/product_name","utf8").catch(()=>""),me("/sys/class/dmi/id/chassis_type","utf8").catch(()=>"")]);o=i[0]?.trim();let p=$e.safeParse(i[1]?.trim());p.success&&(n=p.data)}else if(process.platform==="win32"){let{stdout:i}=await he("powershell.exe",["-NoProfile","-NonInteractive","-Command","[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new(); @{model=(Get-CimInstance Win32_ComputerSystem).Model;chassis=@((Get-CimInstance Win32_SystemEnclosure).ChassisTypes)} | ConvertTo-Json -Compress"],{timeout:2e3,maxBuffer:65536,windowsHide:!0}),p=De.safeParse(JSON.parse(i));p.success&&(o=p.data.model,n=p.data.chassis?.[0])}}catch{}let a=se.shape.model.safeParse(o);return Ne({platform:process.env.TERMUX_VERSION?"android":process.platform,model:a.success?a.data:void 0,chassis:n,client:e})}import{stripVTControlCharacters as Oe}from"node:util";function b(e){return Oe(e).replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g," ").trim()}function N(e){return e?new Date(e).toLocaleDateString(void 0,{month:"short",day:"numeric",year:"numeric"}):"Never"}function g(e,o=[]){let n=Math.max(10,...o.map(([a])=>a.length+1));return`
3
+ ${b(e)}
4
+ ${o.length?`
5
+ `+o.map(([a,i])=>` ${a.padEnd(n)}${b(i)}`).join(`
6
6
  `)+`
7
- `:""}`}function D(e){let s={"artifacts:read":"Read artifacts","artifacts:write":"Upload and edit","artifacts:delete":"Delete artifacts","tokens:manage":"Manage tokens"};return e.map(r=>s[r]).join(" \xB7 ")}function z(e,s,r){let a=e.profile==="default"?"":` --profile ${e.profile}`;return g(`${s?"\u2713 Already connected to":"\u2713 Connected to"} ${e.space.name}`,[["Profile",e.profile],["Service",e.endpoint],["Access",D(e.scopes)],["Expires",_(e.expiresAt)],["Stored in",e.storage==="keychain"?"OS credential vault":"Private credential file (0600)"],[r?"Agent uses":"Try next",`seshat upload ./report.html${a}${r?" --json":""}`]])}function $(e,s){return g(`${s} ${e.title}`,[["Link",e.url],["File",e.filename],["Size",`${new Intl.NumberFormat().format(e.size)} bytes`],["Expires",_(e.expiresAt)]])}function ie(e,s){return process.env.NO_COLOR!==void 0||process.env.TERM==="dumb"?e:e.replace(/^([✓✕].*)$/m,r=>`\x1B[1;${s}m${r}\x1B[0m`)}function j(e,s,r){process.stdout.write(r?(process.stdout.isTTY?ie(s,32):s)+`
7
+ `:""}`}function F(e){let o={"artifacts:read":"Read artifacts","artifacts:write":"Upload and edit","artifacts:delete":"Delete artifacts","tokens:manage":"Manage tokens"};return e.map(n=>o[n]).join(" \xB7 ")}function Q(e,o,n){let a=e.profile==="default"?"":` --profile ${e.profile}`;return g(`${o?"\u2713 Already connected to":"\u2713 Connected to"} ${e.space.name}`,[["Profile",e.profile],["Service",e.endpoint],["Access",F(e.scopes)],["Expires",N(e.expiresAt)],["Stored in",e.storage==="keychain"?"OS credential vault":"Private credential file (0600)"],[n?"Agent uses":"Try next",`seshat upload ./report.html${a}${n?" --json":""}`]])}function we(e){return new Date(e).toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}function j(e,o,n={}){return g(`${o} ${e.title}`,[["Link",e.url],["File",e.filename],["Size",`${new Intl.NumberFormat().format(e.size)} bytes`],...n.dates?[["Uploaded",we(e.createdAt)]]:[],...n.dates&&e.updatedAt&&e.updatedAt!==e.createdAt?[["Updated",we(e.updatedAt)]]:[],["Expires",N(e.expiresAt)]])}function ye(e,o){return process.env.NO_COLOR!==void 0||process.env.TERM==="dumb"?e:e.replace(/^([✓✕].*)$/m,n=>`\x1B[1;${o}m${n}\x1B[0m`)}function K(e,o,n){process.stdout.write(n?(process.stdout.isTTY?ye(o,process.exitCode?31:32):o)+`
8
8
  `:JSON.stringify(e)+`
9
- `)}function ce(e,s){if(!s){process.stderr.write(JSON.stringify({error:{code:e.code,message:e.message},...e.requestId?{requestId:e.requestId}:{}})+`
10
- `);return}let r=g(`\u2715 ${e.message}`,[["Code",e.code],...e.hint?[["Next step",e.hint]]:[],...e.requestId?[["Retry ID",e.requestId]]:[]]);process.stderr.write((process.stderr.isTTY?ie(r,31):r)+`
11
- `)}import{randomBytes as Ie,createHash as Ee}from"node:crypto";import{execFile as Ce}from"node:child_process";import{promisify as Ae}from"node:util";import{hostname as Re}from"node:os";import{setTimeout as Pe}from"node:timers/promises";import{z as le}from"zod";var W=Ae(Ce);async function J(e,s,r,a,i){let l=await fetch(e+s,{method:"POST",redirect:"error",headers:{"content-type":"application/json"},body:JSON.stringify(r),signal:i?AbortSignal.any([i,AbortSignal.timeout(15e3)]):AbortSignal.timeout(15e3)}),n=await l.json();if(!l.ok){let h=V.safeParse(n);throw new E(h.success?h.data.error.code:"connection_failed",h.success?h.data.error.message:"Could not connect to Seshat",l.status)}let p=a.safeParse(n);if(!p.success)throw new Error("Unexpected connection response from the service");return p.data}async function De(e){process.platform==="darwin"?await W("open",[e],{timeout:1e4}):process.platform==="win32"?await W("rundll32",["url.dll,FileProtocolHandler",e],{timeout:1e4}):await W("xdg-open",[e],{timeout:1e4})}async function pe(e){let s=T(e.profile),r=L.parse(e.endpoint??process.env.SESHAT_ENDPOINT??H),a=ee.parse(e.storage??"keychain");if(a==="file"&&process.platform==="win32")throw new Error("Use keychain storage with Windows Credential Manager");let i=le.array(O).min(1).parse((e.scopes??"artifacts:read,artifacts:write").split(",")),l=le.coerce.number().int().min(5).max(600).parse(e.timeout??600),n=await N(s),p;if(n){try{p=new R({endpoint:n.endpoint,token:await K(n)})}catch{}if(!e.force&&e.expiresAt===void 0&&e.name===void 0&&(e.storage===void 0||e.storage===n.credential.kind)&&p&&n.endpoint===r&&n.expiresAt>new Date().toISOString()&&[...new Set(i)].sort().join(",")===[...n.scopes].sort().join(","))try{return await B(s,n,p),await p.whoami(),{connected:!0,alreadyConnected:!0,...U(s,n)}}catch(y){if(!(y instanceof E&&y.status===401))throw y}}let h="seshat_"+Ie(32).toString("base64url"),w=Ee("sha256").update(h).digest("hex"),d=await J(r,"/v1/auth/device",{name:e.name??`${Re().slice(0,60)} \xB7 ${s.slice(0,32)}`,scopes:i,tokenHash:w,device:await ae(e.agent?"agent":"terminal"),...e.expiresAt?{expiresAt:e.expiresAt}:{}},Q);if(!URL.canParse(d.verificationUrl))throw new Error("The service returned an invalid authorization URL");let o=new URL(d.verificationUrl);if(o.origin!==r||o.pathname!=="/connect"||o.username||o.password||o.hash||o.searchParams.get("code")!==d.userCode)throw new Error("The service returned an unexpected authorization URL");process.stderr.write(e.human?g("Connect this terminal to Seshat",[["Open",d.verificationUrl],["Code",d.userCode]])+`
9
+ `)}function ke(e,o){if(!o){process.stderr.write(JSON.stringify({error:{code:e.code,message:e.message},...e.requestId?{requestId:e.requestId}:{}})+`
10
+ `);return}let n=g(`\u2715 ${e.message}`,[["Code",e.code],...e.hint?[["Next step",e.hint]]:[],...e.requestId?[["Retry ID",e.requestId]]:[]]);process.stderr.write((process.stderr.isTTY?ye(n,31):n)+`
11
+ `)}import{randomBytes as Ue,createHash as _e}from"node:crypto";import{execFile as Fe}from"node:child_process";import{promisify as je}from"node:util";import{hostname as qe}from"node:os";import{setTimeout as Le}from"node:timers/promises";import{z as xe}from"zod";var ee=je(Fe);async function te(e,o,n,a,i){let p=await fetch(e+o,{method:"POST",redirect:"error",headers:{"content-type":"application/json"},body:JSON.stringify(n),signal:i?AbortSignal.any([i,AbortSignal.timeout(15e3)]):AbortSignal.timeout(15e3)}),t=await p.json();if(!p.ok){let h=oe.safeParse(t);throw new R(h.success?h.data.error.code:"connection_failed",h.success?h.data.error.message:"Could not connect to Seshat",p.status)}let d=a.safeParse(t);if(!d.success)throw new Error("Unexpected connection response from the service");return d.data}async function Me(e){process.platform==="darwin"?await ee("open",[e],{timeout:1e4}):process.platform==="win32"?await ee("rundll32",["url.dll,FileProtocolHandler",e],{timeout:1e4}):await ee("xdg-open",[e],{timeout:1e4})}async function ve(e){let o=$(e.profile),n=Y.parse(e.endpoint??process.env.SESHAT_ENDPOINT??G),a=ie.parse(e.storage??"keychain");if(a==="file"&&process.platform==="win32")throw new Error("Use keychain storage with Windows Credential Manager");let i=xe.array(L).min(1).parse((e.scopes??"artifacts:read,artifacts:write").split(",")),p=xe.coerce.number().int().min(5).max(600).parse(e.timeout??600),t=await M(o),d;if(t){try{d=new U({endpoint:t.endpoint,token:await V(t)})}catch{}if(!e.force&&e.expiresAt===void 0&&e.name===void 0&&(e.storage===void 0||e.storage===t.credential.kind)&&d&&t.endpoint===n&&t.expiresAt>new Date().toISOString()&&[...new Set(i)].sort().join(",")===[...t.scopes].sort().join(","))try{return await Z(o,t,d),await d.whoami(),{connected:!0,alreadyConnected:!0,...H(o,t)}}catch(k){if(!(k instanceof R&&k.status===401))throw k}}let h="seshat_"+Ue(32).toString("base64url"),y=_e("sha256").update(h).digest("hex"),f=await te(n,"/v1/auth/device",{name:e.name??`${qe().slice(0,60)} \xB7 ${o.slice(0,32)}`,scopes:i,tokenHash:y,device:await ge(e.agent?"agent":"terminal"),...e.expiresAt?{expiresAt:e.expiresAt}:{}},ae);if(!URL.canParse(f.verificationUrl))throw new Error("The service returned an invalid authorization URL");let s=new URL(f.verificationUrl);if(s.origin!==n||s.pathname!=="/connect"||s.username||s.password||s.hash||s.searchParams.get("code")!==f.userCode)throw new Error("The service returned an unexpected authorization URL");process.stderr.write(e.human?g("Connect this terminal to Seshat",[["Open",f.verificationUrl],["Code",f.userCode]])+`
12
12
  Check the code in your browser, then approve this device.
13
13
  Waiting for approval\u2026
14
14
 
15
15
  `:`Approve this terminal in Seshat:
16
- ${d.verificationUrl}
17
- Code: ${d.userCode}
18
- `),e.noBrowser||await De(d.verificationUrl).catch(()=>process.stderr.write(`Open the URL above in your browser to continue.
19
- `));let k=new AbortController,b=()=>k.abort();process.once("SIGINT",b);let u=Math.min(Date.parse(d.expiresAt),Date.now()+l*1e3),v=!1;try{let y=d.interval;for(;Date.now()<u&&(await Pe(Math.min(y*1e3,Math.max(0,u-Date.now())),void 0,{signal:k.signal}),!(Date.now()>=u));){let c;try{c=await J(r,"/v1/auth/device/poll",{deviceCode:d.deviceCode},M,k.signal)}catch(m){if(m instanceof E&&m.code==="slow_down"){y+=2;continue}throw m}if(c.status==="denied")throw new Error("Connection was denied. No credential was saved");if(c.status==="pending"){y=c.interval;continue}let f=await new R({endpoint:r,token:h}).whoami({signal:k.signal});if(f.spaceId!==c.space.id||f.tokenId!==c.id||[...new Set(i)].sort().join(",")!==[...f.scopes].sort().join(","))throw new Error("The approved credential does not match the requested space and permissions");k.signal.throwIfAborted();let t=await te({name:s,endpoint:r,tokenId:c.id,token:h,space:c.space,scopes:c.scopes,expiresAt:c.expiresAt,storage:a,pendingConnection:!0,expectedTokenId:n?.tokenId??null,signal:k.signal});v=!0;try{await B(s,t,new R({endpoint:r,token:h}))}catch{throw new Error("Credential saved, but connection completion was interrupted. Retry seshat auth status with this --profile to finish, or login --force if its five-minute approval expired")}return p&&await p.revokeSelf().catch(()=>process.stderr.write(`The previous credential could not be revoked. Revoke it in Seshat Agent access.
20
- `)),{connected:!0,...U(s,t)}}throw new Error("Connection approval timed out. Run seshat login again")}finally{process.removeListener("SIGINT",b),v||await J(r,"/v1/auth/device/cancel",{deviceCode:d.deviceCode},M).catch(()=>{})}}async function de(e){let s=T(e),r=await N(s);if(!r)return{disconnected:!0,alreadyDisconnected:!0,profile:s};try{await new R({endpoint:r.endpoint,token:await K(r)}).revokeSelf()}catch(a){if(!(a instanceof E&&a.status===401))throw new Error("Could not revoke this connection. Retry logout when the service is available; the local credential has been kept")}return await ne(s,r.tokenId),{disconnected:!0,profile:s}}async function ue(e={}){let s=T(e.profile);if(process.env.SESHAT_TOKEN&&!e.profile&&!process.env.SESHAT_PROFILE){let i=await(await P(e)).whoami();return{connected:!0,source:"environment",endpoint:L.parse(e.endpoint??process.env.SESHAT_ENDPOINT??H),...i}}let r=await N(s);if(!r)return{connected:!1,profile:s};let a=await(await P(e)).whoami();return{connected:!0,...U(s,r),...a}}import $e from"string-width";import{setInterval as Oe,clearInterval as Ne}from"node:timers";var Ue=new Intl.Segmenter(void 0,{granularity:"grapheme"}),C=e=>$e(e,{ambiguousIsNarrow:!1});function fe({value:e,columns:s}){if(C(e)<=s)return e;if(s<1)return"";let r=s>=C("\u2026")?"\u2026":".",a=s-C(r),i="";for(let{segment:l}of Ue.segment(e)){let n=C(l);if(n>a)break;i+=l,a-=n}return i+r}var me=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];function he(e=process.stderr){let s=!!e.isTTY&&process.env.TERM!=="dumb",r={kind:"connecting"},a=0,i=!1,l="",n="artifact";function p(){if(i)return;let w=r.kind==="sending"?r.percent:r.kind==="processing"?100:null,d=`${r.kind}:${w===null?"":Math.floor(w/10)}`;if(!s&&l===d)return;l=d;let o=Math.max(0,(e.columns??100)-1),k=s?me[a++%me.length]??"\u280B":"\u2026",b=o-C(k+" "),u;switch(r.kind){case"connecting":u="Connecting to Seshat\u2026";break;case"reading":u=`Reading ${r.filename}\u2026`;break;case"processing":u=o<35?"Preparing link\u2026":"Preparing your link\u2026 100% sent";break;case"sending":{let y=`Uploading\u2026 ${r.percent}%`,c=Math.floor(r.percent/10),f=` ${"\u2588".repeat(c)}${"\u2591".repeat(10-c)} ${r.percent}%`,t=b-C("Uploading "+f);b<C(y)?u=`${r.percent}%`:t<4?u=y:u=`Uploading ${fe({value:r.filename,columns:t})}${f}`;break}}let v=`${k} ${u}`;e.write(s?`\r\x1B[2K${fe({value:v,columns:o})}`:v+`
21
- `)}p();let h=s?Oe(p,90):void 0;return h?.unref(),{reading(w){n=I(w),r={kind:"reading",filename:n},p()},transferred({loadedBytes:w,totalBytes:d}){let o=Math.min(100,Math.floor(100*w/d));r=o===100?{kind:"processing"}:{kind:"sending",filename:n,percent:o},p()},stop(){i||(i=!0,h&&Ne(h),s&&e.write("\r\x1B[2K"))}}}var G={root:`Seshat: publish and manage agent artifacts.
16
+ ${f.verificationUrl}
17
+ Code: ${f.userCode}
18
+ `),e.noBrowser||await Me(f.verificationUrl).catch(()=>process.stderr.write(`Open the URL above in your browser to continue.
19
+ `));let S=new AbortController,C=()=>S.abort();process.once("SIGINT",C);let l=Math.min(Date.parse(f.expiresAt),Date.now()+p*1e3),E=!1;try{let k=f.interval;for(;Date.now()<l&&(await Le(Math.min(k*1e3,Math.max(0,l-Date.now())),void 0,{signal:S.signal}),!(Date.now()>=l));){let c;try{c=await te(n,"/v1/auth/device/poll",{deviceCode:f.deviceCode},X,S.signal)}catch(u){if(u instanceof R&&u.code==="slow_down"){k+=2;continue}throw u}if(c.status==="denied")throw new Error("Connection was denied. No credential was saved");if(c.status==="pending"){k=c.interval;continue}let m=await new U({endpoint:n,token:h}).whoami({signal:S.signal});if(m.spaceId!==c.space.id||m.tokenId!==c.id||[...new Set(i)].sort().join(",")!==[...m.scopes].sort().join(","))throw new Error("The approved credential does not match the requested space and permissions");S.signal.throwIfAborted();let r=await ce({name:o,endpoint:n,tokenId:c.id,token:h,space:c.space,scopes:c.scopes,expiresAt:c.expiresAt,storage:a,pendingConnection:!0,expectedTokenId:t?.tokenId??null,signal:S.signal});E=!0;try{await Z(o,r,new U({endpoint:n,token:h}))}catch{throw new Error("Credential saved, but connection completion was interrupted. Retry seshat auth status with this --profile to finish, or login --force if its five-minute approval expired")}return d&&await d.revokeSelf().catch(()=>process.stderr.write(`The previous credential could not be revoked. Revoke it in Seshat Agent access.
20
+ `)),{connected:!0,...H(o,r)}}throw new Error("Connection approval timed out. Run seshat login again")}finally{process.removeListener("SIGINT",C),E||await te(n,"/v1/auth/device/cancel",{deviceCode:f.deviceCode},X).catch(()=>{})}}async function be(e){let o=$(e),n=await M(o);if(!n)return{disconnected:!0,alreadyDisconnected:!0,profile:o};try{await new U({endpoint:n.endpoint,token:await V(n)}).revokeSelf()}catch(a){if(!(a instanceof R&&a.status===401))throw new Error("Could not revoke this connection. Retry logout when the service is available; the local credential has been kept")}return await de(o,n.tokenId),{disconnected:!0,profile:o}}async function Se(e={}){let o=$(e.profile);if(process.env.SESHAT_TOKEN&&!e.profile&&!process.env.SESHAT_PROFILE){let i=await(await _(e)).whoami();return{connected:!0,source:"environment",endpoint:Y.parse(e.endpoint??process.env.SESHAT_ENDPOINT??G),...i}}let n=await M(o);if(!n)return{connected:!1,profile:o};let a=await(await _(e)).whoami();return{connected:!0,...H(o,n),...a}}import He from"string-width";import{setInterval as Ke,clearInterval as Be}from"node:timers";var ze=new Intl.Segmenter(void 0,{granularity:"grapheme"}),P=e=>He(e,{ambiguousIsNarrow:!1});function Te({value:e,columns:o}){if(P(e)<=o)return e;if(o<1)return"";let n=o>=P("\u2026")?"\u2026":".",a=o-P(n),i="";for(let{segment:p}of ze.segment(e)){let t=P(p);if(t>a)break;i+=p,a-=t}return i+n}var Ee=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];function Ie(e=process.stderr){let o=!!e.isTTY&&process.env.TERM!=="dumb",n={kind:"connecting"},a=0,i=!1,p="",t="artifact";function d(){if(i)return;let y=n.kind==="sending"?n.percent:n.kind==="processing"?100:n.kind==="batch"?Math.floor(100*n.completed/n.total):null,f=`${n.kind}:${y===null?"":Math.floor(y/10)}`;if(!o&&p===f)return;p=f;let s=Math.max(0,(e.columns??100)-1),S=o?Ee[a++%Ee.length]??"\u280B":"\u2026",C=s-P(S+" "),l;switch(n.kind){case"connecting":l="Connecting to Seshat\u2026";break;case"reading":l=`Reading ${n.filename}\u2026`;break;case"batch":l=`Uploading files\u2026 ${n.completed} of ${n.total}`;break;case"processing":l=s<35?"Preparing link\u2026":"Preparing your link\u2026 100% sent";break;case"sending":{let k=`Uploading\u2026 ${n.percent}%`,c=Math.floor(n.percent/10),m=` ${"\u2588".repeat(c)}${"\u2591".repeat(10-c)} ${n.percent}%`,r=C-P("Uploading "+m);C<P(k)?l=`${n.percent}%`:r<4?l=k:l=`Uploading ${Te({value:n.filename,columns:r})}${m}`;break}}let E=`${S} ${l}`;e.write(o?`\r\x1B[2K${Te({value:E,columns:s})}`:E+`
21
+ `)}d();let h=o?Ke(d,90):void 0;return h?.unref(),{reading(y){t=b(y),n={kind:"reading",filename:t},d()},transferred({loadedBytes:y,totalBytes:f}){let s=Math.min(100,Math.floor(100*y/f));n=s===100?{kind:"processing"}:{kind:"sending",filename:t,percent:s},d()},batch(y){n={kind:"batch",...y},d()},stop(){i||(i=!0,h&&Be(h),o&&e.write("\r\x1B[2K"))}}}var ne={root:`Seshat: publish and manage agent artifacts.
22
22
 
23
23
  Commands default to human-readable output. Use --json for structured results and errors.
24
24
 
25
25
  Commands:
26
26
  login Connect this terminal through your browser
27
- upload FILE Publish an artifact and return its share link
27
+ upload FILE... Publish artifacts, or gather files into one folder
28
28
  list List your artifacts
29
29
  whoami Show the connected space and permissions
30
30
  logout Revoke this connection and remove its credential
@@ -46,12 +46,21 @@ Examples:
46
46
  seshat upload report.html --title "Weekly report"
47
47
  seshat agent setup --profile t3 --name "T3 agent"
48
48
  seshat list --limit 20
49
- seshat whoami`,"artifacts upload":`Upload one file, or stdin with path -. Links are public and unlisted. Files do not expire by default. Blank metadata is generated automatically.
49
+ seshat whoami`,"artifacts upload":`Upload one file, several files, or stdin with path -. Links are public and unlisted. Files do not expire by default. Blank metadata is generated automatically.
50
50
  Options: --title (optional), --description, --content-type, --filename (stdin), --request-id, --expires-at, --no-expiry
51
+ Several files: each becomes its own artifact. --title, --description, --content-type and --filename apply to one file only.
52
+ Folders: --folder NAME gathers every path into one new folder with a single link; --into FOLDER adds to an existing folder (ID or share link).
53
+ A file is stored under its own name. A directory contributes its contents, keeping their layout. --prefix PATH places them in a subfolder.
54
+ Hidden files and node_modules are skipped unless --include-hidden is set. A folder holds up to 100 files and 100 MiB.
55
+ Files that are already stored unchanged are not sent again, so an interrupted upload can be repeated.
51
56
  Human output shows upload progress on stderr; --json suppresses it.
52
57
  Keep --request-id stable across retries. Reusing it with different content returns a conflict.
53
58
  Examples:
54
59
  seshat artifacts upload report.html --title "Weekly report" --request-id report-2026-11-08
60
+ seshat upload report.html notes.md chart.png
61
+ seshat upload ./dist --folder "Launch site"
62
+ seshat upload ~/Desktop/logo.png ./exports/deck.pdf --folder "Launch kit"
63
+ seshat upload ./photos --into FOLDER_ID --prefix images
55
64
  cat notes.md | seshat artifacts upload - --filename notes.md --content-type text/markdown --title Notes`,"artifacts list":`List uploads in your token's space. Options: --limit (1-100), --cursor.
56
65
  Examples:
57
66
  seshat artifacts list --limit 20
@@ -83,14 +92,23 @@ Examples:
83
92
  seshat agent setup --profile t3 --name "T3 agent"
84
93
  seshat agent setup --profile remote --storage file --no-browser`,"auth whoami":`Inspect the active token's space and scopes.
85
94
  Example: seshat auth whoami`,mcp:`Start the local stdio MCP server. SESHAT_UPLOAD_ROOTS is an optional JSON array of allowed directories.
86
- Example: seshat mcp`},qe={help:{type:"boolean"},json:{type:"boolean"},title:{type:"string"},description:{type:"string"},"content-type":{type:"string"},filename:{type:"string"},"request-id":{type:"string"},"expires-at":{type:"string"},"no-expiry":{type:"boolean"},limit:{type:"string"},cursor:{type:"string"},yes:{type:"boolean"},"dry-run":{type:"boolean"},name:{type:"string"},scopes:{type:"string"},slug:{type:"string"},profile:{type:"string"},endpoint:{type:"string"},storage:{type:"string"},"no-browser":{type:"boolean"},timeout:{type:"string"},force:{type:"boolean"}},He={"artifacts upload":["title","description","content-type","filename","request-id","expires-at","no-expiry"],"artifacts list":["limit","cursor"],"artifacts get":[],"artifacts update":["title","description","expires-at","no-expiry"],"artifacts delete":["yes","dry-run"],"tokens create":["name","scopes","expires-at"],"tokens revoke":["yes","dry-run"],"spaces create":["slug","name"],"auth whoami":[],"auth login":["name","scopes","expires-at","storage","no-browser","timeout","force"],"agent setup":["name","scopes","expires-at","storage","no-browser","timeout","force"],"auth logout":[],"auth status":[],mcp:[]};function x(e,s){let r=F.string().min(1).safeParse(e);if(!r.success)throw new Error(`Provide ${s}. Run this command with --help for an example`);return r.data}async function Le(){let e=[],s=0;for await(let r of process.stdin){let a=Buffer.from(r);if(s+=a.length,s>q)throw new Error(`stdin exceeds ${q} bytes`);e.push(a)}return new Uint8Array(Buffer.concat(e))}async function Me(e){let s,r,a=!e.includes("--json"),i="default",l="seshat --help";try{let{values:n,positionals:p}=_e({args:e,options:qe,allowPositionals:!0}),h={login:"auth login",logout:"auth logout",whoami:"auth whoami",upload:"artifacts upload",list:"artifacts list"},w=p[0]??"",d=Object.hasOwn(h,w)?h[w]:void 0,o=d??(w==="mcp"?"mcp":p.slice(0,2).join(" "));if(n.help||!o){process.stdout.write((G[o]??G.root)+(o?`
95
+ Example: seshat mcp`},Ye={help:{type:"boolean"},json:{type:"boolean"},title:{type:"string"},description:{type:"string"},"content-type":{type:"string"},filename:{type:"string"},"request-id":{type:"string"},"expires-at":{type:"string"},"no-expiry":{type:"boolean"},limit:{type:"string"},cursor:{type:"string"},yes:{type:"boolean"},"dry-run":{type:"boolean"},name:{type:"string"},scopes:{type:"string"},slug:{type:"string"},profile:{type:"string"},endpoint:{type:"string"},storage:{type:"string"},"no-browser":{type:"boolean"},timeout:{type:"string"},force:{type:"boolean"},folder:{type:"string"},into:{type:"string"},prefix:{type:"string"},"include-hidden":{type:"boolean"}},Xe={"artifacts upload":["title","description","content-type","filename","request-id","expires-at","no-expiry","folder","into","prefix","include-hidden"],"artifacts list":["limit","cursor"],"artifacts get":[],"artifacts update":["title","description","expires-at","no-expiry"],"artifacts delete":["yes","dry-run"],"tokens create":["name","scopes","expires-at"],"tokens revoke":["yes","dry-run"],"spaces create":["slug","name"],"auth whoami":[],"auth login":["name","scopes","expires-at","storage","no-browser","timeout","force"],"agent setup":["name","scopes","expires-at","storage","no-browser","timeout","force"],"auth logout":[],"auth status":[],mcp:[]};function T(e,o){let n=B.string().min(1).safeParse(e);if(!n.success)throw new Error(`Provide ${o}. Run this command with --help for an example`);return n.data}async function Ae(e){try{return await le(e)}catch(o){throw o instanceof Error&&/regular file/.test(o.message)?new Error(`${e} is a directory. Add --folder NAME to upload it as one folder`):o}}async function Ve(){let e=[],o=0;for await(let n of process.stdin){let a=Buffer.from(n);if(o+=a.length,o>W)throw new Error(`stdin exceeds ${W} bytes`);e.push(a)}return new Uint8Array(Buffer.concat(e))}async function Ze(e){let o,n,a=!e.includes("--json"),i="default",p="seshat --help";try{let{values:t,positionals:d}=We({args:e,options:Ye,allowPositionals:!0}),h={login:"auth login",logout:"auth logout",whoami:"auth whoami",upload:"artifacts upload",list:"artifacts list"},y=d[0]??"",f=Object.hasOwn(h,y)?h[y]:void 0,s=f??(y==="mcp"?"mcp":d.slice(0,2).join(" "));if(t.help||!s){process.stdout.write((ne[s]??ne.root)+(s?`
87
96
 
88
97
  Output: human-readable by default. Use --json for structured results and errors.`:"")+`
89
- `);return}if(!G[o])throw new Error("Unknown command. Run seshat --help");l=`seshat ${o} --help`,i=T(n.profile??(o==="agent setup"?"agent":void 0));let k=["artifacts upload","artifacts get","artifacts update","artifacts delete","tokens revoke"].includes(o);if(p.length>(o==="mcp"?1:(d?1:2)+(k?1:0)))throw new Error("Unexpected positional argument. Run this command with --help");let b=Object.keys(n).find(t=>!["help","json","profile","endpoint",...He[o]??[]].includes(t));if(b)throw new Error(`--${b} is not supported by ${o}. Run this command with --help`);if(o==="mcp"){let{startMcp:t}=await import("./mcp.js");await t({profile:n.profile,endpoint:n.endpoint});return}if(n["no-expiry"]&&n["expires-at"])throw new Error("Choose --no-expiry or --expires-at");if(o==="auth login"||o==="agent setup"){let t=o==="agent setup"?T(n.profile??"agent"):T(n.profile),m=await pe({profile:t,endpoint:n.endpoint,name:n.name,scopes:n.scopes??(o==="agent setup"?"artifacts:write":void 0),expiresAt:n["expires-at"],storage:n.storage,noBrowser:n["no-browser"],timeout:n.timeout,force:n.force,human:a,agent:o==="agent setup"}),A=o==="agent setup"?{...m,agent:{environment:{SESHAT_PROFILE:t},uploadCommand:`pnpx @seshat.sh/cli upload FILE --profile ${t} --json`,helpCommand:"pnpx @seshat.sh/cli upload --help"}}:m;j(A,z(m,"alreadyConnected"in m&&!!m.alreadyConnected,o==="agent setup"),a);return}if(o==="auth logout"||o==="auth status"){let t=o==="auth logout"?await de(n.profile):await ue({profile:n.profile,endpoint:n.endpoint}),m="disconnected"in t?g(t.alreadyDisconnected?"\u2713 No saved connection":"\u2713 Device disconnected",[["Profile",t.profile]]):t.connected?"storage"in t?z(t,!0,!1):g("\u2713 Connected with SESHAT_TOKEN",[["Service",t.endpoint],["Space",t.space?.name??t.spaceId],["Access",D(t.scopes)]]):g("No connected device",[["Profile",t.profile],["Next step",`seshat login --profile ${t.profile}`]]);j(t,m,a);return}a&&o==="artifacts upload"&&(r=he());let u=await P({profile:n.profile,endpoint:n.endpoint}),v=p[d?1:2],y=n["no-expiry"]?null:n["expires-at"],c,f="";switch(o){case"auth whoami":{let t=await u.whoami();c={spaceId:t.spaceId,tokenId:t.tokenId,scopes:t.scopes},f=g("\u2713 Connected to Seshat",[["Space",t.space?.name??t.spaceId],["Access",D(t.scopes)]]);break}case"spaces create":{let t=await u.createSpace({slug:x(n.slug,"--slug"),name:x(n.name,"--name")});c=t,f=g(`\u2713 Created ${t.space.name}`,[["Space ID",t.space.id],["Token",t.credential.token],["Keep safe","Copy this token now. It will not be shown again."]]);break}case"tokens create":{let t=await u.createToken({name:x(n.name,"--name"),scopes:F.array(O).parse((n.scopes??"artifacts:write").split(",")),expiresAt:n["expires-at"]});c=t,f=g("\u2713 Token created",[["Token",t.token],["Access",D(t.scopes)],["Expires",_(t.expiresAt)],["Keep safe","Copy this token now. It will not be shown again."]]);break}case"tokens revoke":{let t=x(v,"TOKEN_ID");if(n["dry-run"]){c={dryRun:!0,action:"revoke_token",id:t},f=g("Preview: revoke token",[["Token ID",t],["To apply",`seshat tokens revoke ${t} --yes`]]);break}if(!n.yes)throw new Error("Revocation requires --yes. Inspect with seshat tokens revoke TOKEN_ID --dry-run");c=await u.revokeToken(t),f=g("\u2713 Token revoked",[["Token ID",t]]);break}case"artifacts list":{let t=await u.list({limit:n.limit===void 0?void 0:F.coerce.number().int().min(1).max(100).parse(n.limit),cursor:n.cursor});c=t,f=g(t.artifacts.length?`${t.artifacts.length} artifacts`:"No artifacts yet")+t.artifacts.map(m=>`
90
- ${I(m.title)}
91
- ${I(m.url)}
92
- `).join("")+(t.nextCursor?`
93
- Next page: seshat list --cursor ${I(t.nextCursor)}
94
- `:"");break}case"artifacts get":{let t=await u.get(x(v,"ARTIFACT_ID"));c=t,f=$(t,"Artifact:");break}case"artifacts update":{let t=await u.update(x(v,"ARTIFACT_ID"),{title:n.title,description:n.description,expiresAt:y});c=t,f=$(t,"\u2713 Updated");break}case"artifacts delete":{let t=x(v,"ARTIFACT_ID");if(n["dry-run"]){let m=await u.get(t);c={dryRun:!0,action:"delete_artifact",artifact:m},f=$(m,"Preview: delete")+`
95
- To apply: seshat artifacts delete ${t} --yes
96
- `;break}if(!n.yes)throw new Error("Deletion requires --yes. Inspect with seshat artifacts delete ARTIFACT_ID --dry-run");c=await u.delete(t),f=g("\u2713 Artifact deleted",[["ID",t]]);break}case"artifacts upload":{let t=x(v,"FILE or - for stdin"),m=n.title;r?.reading(t==="-"?"stdin":t),s=X.parse(n["request-id"]??crypto.randomUUID());let A;if(t==="-"){let ge=x(n.filename,"--filename for stdin"),we=x(n["content-type"],"--content-type for stdin");A={bytes:await Le(),filename:ge,contentType:we}}else A=await re(t);let Y=await u.upload({...A,contentType:n["content-type"]??A.contentType,metadata:{title:m,description:n.description??"",expiresAt:y},requestId:s,onProgress:r?.transferred});c={...Y,requestId:s},f=$(Y,"\u2713 Uploaded");break}}r?.stop(),j(c,f,a)}catch(n){r?.stop();let p=n instanceof E?n.code:"invalid_command",h=n instanceof F.ZodError?`Invalid options: ${n.issues.map(d=>d.message).join("; ")}. Run ${l}`:n instanceof Error?n.message:"Command failed",w=p==="unauthorized"?`Reconnect with seshat login --profile ${i}, or check SESHAT_TOKEN if you use it.`:p==="insufficient_scope"?`Reconnect with the required --scopes using seshat login --profile ${i}.`:p==="connection_failed"?"Check your connection and service endpoint. Retry uploads with the same --request-id.":void 0;ce({code:p,message:h,requestId:s,hint:w},a),process.exitCode=1}}process.argv[1]&&import.meta.url===je(Fe(process.argv[1])).href&&await Me(process.argv.slice(2));export{Me as runCli};
98
+ `);return}if(!ne[s])throw new Error("Unknown command. Run seshat --help");p=`seshat ${s} --help`,i=$(t.profile??(s==="agent setup"?"agent":void 0));let S=["artifacts upload","artifacts get","artifacts update","artifacts delete","tokens revoke"].includes(s);if(s!=="artifacts upload"&&d.length>(s==="mcp"?1:(f?1:2)+(S?1:0)))throw new Error("Unexpected positional argument. Run this command with --help");let C=Object.keys(t).find(r=>!["help","json","profile","endpoint",...Xe[s]??[]].includes(r));if(C)throw new Error(`--${C} is not supported by ${s}. Run this command with --help`);if(s==="mcp"){let{startMcp:r}=await import("./mcp.js");await r({profile:t.profile,endpoint:t.endpoint});return}if(t["no-expiry"]&&t["expires-at"])throw new Error("Choose --no-expiry or --expires-at");if(s==="auth login"||s==="agent setup"){let r=s==="agent setup"?$(t.profile??"agent"):$(t.profile),u=await ve({profile:r,endpoint:t.endpoint,name:t.name,scopes:t.scopes??(s==="agent setup"?"artifacts:write":void 0),expiresAt:t["expires-at"],storage:t.storage,noBrowser:t["no-browser"],timeout:t.timeout,force:t.force,human:a,agent:s==="agent setup"}),z=s==="agent setup"?{...u,agent:{environment:{SESHAT_PROFILE:r},uploadCommand:`pnpx @seshat.sh/cli upload FILE --profile ${r} --json`,helpCommand:"pnpx @seshat.sh/cli upload --help"}}:u;K(z,Q(u,"alreadyConnected"in u&&!!u.alreadyConnected,s==="agent setup"),a);return}if(s==="auth logout"||s==="auth status"){let r=s==="auth logout"?await be(t.profile):await Se({profile:t.profile,endpoint:t.endpoint}),u="disconnected"in r?g(r.alreadyDisconnected?"\u2713 No saved connection":"\u2713 Device disconnected",[["Profile",r.profile]]):r.connected?"storage"in r?Q(r,!0,!1):g("\u2713 Connected with SESHAT_TOKEN",[["Service",r.endpoint],["Space",r.space?.name??r.spaceId],["Access",F(r.scopes)]]):g("No connected device",[["Profile",r.profile],["Next step",`seshat login --profile ${r.profile}`]]);K(r,u,a);return}a&&s==="artifacts upload"&&(n=Ie());let l=await _({profile:t.profile,endpoint:t.endpoint}),E=d[f?1:2],k=t["no-expiry"]?null:t["expires-at"],c,m="";switch(s){case"auth whoami":{let r=await l.whoami();c={spaceId:r.spaceId,tokenId:r.tokenId,scopes:r.scopes},m=g("\u2713 Connected to Seshat",[["Space",r.space?.name??r.spaceId],["Access",F(r.scopes)]]);break}case"spaces create":{let r=await l.createSpace({slug:T(t.slug,"--slug"),name:T(t.name,"--name")});c=r,m=g(`\u2713 Created ${r.space.name}`,[["Space ID",r.space.id],["Token",r.credential.token],["Keep safe","Copy this token now. It will not be shown again."]]);break}case"tokens create":{let r=await l.createToken({name:T(t.name,"--name"),scopes:B.array(L).parse((t.scopes??"artifacts:write").split(",")),expiresAt:t["expires-at"]});c=r,m=g("\u2713 Token created",[["Token",r.token],["Access",F(r.scopes)],["Expires",N(r.expiresAt)],["Keep safe","Copy this token now. It will not be shown again."]]);break}case"tokens revoke":{let r=T(E,"TOKEN_ID");if(t["dry-run"]){c={dryRun:!0,action:"revoke_token",id:r},m=g("Preview: revoke token",[["Token ID",r],["To apply",`seshat tokens revoke ${r} --yes`]]);break}if(!t.yes)throw new Error("Revocation requires --yes. Inspect with seshat tokens revoke TOKEN_ID --dry-run");c=await l.revokeToken(r),m=g("\u2713 Token revoked",[["Token ID",r]]);break}case"artifacts list":{let r=await l.list({limit:t.limit===void 0?void 0:B.coerce.number().int().min(1).max(100).parse(t.limit),cursor:t.cursor});c=r,m=g(r.artifacts.length?`${r.artifacts.length} artifacts`:"No artifacts yet")+r.artifacts.map(u=>`
99
+ ${b(u.title)}
100
+ ${b(u.url)}
101
+ Uploaded ${N(u.createdAt)}
102
+ `).join("")+(r.nextCursor?`
103
+ Next page: seshat list --cursor ${b(r.nextCursor)}
104
+ `:"");break}case"artifacts get":{let r=await l.get(T(E,"ARTIFACT_ID"));c=r,m=j(r,"Artifact:",{dates:!0});break}case"artifacts update":{let r=await l.update(T(E,"ARTIFACT_ID"),{title:t.title,description:t.description,expiresAt:k});c=r,m=j(r,"\u2713 Updated");break}case"artifacts delete":{let r=T(E,"ARTIFACT_ID");if(t["dry-run"]){let u=await l.get(r);c={dryRun:!0,action:"delete_artifact",artifact:u},m=j(u,"Preview: delete")+`
105
+ To apply: seshat artifacts delete ${r} --yes
106
+ `;break}if(!t.yes)throw new Error("Deletion requires --yes. Inspect with seshat artifacts delete ARTIFACT_ID --dry-run");c=await l.delete(r),m=g("\u2713 Artifact deleted",[["ID",r]]);break}case"artifacts upload":{let r=d.slice(f?1:2),u=T(r[0],"FILE or - for stdin");if(o=J.parse(t["request-id"]??crypto.randomUUID()),t.folder!==void 0||t.into!==void 0){if(t.folder!==void 0&&t.into!==void 0)throw new Error("Choose --folder NAME for a new folder or --into FOLDER for an existing one");if(t.into!==void 0&&(t.title!==void 0||t.description!==void 0||k!==void 0))throw new Error("--title, --description and expiry belong to a new folder. Change an existing one with seshat artifacts update");if(t["content-type"]!==void 0||t.filename!==void 0||r.includes("-"))throw new Error("Folders take files and directories. --content-type, --filename and stdin apply to a single upload");n?.reading(r.length===1?u:`${r.length} paths`);let I=await fe(r,{prefix:t.prefix,includeHidden:t["include-hidden"]}),w=t.folder===void 0?void 0:await l.createFolder({folder:{name:T(t.folder,"a name after --folder"),title:t.title,description:t.description??"",expiresAt:k},requestId:o}),v=w?.id??pe(t.into??""),x=await ue(l,v,I,n?.batch);c={...w??{id:v},requestId:o,files:{total:I.length,uploaded:x.uploaded,unchanged:x.unchanged},failed:x.failed},m=g(`${x.failed.length?"\u2715":"\u2713"} ${w?`Uploaded folder ${w.title}`:"Added to folder"}`,[...w?[["Link",w.url]]:[],["Folder ID",v],["Files",`${x.uploaded} uploaded${x.unchanged?`, ${x.unchanged} unchanged`:""}${x.failed.length?`, ${x.failed.length} failed`:""}`],...x.failed.slice(0,10).map(O=>[O.path,O.message]),...x.failed.length?[["Next step",`Repeat with --into ${v}; stored files are skipped`]]:[],...w?[["Expires",N(w.expiresAt)]]:[]]),x.failed.length&&(process.exitCode=1);break}if(t.prefix!==void 0||t["include-hidden"])throw new Error("--prefix and --include-hidden need --folder NAME or --into FOLDER");if(r.length>1){if(t.title!==void 0||t.description!==void 0||t["content-type"]!==void 0||t.filename!==void 0||r.includes("-"))throw new Error("--title, --description, --content-type, --filename and stdin apply to a single upload. To keep these files together, add --folder NAME");let I=[],w=[];for(let[v,x]of r.entries()){let O=J.parse(`${o}-${v+1}`);try{n?.reading(x);let D=await Ae(x);I.push({...await l.upload({...D,metadata:{description:"",expiresAt:k},requestId:O,onProgress:n?.transferred}),requestId:O})}catch(D){w.push({path:x,code:D instanceof R?D.code:"invalid_command",message:D instanceof Error?D.message:"Upload failed"})}}c={artifacts:I,failed:w,requestId:o},m=g(`${w.length?"\u2715":"\u2713"} Uploaded ${I.length} of ${r.length} files`)+I.map(v=>`
107
+ ${b(v.title)}
108
+ ${b(v.url)}
109
+ `).join("")+w.map(v=>`
110
+ ${b(v.path)}
111
+ ${b(v.message)}
112
+ `).join("")+(w.length?`
113
+ Retry with --request-id ${o}; files already uploaded are not duplicated.
114
+ `:""),w.length&&(process.exitCode=1);break}let z=t.title;n?.reading(u==="-"?"stdin":u);let q;if(u==="-"){let I=T(t.filename,"--filename for stdin"),w=T(t["content-type"],"--content-type for stdin");q={bytes:await Ve(),filename:I,contentType:w}}else q=await Ae(u);let re=await l.upload({...q,contentType:t["content-type"]??q.contentType,metadata:{title:z,description:t.description??"",expiresAt:k},requestId:o,onProgress:n?.transferred});c={...re,requestId:o},m=j(re,"\u2713 Uploaded");break}}n?.stop(),K(c,m,a)}catch(t){n?.stop();let d=t instanceof R?t.code:"invalid_command",h=t instanceof B.ZodError?`Invalid options: ${t.issues.map(f=>f.message).join("; ")}. Run ${p}`:t instanceof Error?t.message:"Command failed",y=d==="unauthorized"?`Reconnect with seshat login --profile ${i}, or check SESHAT_TOKEN if you use it.`:d==="insufficient_scope"?`Reconnect with the required --scopes using seshat login --profile ${i}.`:d==="connection_failed"?"Check your connection and service endpoint. Retry uploads with the same --request-id.":void 0;ke({code:d,message:h,requestId:o,hint:y},a),process.exitCode=1}}process.argv[1]&&import.meta.url===Je(Ge(process.argv[1])).href&&await Ze(process.argv.slice(2));export{Ze as runCli};
package/dist/mcp.js CHANGED
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import{c as s,d as l,e as o,f as c,v as u,x as p}from"./chunk-FGC75MZR.js";import{pathToFileURL as f}from"node:url";import{realpathSync as S}from"node:fs";import{McpServer as g}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as y}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as a}from"zod";var m=`Seshat publishes artifacts to public, unlisted links. Upload only content the user wants shared.
2
+ import{B as v,C as O,c as n,d as g,e as p,f as H,g as y,h as f,x as T,z as S}from"./chunk-73X6ZBVW.js";import{pathToFileURL as b}from"node:url";import{realpathSync as A}from"node:fs";import{McpServer as R}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as I}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as a}from"zod";var q=`Seshat publishes artifacts to public, unlisted links. Upload only content the user wants shared.
3
3
  Run seshat login once to use a stored profile. SESHAT_PROFILE selects a named agent profile. For CI, set SESHAT_ENDPOINT and a tenant-scoped SESHAT_TOKEN, never in tool arguments.
4
4
  Supported readers: interactive HTML, Markdown, images, PDF, JSON, and plain text. Other formats download.
5
5
  Artifacts do not expire by default. Set expiresAt to an ISO date to expire one, or null for no expiration. Omit title or description to generate metadata automatically on Cloudflare; explicit values are preserved. Set metadata.password to a password of 12\u2013128 characters to protect an upload. Update password to null to remove protection; changing it invalidates previous unlocks.
6
6
  Use a stable requestId for the same logical upload. Retrying identical bytes and metadata returns the original artifact.
7
7
  A different payload with the same requestId is rejected. Uploading a changed version requires a new requestId.
8
8
  Use upload_file for local artifacts. File access requires SESHAT_UPLOAD_ROOTS, a JSON array of allowed directories.
9
- Use upload_text for inline content. Generate self-contained HTML with embedded assets; directory bundles are not supported yet.
9
+ Use upload_text for inline content. A single HTML artifact must be self-contained.
10
+ Use upload_files to publish several local files as separate artifacts in one call.
11
+ Use a folder when files belong together under one link: a site with its own styles, scripts and images, a gallery, or a set of documents. create_folder makes one, add_folder_text writes inline files into it by path, and upload_folder sends local files and directories. A folder holds up to 100 files and 100 MiB. A folder with index.html at its root opens as a site at <url>/files/.
10
12
  Return the artifact url to the user. Never return bearer tokens or put them in share URLs.
11
13
  Listing and details require artifacts:read, uploads and edits require artifacts:write, deletion requires artifacts:delete.
12
- View counts are page requests. Owners count separately only when authenticated; logged-out owners count as others. Bots are not filtered yet.`;function w(t){return{content:[{type:"text",text:JSON.stringify(t)}],structuredContent:{result:t}}}async function i(t){try{return w(await t())}catch(n){return{content:[{type:"text",text:n instanceof Error?n.message:"Tool failed"}],isError:!0}}}function T(t,n=[]){let r=new g({name:"seshat",version:"0.1.0"},{instructions:m});r.registerResource("guide","seshat://guide",{description:"Upload formats, authorization scopes, expiration, retry semantics, and share-link behavior",mimeType:"text/plain"},async e=>({contents:[{uri:e.href,mimeType:"text/plain",text:m}]}));let d=l.describe("Stable ID for this logical upload. Reuse unchanged when retrying");return r.registerTool("seshat_upload_text",{description:"Publish inline HTML, Markdown, JSON, or text and return a public unlisted share URL. No expiration by default; missing titles and descriptions are generated automatically. Maximum 10 MiB. Do not upload secrets.",inputSchema:{content:a.string().min(1).max(10*1024*1024),filename:a.string(),contentType:a.enum(["text/html","text/markdown","application/json","text/plain"]),metadata:o,requestId:d},annotations:{destructiveHint:!1,idempotentHint:!0,openWorldHint:!0}},async e=>i(async()=>({...await t.upload({bytes:new TextEncoder().encode(e.content),filename:e.filename,contentType:e.contentType,metadata:e.metadata,requestId:e.requestId}),requestId:e.requestId}))),r.registerTool("seshat_upload_file",{description:"Publish a local file up to 10 MiB and return a public unlisted share URL. Path must be inside configured SESHAT_UPLOAD_ROOTS. HTML must be self-contained. Do not upload secrets.",inputSchema:{path:a.string().min(1),metadata:o,requestId:d},annotations:{destructiveHint:!1,idempotentHint:!0,openWorldHint:!0}},async e=>i(async()=>{if(!n.length)throw new Error("File uploads are disabled. Set SESHAT_UPLOAD_ROOTS to a JSON array of allowed directories, or use seshat_upload_text");return{...await t.upload({...await p(e.path,n),metadata:e.metadata,requestId:e.requestId}),requestId:e.requestId}})),r.registerTool("seshat_list_artifacts",{description:"List artifacts in the authorized space, including share links and expiration. Follow nextCursor for more results. Requires artifacts:read.",inputSchema:{limit:a.number().int().min(1).max(100).optional(),cursor:a.string().optional()},annotations:{readOnlyHint:!0,destructiveHint:!1,openWorldHint:!0}},async e=>i(()=>t.list(e))),r.registerTool("seshat_get_artifact",{description:"Get an artifact's metadata, URL, and owner/other page-request counts. Requires artifacts:read.",inputSchema:{id:s},annotations:{readOnlyHint:!0,destructiveHint:!1,openWorldHint:!0}},async({id:e})=>i(()=>t.get(e))),r.registerTool("seshat_update_artifact",{description:"Edit title, description, expiration, or password protection in the authorized space. Omitted fields stay unchanged. expiresAt:null disables expiration. Requires artifacts:write.",inputSchema:{id:s,changes:c},annotations:{destructiveHint:!0,idempotentHint:!0,openWorldHint:!0}},async({id:e,changes:h})=>i(()=>t.update(e,h))),r.registerTool("seshat_delete_artifact",{description:"Permanently delete an artifact and invalidate its share link. Call only when the user requests deletion. Requires artifacts:delete and confirm:true.",inputSchema:{id:s,confirm:a.literal(!0)},annotations:{destructiveHint:!0,idempotentHint:!0,openWorldHint:!0}},async({id:e})=>i(()=>t.delete(e))),r}async function x(t={}){let n=JSON.parse(process.env.SESHAT_UPLOAD_ROOTS??"[]");await T(await u(t),a.array(a.string().min(1)).parse(n)).connect(new y)}if(process.argv[1]&&import.meta.url===f(S(process.argv[1])).href)try{await x()}catch{process.stderr.write(`Seshat MCP could not start. Run seshat login, or check SESHAT_PROFILE, SESHAT_TOKEN, and SESHAT_UPLOAD_ROOTS
13
- `),process.exitCode=1}export{m as agentGuide,T as createMcpServer,x as startMcp};
14
+ View counts are page requests. Owners count separately only when authenticated; logged-out owners count as others. Bots are not filtered yet.`;function E(t){return{content:[{type:"text",text:JSON.stringify(t)}],structuredContent:{result:t}}}async function s(t){try{return E(await t())}catch(o){return{content:[{type:"text",text:o instanceof Error?o.message:"Tool failed"}],isError:!0}}}function U(t,o=[]){let r=new R({name:"seshat",version:"0.1.0"},{instructions:q});r.registerResource("guide","seshat://guide",{description:"Upload formats, authorization scopes, expiration, retry semantics, and share-link behavior",mimeType:"text/plain"},async e=>({contents:[{uri:e.href,mimeType:"text/plain",text:q}]}));let l=g.describe("Stable ID for this logical upload. Reuse unchanged when retrying");r.registerTool("seshat_upload_text",{description:"Publish inline HTML, Markdown, JSON, or text and return a public unlisted share URL. No expiration by default; missing titles and descriptions are generated automatically. Maximum 10 MiB. Do not upload secrets.",inputSchema:{content:a.string().min(1).max(10*1024*1024),filename:a.string(),contentType:a.enum(["text/html","text/markdown","application/json","text/plain"]),metadata:p,requestId:l},annotations:{destructiveHint:!1,idempotentHint:!0,openWorldHint:!0}},async e=>s(async()=>({...await t.upload({bytes:new TextEncoder().encode(e.content),filename:e.filename,contentType:e.contentType,metadata:e.metadata,requestId:e.requestId}),requestId:e.requestId})));let h=()=>{if(!o.length)throw new Error("File uploads are disabled. Set SESHAT_UPLOAD_ROOTS to a JSON array of allowed directories, or use the inline text tools");return o};r.registerTool("seshat_upload_file",{description:"Publish a local file up to 10 MiB and return a public unlisted share URL. Path must be inside configured SESHAT_UPLOAD_ROOTS. HTML must be self-contained. Do not upload secrets.",inputSchema:{path:a.string().min(1),metadata:p,requestId:l},annotations:{destructiveHint:!1,idempotentHint:!0,openWorldHint:!0}},async e=>s(async()=>({...await t.upload({...await S(e.path,h()),metadata:e.metadata,requestId:e.requestId}),requestId:e.requestId})));let m={expiresAt:p.shape.expiresAt,password:p.shape.password};return r.registerTool("seshat_upload_files",{description:"Publish several local files, each as its own artifact with its own share URL. Paths must be inside configured SESHAT_UPLOAD_ROOTS. Titles and descriptions are generated. One failing file does not stop the rest; check failed. Do not upload secrets.",inputSchema:{paths:a.array(a.string().min(1)).min(1).max(25),...m,requestId:l},annotations:{destructiveHint:!1,idempotentHint:!0,openWorldHint:!0}},async e=>s(async()=>{let i=h(),d=[],c=[];for(let[u,w]of e.paths.entries()){let x=g.parse(`${e.requestId}-${u+1}`);try{d.push({...await t.upload({...await S(w,i),metadata:{expiresAt:e.expiresAt,password:e.password},requestId:x}),requestId:x})}catch(_){c.push({path:w,message:_ instanceof Error?_.message:"Upload failed"})}}return{artifacts:d,failed:c}})),r.registerTool("seshat_create_folder",{description:"Create an empty folder: one artifact with one share URL that holds files by path. Fill it with seshat_add_folder_text or seshat_upload_folder. Requires artifacts:write.",inputSchema:{name:f.shape.name,title:f.shape.title,description:a.string().max(2e3).optional(),...m,requestId:l},annotations:{destructiveHint:!1,idempotentHint:!0,openWorldHint:!0}},async({requestId:e,...i})=>s(async()=>({...await t.createFolder({folder:{...i,description:i.description??""},requestId:e}),requestId:e}))),r.registerTool("seshat_add_folder_text",{description:"Write one inline text file into a folder at a relative path such as index.html or css/site.css, replacing any file already there. The type is taken from the file name. Files in a folder can reference each other by relative path. Requires artifacts:write.",inputSchema:{id:n,path:y,content:a.string().min(1).max(10*1024*1024)},annotations:{destructiveHint:!0,idempotentHint:!0,openWorldHint:!0}},async({id:e,path:i,content:d})=>s(()=>t.putFile(e,{path:i,bytes:new TextEncoder().encode(d)}))),r.registerTool("seshat_upload_folder",{description:"Send local files and directories into one folder. Give name to create a folder, or id to add to an existing one. A file is stored under its own name; a directory contributes its contents and keeps their layout; prefix places everything in a subfolder. Hidden files and node_modules are skipped. Unchanged files are not sent again. Paths must be inside configured SESHAT_UPLOAD_ROOTS.",inputSchema:{paths:a.array(a.string().min(1)).min(1).max(25),name:f.shape.name.optional(),id:n.optional(),prefix:a.string().max(200).optional(),title:f.shape.title,description:a.string().max(2e3).optional(),...m,requestId:l},annotations:{destructiveHint:!1,idempotentHint:!0,openWorldHint:!0}},async e=>s(async()=>{if(e.name===void 0==(e.id===void 0))throw new Error("Give name to create a folder, or id to add to an existing one");let i=await v(e.paths,{prefix:e.prefix,allowedRoots:h()}),d=e.name===void 0?void 0:await t.createFolder({folder:{name:e.name,title:e.title,description:e.description??"",expiresAt:e.expiresAt,password:e.password},requestId:e.requestId}),c=d?.id??n.parse(e.id),u=await O(t,c,i);return{...d??{id:c},requestId:e.requestId,files:{total:i.length,uploaded:u.uploaded,unchanged:u.unchanged},failed:u.failed}})),r.registerTool("seshat_list_folder_files",{description:"List the files in a folder with their paths, types, sizes and digests. Requires artifacts:read.",inputSchema:{id:n},annotations:{readOnlyHint:!0,destructiveHint:!1,openWorldHint:!0}},async({id:e})=>s(()=>t.listFiles(e))),r.registerTool("seshat_delete_folder_file",{description:"Remove one file from a folder. Call only when the user asks for it. Requires artifacts:delete and confirm:true.",inputSchema:{id:n,path:y,confirm:a.literal(!0)},annotations:{destructiveHint:!0,idempotentHint:!0,openWorldHint:!0}},async({id:e,path:i})=>s(()=>t.deleteFile(e,i))),r.registerTool("seshat_list_artifacts",{description:"List artifacts in the authorized space, including share links and expiration. Follow nextCursor for more results. Requires artifacts:read.",inputSchema:{limit:a.number().int().min(1).max(100).optional(),cursor:a.string().optional()},annotations:{readOnlyHint:!0,destructiveHint:!1,openWorldHint:!0}},async e=>s(()=>t.list(e))),r.registerTool("seshat_get_artifact",{description:"Get an artifact's metadata, URL, and owner/other page-request counts. Requires artifacts:read.",inputSchema:{id:n},annotations:{readOnlyHint:!0,destructiveHint:!1,openWorldHint:!0}},async({id:e})=>s(()=>t.get(e))),r.registerTool("seshat_update_artifact",{description:"Edit title, description, expiration, or password protection in the authorized space. Omitted fields stay unchanged. expiresAt:null disables expiration. Requires artifacts:write.",inputSchema:{id:n,changes:H},annotations:{destructiveHint:!0,idempotentHint:!0,openWorldHint:!0}},async({id:e,changes:i})=>s(()=>t.update(e,i))),r.registerTool("seshat_delete_artifact",{description:"Permanently delete an artifact and invalidate its share link. Call only when the user requests deletion. Requires artifacts:delete and confirm:true.",inputSchema:{id:n,confirm:a.literal(!0)},annotations:{destructiveHint:!0,idempotentHint:!0,openWorldHint:!0}},async({id:e})=>s(()=>t.delete(e))),r}async function L(t={}){let o=JSON.parse(process.env.SESHAT_UPLOAD_ROOTS??"[]");await U(await T(t),a.array(a.string().min(1)).parse(o)).connect(new I)}if(process.argv[1]&&import.meta.url===b(A(process.argv[1])).href)try{await L()}catch{process.stderr.write(`Seshat MCP could not start. Run seshat login, or check SESHAT_PROFILE, SESHAT_TOKEN, and SESHAT_UPLOAD_ROOTS
15
+ `),process.exitCode=1}export{q as agentGuide,U as createMcpServer,L as startMcp};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seshat.sh/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Publish and share artifacts with Seshat from terminals and agents.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -1,3 +0,0 @@
1
- import{z as r}from"zod";import{z as te}from"zod";var P=te.enum(["never","hour","day","week","month","year","custom"]);var b=10*1024*1024,z=["artifacts:read","artifacts:write","artifacts:delete","tokens:manage"],v=r.enum(z),l=r.string().regex(/^[a-f0-9]{48}$/),R=r.string().min(8).max(128).regex(/^[A-Za-z0-9_-]+$/),u=r.string().regex(/^seshat_[A-Za-z0-9_-]{43}$/),F=r.iso.datetime().transform(e=>new Date(e).toISOString()),h=r.object({title:r.string().trim().min(1).max(200).optional(),description:r.string().max(2e3).default(""),expiresAt:F.nullable().optional(),expirationPreset:P.nullable().optional(),password:r.string().min(12).max(128).nullable().optional(),generateMetadata:r.boolean().optional()}).strict().refine(e=>e.expirationPreset==null||(e.expirationPreset==="never"?e.expiresAt===null:typeof e.expiresAt=="string"),"Expiration preset must match an explicit expiration value"),N=r.object({filename:r.string().min(1).max(200).regex(/^[^/\\\x00-\x1f\x7f]+$/),contentType:r.string().max(150).regex(/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/i),size:r.number().int().min(1).max(b)}).strict(),_=r.object({title:h.shape.title.optional(),description:r.string().max(2e3).optional(),expiresAt:h.shape.expiresAt,expirationPreset:h.shape.expirationPreset,password:h.shape.password}).strict().refine(e=>e.expirationPreset==null||(e.expirationPreset==="never"?e.expiresAt===null:typeof e.expiresAt=="string"),"Expiration preset must match an explicit expiration value").refine(e=>Object.values(e).some(t=>t!==void 0),"Provide at least one metadata field"),$=r.object({slug:r.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).min(2).max(63),name:r.string().trim().min(1).max(100)}).strict(),f=r.object({name:r.string().trim().min(1).max(100),scopes:r.array(v).min(1).max(z.length),expiresAt:F.optional()}).strict(),g=r.object({id:l,title:r.string(),description:r.string(),filename:r.string(),contentType:r.string(),size:r.number(),createdAt:r.string(),expiresAt:r.string().nullable(),expirationPreset:P.nullable().default(null),url:r.url(),downloadUrl:r.url(),passwordProtected:r.boolean().default(!1),views:r.object({owner:r.number(),others:r.number()})}),q=r.object({artifacts:r.array(g),nextCursor:r.string().nullable()}),E=r.object({id:l,token:u,scopes:r.array(v),expiresAt:r.string().nullable()}),O=r.object({space:r.object({id:l,slug:r.string(),name:r.string()}),credential:E}),M=r.object({spaceId:l,tokenId:l,scopes:f.shape.scopes,space:O.shape.space.optional()}),H=r.object({error:r.object({code:r.string(),message:r.string()})});import{z as m}from"zod";import{z as o}from"zod";var B="https://app.seshat.sh",S=o.url().refine(e=>{let t=new URL(e);return!t.username&&!t.password&&!t.search&&!t.hash&&t.pathname==="/"&&(t.protocol==="https:"||t.protocol==="http:"&&["localhost","127.0.0.1","[::1]"].includes(t.hostname))},"Use an HTTPS origin, or HTTP localhost for development").transform(e=>new URL(e).origin),k=o.string().min(1).max(64).regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/),je=o.string().max(16).transform(e=>e.toUpperCase().replaceAll("-","").trim()).pipe(o.string().regex(/^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{8}$/)),j=o.string().trim().min(1).max(80).regex(/^[^\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]+$/),re=o.strictObject({city:j.optional(),region:j.optional(),country:o.string().regex(/^[A-Z]{2}$/).optional()}).refine(e=>!!(e.city||e.region||e.country),"Location is unavailable"),C=o.strictObject({kind:o.enum(["laptop","desktop","phone","tablet","terminal"]),os:o.enum(["macos","windows","linux","android","ios","other"]),model:j.optional(),client:o.enum(["terminal","agent"])});var Ce=f.extend({tokenHash:o.string().regex(/^[a-f0-9]{64}$/),device:C.optional()}).strict().refine(e=>!e.expiresAt||Date.parse(e.expiresAt)>Date.now()&&Date.parse(e.expiresAt)<=Date.now()+366*864e5,"Choose a credential expiry within the next year"),Ie=o.strictObject({deviceCode:l}),Ue=o.strictObject({approve:o.boolean()}),T=o.object({id:l,slug:o.string(),name:o.string()}),De=o.object({deviceCode:l,userCode:o.string().regex(/^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{4}-[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{4}$/),verificationUrl:o.url(),expiresAt:o.iso.datetime(),interval:o.number().int().min(1).max(60)}),Le=o.discriminatedUnion("status",[o.object({status:o.literal("pending"),interval:o.number().int().min(1).max(60)}),o.object({status:o.literal("denied")}),o.object({status:o.literal("approved"),id:l,space:T,scopes:f.shape.scopes,expiresAt:o.iso.datetime()})]),ze=o.object({device:C.nullable().default(null),userCode:o.string(),name:o.string(),scopes:f.shape.scopes,status:o.enum(["pending","approved","denied"]),expiresAt:o.iso.datetime(),tokenExpiresAt:o.iso.datetime(),space:T});var Re=o.object({devices:o.array(o.object({id:l,name:o.string(),device:C.nullable().default(null),scopes:f.shape.scopes,createdAt:o.iso.datetime(),completedAt:o.iso.datetime().nullable(),lastUsedAt:o.iso.datetime().nullable(),lastIp:o.union([o.ipv4(),o.ipv6()]).nullable().default(null),lastLocation:re.nullable().default(null),expiresAt:o.iso.datetime().nullable()}))});var ne=m.object({endpoint:S,token:u}).strict();async function oe({form:e,onProgress:t}){let n=new Response(e),i=n.headers.get("content-type");if(!i)throw new Error("Could not encode the upload");let a=new Uint8Array(await n.arrayBuffer()),s=0;return t({loadedBytes:0,totalBytes:a.byteLength}),{body:new ReadableStream({pull(p){let w=Math.min(s+65536,a.byteLength);p.enqueue(a.subarray(s,w)),s=w,t({loadedBytes:s,totalBytes:a.byteLength}),s===a.byteLength&&p.close()}},{highWaterMark:0}),headers:{"content-type":i,"content-length":String(a.byteLength)},duplex:"half"}}var A=class extends Error{constructor(n,i,a){super(i);this.code=n;this.status=a}code;status},x=class{settings;constructor(t){this.settings=ne.parse(t)}async request(t,n,i={}){let a=new Headers(i.headers);a.set("authorization",`Bearer ${this.settings.token}`),typeof i.body=="string"&&a.set("content-type","application/json");let s;try{s=await fetch(new URL(t,this.settings.endpoint),{...i,headers:a,redirect:"error",signal:i.signal?AbortSignal.any([i.signal,AbortSignal.timeout(6e4)]):AbortSignal.timeout(6e4)})}catch{throw new A("connection_failed","Request failed or timed out. Check SESHAT_ENDPOINT; retry uploads with the same --request-id")}let c=await s.json();if(!s.ok){let p=H.safeParse(c);throw new A(p.success?p.data.error.code:"request_failed",p.success?p.data.error.message:`HTTP ${s.status}`,s.status)}return n.parse(c)}async upload(t){let n=N.parse({filename:t.filename,contentType:t.contentType,size:t.bytes.byteLength}),i=h.parse(t.metadata),a=R.parse(t.requestId),s=new FormData;if(s.set("metadata",JSON.stringify(i)),s.set("file",new Blob([new Uint8Array(t.bytes)],{type:n.contentType}),n.filename),t.onProgress){let c=await oe({form:s,onProgress:t.onProgress}),p={...c,method:"POST",headers:{...c.headers,"idempotency-key":a}};return this.request("/v1/artifacts",g,p)}return this.request("/v1/artifacts",g,{method:"POST",body:s,headers:{"idempotency-key":a}})}list(t={}){let n=new URLSearchParams;return t.limit!==void 0&&n.set("limit",String(t.limit)),t.cursor!==void 0&&n.set("cursor",t.cursor),this.request(`/v1/artifacts?${n}`,q)}get(t){return this.request(`/v1/artifacts/${l.parse(t)}`,g)}update(t,n){return this.request(`/v1/artifacts/${l.parse(t)}`,g,{method:"PATCH",body:JSON.stringify(_.parse(n))})}delete(t){return this.request(`/v1/artifacts/${l.parse(t)}`,m.object({deleted:m.literal(!0),id:l}),{method:"DELETE"})}whoami(t={}){return this.request("/v1/me",M,{signal:t.signal})}completeConnection(){return this.request("/v1/auth/complete",m.object({completed:m.literal(!0)}),{method:"POST"})}revokeSelf(){return this.request("/v1/auth/logout",m.object({revoked:m.literal(!0),id:l}),{method:"POST"})}createSpace(t){return this.request("/v1/admin/spaces",O,{method:"POST",body:JSON.stringify($.parse(t))})}createToken(t){return this.request("/v1/tokens",E,{method:"POST",body:JSON.stringify(f.parse(t))})}revokeToken(t){return this.request(`/v1/tokens/${l.parse(t)}`,m.object({revoked:m.literal(!0),id:l}),{method:"DELETE"})}};import{constants as W}from"node:fs";import{lstat as ie,mkdir as ae,open as I,rename as se,unlink as J}from"node:fs/promises";import{homedir as ce}from"node:os";import{join as y,resolve as le}from"node:path";import{randomUUID as Y}from"node:crypto";import{setTimeout as pe}from"node:timers/promises";import{z as d}from"zod";var Qe=d.enum(["keychain","file"]),Z=d.strictObject({endpoint:S,tokenId:l,space:T,scopes:d.array(v),expiresAt:d.iso.datetime(),pendingConnection:d.literal(!0).optional(),credential:d.discriminatedUnion("kind",[d.strictObject({kind:d.literal("keychain"),account:d.string().min(1)}),d.strictObject({kind:d.literal("file"),token:u})])}),de=d.strictObject({version:d.literal(1),profiles:d.record(k,Z)});function X(){return le(process.env.SESHAT_CONFIG_DIR??y(process.env.XDG_CONFIG_HOME??y(ce(),".config"),"seshat"))}function G(e){return e instanceof Error&&"code"in e&&e.code==="ENOENT"}async function K(e,t=!1){let n=await ie(e);if(n.isSymbolicLink()||(t?!n.isDirectory():!n.isFile()))throw new Error("Seshat credential paths must be real files and directories, not symlinks");if(process.platform!=="win32"&&((n.mode&63)!==0||process.getuid&&n.uid!==process.getuid()))throw new Error(`Credential path must belong to you and have ${t?"0700":"0600"} permissions: ${e}`)}async function Q(){let e=X();try{await K(e,!0);let t=y(e,"profiles.json"),n=await I(t,W.O_RDONLY|(W.O_NOFOLLOW??0));try{let i=await n.stat();if(!i.isFile()||i.size>65536||process.platform!=="win32"&&((i.mode&63)!==0||process.getuid&&i.uid!==process.getuid()))throw new Error("Credential file must be a small, owner-only regular file");try{let a=de.safeParse(JSON.parse(await n.readFile("utf8")));if(a.success)return a.data}catch{}throw new Error("Invalid Seshat profile file. Run seshat login again")}finally{await n.close()}}catch(t){if(G(t))return{version:1,profiles:{}};throw t}}async function D(e,t){let n=X();await ae(n,{recursive:!0,mode:448}),await K(n,!0);let i=y(n,"profiles.lock"),a;for(let c=0;c<40;c++)try{a=await I(i,"wx",384);break}catch(p){if(!(p instanceof Error&&"code"in p&&p.code==="EEXIST"))throw p;await pe(50)}if(!a)throw new Error("Another Seshat process is updating profiles. Retry shortly; if it stopped unexpectedly, remove profiles.lock from your Seshat config directory");let s=y(n,`profiles-${Y()}.tmp`);try{t?.throwIfAborted();let c=e(await Q()),p=await I(s,"wx",384);try{let w=JSON.stringify(c)+`
2
- `;if(Buffer.byteLength(w)>65536)throw new Error("Too many stored profiles; remove unused connections before adding another");await p.writeFile(w),await p.sync()}finally{await p.close()}t?.throwIfAborted(),await se(s,y(n,"profiles.json"))}finally{try{await J(s).catch(c=>{if(!G(c))throw c})}finally{await a.close(),await J(i)}}}async function L(e){let{AsyncEntry:t}=await import("@napi-rs/keyring");return new t("dev.djamil.seshat.cli",e,{linux:{store:"secret-service"}})}async function U(e){e.credential.kind==="keychain"&&await(await L(e.credential.account)).deletePassword(AbortSignal.timeout(15e3))}async function Ve(e){let t=k.parse(e.name),n=u.parse(e.token);if(e.storage==="file"&&process.platform==="win32")throw new Error("Use keychain storage with Windows Credential Manager");let i=Y();if(e.storage==="keychain")try{await(await L(i)).setPassword(n,e.signal?AbortSignal.any([e.signal,AbortSignal.timeout(15e3)]):AbortSignal.timeout(15e3))}catch{throw e.signal?.aborted?new Error("Connection cancelled"):new Error("The OS credential vault is unavailable. On a headless POSIX machine, rerun login with --storage file to use an owner-only credential file")}let a=Z.parse({pendingConnection:e.pendingConnection,endpoint:e.endpoint,tokenId:e.tokenId,space:e.space,scopes:e.scopes,expiresAt:e.expiresAt,credential:e.storage==="keychain"?{kind:"keychain",account:i}:{kind:"file",token:n}}),s;try{await D(c=>{if(s=Object.hasOwn(c.profiles,t)?c.profiles[t]:void 0,(s?.tokenId??null)!==e.expectedTokenId)throw new Error("This profile changed during login. Retry; the newer connection has been preserved");return{...c,profiles:{...c.profiles,[t]:a}}},e.signal)}catch(c){throw await U(a).catch(()=>{}),c}return s&&await U(s).catch(()=>{process.stderr.write(`The previous OS credential entry could not be removed. Revoke its token in Seshat Agent access.
3
- `)}),a}function me(e){return k.parse(e??process.env.SESHAT_PROFILE??"default")}async function V(e){let t=await Q();return Object.hasOwn(t.profiles,e)?t.profiles[e]:void 0}async function fe(e){if(e.credential.kind==="file")return e.credential.token;try{return u.parse(await(await L(e.credential.account)).getPassword(AbortSignal.timeout(15e3)))}catch{throw new Error("Stored credential is unavailable. Run seshat login again for this profile")}}async function et(e,t){let n=await V(e);if(n){if(n.tokenId!==t)throw new Error("This profile changed during logout. The newer connection has been preserved");await U(n),await D(i=>{let a=Object.hasOwn(i.profiles,e)?i.profiles[e]:void 0;if(a&&a.tokenId!==t)throw new Error("This profile changed during logout. The newer connection has been preserved");let s={...i.profiles};return delete s[e],{...i,profiles:s}})}}async function ue(e,t,n){t.pendingConnection&&(await n.completeConnection(),await D(i=>{let a=Object.hasOwn(i.profiles,e)?i.profiles[e]:void 0;if(a?.tokenId!==t.tokenId)throw new Error("This profile changed during connection. The newer connection has been preserved");let{pendingConnection:s,...c}=a;return{...i,profiles:{...i.profiles,[e]:c}}}))}async function tt(e={}){let t=e.endpoint??process.env.SESHAT_ENDPOINT;if(process.env.SESHAT_TOKEN&&!e.profile&&!process.env.SESHAT_PROFILE)return new x({endpoint:t??B,token:process.env.SESHAT_TOKEN});let n=me(e.profile),i=await V(n);if(!i)throw new Error(`No credential for profile "${n}". Run seshat login --profile ${n}`);if(t&&S.parse(t)!==i.endpoint)throw new Error("This profile belongs to another endpoint. Log in with a different --profile; stored credentials are never sent to a different service");let a=new x({endpoint:i.endpoint,token:await fe(i)});return await ue(n,i,a),a}function rt(e,t){return{profile:e,endpoint:t.endpoint,tokenId:t.tokenId,space:t.space,scopes:t.scopes,expiresAt:t.expiresAt,storage:t.credential.kind}}import{realpath as ee,stat as he,readFile as ge}from"node:fs/promises";import{basename as ye,extname as we,relative as Se,isAbsolute as xe}from"node:path";var be={".html":"text/html",".htm":"text/html",".md":"text/markdown",".markdown":"text/markdown",".json":"application/json",".pdf":"application/pdf",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".webp":"image/webp",".svg":"image/svg+xml",".txt":"text/plain"};async function st(e,t){let n=await ee(e);if(t&&!(await Promise.all(t.map(s=>ee(s)))).some(s=>{let c=Se(s,n);return c!==".."&&!c.startsWith("../")&&!xe(c)}))throw new Error("File is outside SESHAT_UPLOAD_ROOTS. Configure explicit directories for MCP file access");let i=await he(n);if(!i.isFile())throw new Error("Upload path must be a regular file");if(i.size<1||i.size>b)throw new Error(`File must contain 1 to ${b} bytes`);return{bytes:new Uint8Array(await ge(n)),filename:ye(n),contentType:be[we(n).toLowerCase()]??"application/octet-stream"}}export{b as a,v as b,l as c,R as d,h as e,_ as f,H as g,B as h,S as i,C as j,De as k,Le as l,A as m,x as n,Qe as o,Ve as p,me as q,V as r,fe as s,et as t,ue as u,tt as v,rt as w,st as x};