@seshat.sh/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Seshat CLI
2
2
 
3
- Publish HTML, Markdown, images, PDF, JSON, and other artifacts to your space on [Seshat](https://seshat.djamil.dev). Node.js 22 or newer is required. Seshat is an invite-only hosted service; no server clone or Cloudflare account is needed.
3
+ Publish HTML, Markdown, images, PDF, JSON, and other artifacts to your space on [Seshat](https://seshat.sh). Node.js 22 or newer is required. Seshat is an invite-only hosted service; no server clone or Cloudflare account is needed.
4
4
 
5
5
  ```sh
6
6
  npx --yes @seshat.sh/cli login
@@ -28,15 +28,26 @@ seshat whoami
28
28
  seshat logout
29
29
  ```
30
30
 
31
- Logout revokes the connection on Seshat before removing the local credential. Connections can also be revoked in the web app under Agent access. Credentials expire after 90 days by default; `login --expires-at ISO_DATE` chooses a date within the next year.
31
+ `seshat whoami` shows your space name and readable permissions:
32
+
33
+ ```text
34
+ ✓ Connected to Seshat
35
+
36
+ Space Djamil
37
+ Access Read artifacts · Upload and edit
38
+ ```
39
+
40
+ Use `seshat whoami --json` for the space ID, credential ID, and scope values. Use `--json` in scripts and pipelines too.
41
+
42
+ Logout revokes the connection on Seshat before removing the local credential. Connections are listed under **Agent access → Connected devices**, with their name, permissions, last activity, and expiry. Remove a device there to revoke access immediately; its artifacts remain available. Credentials expire after 90 days by default; `login --expires-at ISO_DATE` chooses a date within the next year.
32
43
 
33
44
  ## Connect an agent
34
45
 
35
46
  Use a separate profile for each agent or harness. Setup defaults to upload-only access and returns a profile-based upload command, without a token to paste into configuration.
36
47
 
37
48
  ```sh
38
- seshat agent setup --profile t3 --name "T3 agent"
39
- seshat upload visualizer.html --profile t3
49
+ seshat agent setup --profile t3 --name "T3 agent" --json
50
+ seshat upload visualizer.html --profile t3 --json
40
51
  ```
41
52
 
42
53
  Set `SESHAT_PROFILE=t3` in the harness environment to use that profile automatically. Use `--scopes artifacts:read,artifacts:write` during setup if the agent needs to list or inspect uploads. Existing resource commands remain available; run `seshat --help` and `seshat upload --help` for examples.
@@ -51,7 +62,7 @@ cat notes.md | seshat upload - --filename notes.md --content-type text/markdown
51
62
  seshat list --limit 20
52
63
  ```
53
64
 
54
- Successful commands write JSON to stdout. 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.
65
+ 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.
55
66
 
56
67
  ## Local MCP
57
68
 
@@ -0,0 +1,3 @@
1
+ import{z as r}from"zod";import{z as V}from"zod";var A=V.enum(["never","hour","day","week","month","year","custom"]);var y=10*1024*1024,D=["artifacts:read","artifacts:write","artifacts:delete","tokens:manage"],b=r.enum(D),l=r.string().regex(/^[a-f0-9]{48}$/),z=r.string().min(8).max(128).regex(/^[A-Za-z0-9_-]+$/),h=r.string().regex(/^seshat_[A-Za-z0-9_-]{43}$/),R=r.iso.datetime().transform(e=>new Date(e).toISOString()),u=r.object({title:r.string().trim().min(1).max(200).optional(),description:r.string().max(2e3).default(""),expiresAt:R.nullable().optional(),expirationPreset:A.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"),F=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(y)}).strict(),N=r.object({title:u.shape.title.optional(),description:r.string().max(2e3).optional(),expiresAt:u.shape.expiresAt,expirationPreset:u.shape.expirationPreset,password:u.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(b).min(1).max(D.length),expiresAt:R.optional()}).strict(),w=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:A.nullable().default(null),url:r.url(),downloadUrl:r.url(),passwordProtected:r.boolean().default(!1),views:r.object({owner:r.number(),others:r.number()})}),L=r.object({artifacts:r.array(w),nextCursor:r.string().nullable()}),E=r.object({id:l,token:h,scopes:r.array(b),expiresAt:r.string().nullable()}),P=r.object({space:r.object({id:l,slug:r.string(),name:r.string()}),credential:E}),$=r.object({spaceId:l,tokenId:l,scopes:f.shape.scopes,space:P.shape.space.optional()}),q=r.object({error:r.object({code:r.string(),message:r.string()})});import{z as m}from"zod";import{z as i}from"zod";var M="https://app.seshat.sh",S=i.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),v=i.string().min(1).max(64).regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/),Ae=i.string().max(16).transform(e=>e.toUpperCase().replaceAll("-","").trim()).pipe(i.string().regex(/^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{8}$/)),Ee=f.extend({tokenHash:i.string().regex(/^[a-f0-9]{64}$/)}).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"),Pe=i.strictObject({deviceCode:l}),Oe=i.strictObject({approve:i.boolean()}),k=i.object({id:l,slug:i.string(),name:i.string()}),je=i.object({deviceCode:l,userCode:i.string().regex(/^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{4}-[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{4}$/),verificationUrl:i.url(),expiresAt:i.iso.datetime(),interval:i.number().int().min(1).max(60)}),Ce=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:l,space:k,scopes:f.shape.scopes,expiresAt:i.iso.datetime()})]),Ie=i.object({userCode:i.string(),name:i.string(),scopes:f.shape.scopes,status:i.enum(["pending","approved","denied"]),expiresAt:i.iso.datetime(),tokenExpiresAt:i.iso.datetime(),space:k});var Ue=i.object({devices:i.array(i.object({id:l,name:i.string(),scopes:f.shape.scopes,createdAt:i.iso.datetime(),completedAt:i.iso.datetime().nullable(),lastUsedAt:i.iso.datetime().nullable(),expiresAt:i.iso.datetime().nullable()}))});var ee=m.object({endpoint:S,token:h}).strict(),T=class extends Error{constructor(n,o,a){super(o);this.code=n;this.status=a}code;status},x=class{settings;constructor(t){this.settings=ee.parse(t)}async request(t,n,o={}){let a=new Headers(o.headers);a.set("authorization",`Bearer ${this.settings.token}`),typeof o.body=="string"&&a.set("content-type","application/json");let s;try{s=await fetch(new URL(t,this.settings.endpoint),{...o,headers:a,redirect:"error",signal:o.signal?AbortSignal.any([o.signal,AbortSignal.timeout(6e4)]):AbortSignal.timeout(6e4)})}catch{throw new T("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 d=q.safeParse(c);throw new T(d.success?d.data.error.code:"request_failed",d.success?d.data.error.message:`HTTP ${s.status}`,s.status)}return n.parse(c)}async upload(t){let n=F.parse({filename:t.filename,contentType:t.contentType,size:t.bytes.byteLength}),o=u.parse(t.metadata),a=z.parse(t.requestId),s=new FormData;return s.set("metadata",JSON.stringify(o)),s.set("file",new Blob([new Uint8Array(t.bytes)],{type:n.contentType}),n.filename),this.request("/v1/artifacts",w,{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}`,L)}get(t){return this.request(`/v1/artifacts/${l.parse(t)}`,w)}update(t,n){return this.request(`/v1/artifacts/${l.parse(t)}`,w,{method:"PATCH",body:JSON.stringify(N.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",$,{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",P,{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 H}from"node:fs";import{lstat as te,mkdir as re,open as O,rename as ne,unlink as J}from"node:fs/promises";import{homedir as oe}from"node:os";import{join as g,resolve as ie}from"node:path";import{randomUUID as Y}from"node:crypto";import{setTimeout as ae}from"node:timers/promises";import{z as p}from"zod";var Ze=p.enum(["keychain","file"]),B=p.strictObject({endpoint:S,tokenId:l,space:k,scopes:p.array(b),expiresAt:p.iso.datetime(),pendingConnection:p.literal(!0).optional(),credential:p.discriminatedUnion("kind",[p.strictObject({kind:p.literal("keychain"),account:p.string().min(1)}),p.strictObject({kind:p.literal("file"),token:h})])}),se=p.strictObject({version:p.literal(1),profiles:p.record(v,B)});function X(){return ie(process.env.SESHAT_CONFIG_DIR??g(process.env.XDG_CONFIG_HOME??g(oe(),".config"),"seshat"))}function Z(e){return e instanceof Error&&"code"in e&&e.code==="ENOENT"}async function W(e,t=!1){let n=await te(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 G(){let e=X();try{await W(e,!0);let t=g(e,"profiles.json"),n=await O(t,H.O_RDONLY|(H.O_NOFOLLOW??0));try{let o=await n.stat();if(!o.isFile()||o.size>65536||process.platform!=="win32"&&((o.mode&63)!==0||process.getuid&&o.uid!==process.getuid()))throw new Error("Credential file must be a small, owner-only regular file");try{let a=se.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(Z(t))return{version:1,profiles:{}};throw t}}async function C(e,t){let n=X();await re(n,{recursive:!0,mode:448}),await W(n,!0);let o=g(n,"profiles.lock"),a;for(let c=0;c<40;c++)try{a=await O(o,"wx",384);break}catch(d){if(!(d instanceof Error&&"code"in d&&d.code==="EEXIST"))throw d;await ae(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=g(n,`profiles-${Y()}.tmp`);try{t?.throwIfAborted();let c=e(await G()),d=await O(s,"wx",384);try{let U=JSON.stringify(c)+`
2
+ `;if(Buffer.byteLength(U)>65536)throw new Error("Too many stored profiles; remove unused connections before adding another");await d.writeFile(U),await d.sync()}finally{await d.close()}t?.throwIfAborted(),await ne(s,g(n,"profiles.json"))}finally{try{await J(s).catch(c=>{if(!Z(c))throw c})}finally{await a.close(),await J(o)}}}async function I(e){let{AsyncEntry:t}=await import("@napi-rs/keyring");return new t("dev.djamil.seshat.cli",e,{linux:{store:"secret-service"}})}async function j(e){e.credential.kind==="keychain"&&await(await I(e.credential.account)).deletePassword(AbortSignal.timeout(15e3))}async function We(e){let t=v.parse(e.name),n=h.parse(e.token);if(e.storage==="file"&&process.platform==="win32")throw new Error("Use keychain storage with Windows Credential Manager");let o=Y();if(e.storage==="keychain")try{await(await I(o)).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=B.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:o}:{kind:"file",token:n}}),s;try{await C(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 j(a).catch(()=>{}),c}return s&&await j(s).catch(()=>{process.stderr.write(`The previous OS credential entry could not be removed. Revoke its token in Seshat Agent access.
3
+ `)}),a}function ce(e){return v.parse(e??process.env.SESHAT_PROFILE??"default")}async function K(e){let t=await G();return Object.hasOwn(t.profiles,e)?t.profiles[e]:void 0}async function le(e){if(e.credential.kind==="file")return e.credential.token;try{return h.parse(await(await I(e.credential.account)).getPassword(AbortSignal.timeout(15e3)))}catch{throw new Error("Stored credential is unavailable. Run seshat login again for this profile")}}async function Ge(e,t){let n=await K(e);if(n){if(n.tokenId!==t)throw new Error("This profile changed during logout. The newer connection has been preserved");await j(n),await C(o=>{let a=Object.hasOwn(o.profiles,e)?o.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={...o.profiles};return delete s[e],{...o,profiles:s}})}}async function pe(e,t,n){t.pendingConnection&&(await n.completeConnection(),await C(o=>{let a=Object.hasOwn(o.profiles,e)?o.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{...o,profiles:{...o.profiles,[e]:c}}}))}async function Ke(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??M,token:process.env.SESHAT_TOKEN});let n=ce(e.profile),o=await K(n);if(!o)throw new Error(`No credential for profile "${n}". Run seshat login --profile ${n}`);if(t&&S.parse(t)!==o.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:o.endpoint,token:await le(o)});return await pe(n,o,a),a}function Qe(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 Q,stat as de,readFile as me}from"node:fs/promises";import{basename as fe,extname as he,relative as ue,isAbsolute as ge}from"node:path";var we={".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 nt(e,t){let n=await Q(e);if(t&&!(await Promise.all(t.map(s=>Q(s)))).some(s=>{let c=ue(s,n);return c!==".."&&!c.startsWith("../")&&!ge(c)}))throw new Error("File is outside SESHAT_UPLOAD_ROOTS. Configure explicit directories for MCP file access");let o=await de(n);if(!o.isFile())throw new Error("Upload path must be a regular file");if(o.size<1||o.size>y)throw new Error(`File must contain 1 to ${y} bytes`);return{bytes:new Uint8Array(await me(n)),filename:fe(n),contentType:we[he(n).toLowerCase()]??"application/octet-stream"}}export{y as a,b,l as c,z as d,u as e,N as f,q as g,M as h,S as i,je as j,Ce as k,T as l,x as m,Ze as n,We as o,ce as p,K as q,le as r,Ge as s,pe as t,Ke as u,Qe as v,nt as w};
package/dist/cli.js CHANGED
@@ -1,10 +1,25 @@
1
1
  #!/usr/bin/env node
2
- import{a as O,b,d as j,g as H,h as N,i as _,j as B,k as D,l as y,m as x,n as $,o as J,p as v,q as A,r as P,s as K,t as U,u as S,v as I,w as M}from"./chunk-ZDGADCDP.js";import{parseArgs as ae}from"node:util";import{pathToFileURL as oe}from"node:url";import{realpathSync as se}from"node:fs";import{z as R}from"zod";import{randomBytes as Z,createHash as Y}from"node:crypto";import{execFile as Q}from"node:child_process";import{promisify as V}from"node:util";import{hostname as ee}from"node:os";import{setTimeout as te}from"node:timers/promises";import{z}from"zod";var q=V(Q);async function F(t,r,e,n,l){let h=await fetch(t+r,{method:"POST",redirect:"error",headers:{"content-type":"application/json"},body:JSON.stringify(e),signal:l?AbortSignal.any([l,AbortSignal.timeout(15e3)]):AbortSignal.timeout(15e3)}),i=await h.json();if(!h.ok){let u=H.safeParse(i);throw new y(u.success?u.data.error.code:"connection_failed",u.success?u.data.error.message:"Could not connect to Seshat",h.status)}let a=n.safeParse(i);if(!a.success)throw new Error("Unexpected connection response from the service");return a.data}async function re(t){process.platform==="darwin"?await q("open",[t],{timeout:1e4}):process.platform==="win32"?await q("rundll32",["url.dll,FileProtocolHandler",t],{timeout:1e4}):await q("xdg-open",[t],{timeout:1e4})}async function G(t){let r=v(t.profile),e=_.parse(t.endpoint??process.env.SESHAT_ENDPOINT??N),n=$.parse(t.storage??"keychain");if(n==="file"&&process.platform==="win32")throw new Error("Use keychain storage with Windows Credential Manager");let l=z.array(b).min(1).parse((t.scopes??"artifacts:read,artifacts:write").split(",")),h=z.coerce.number().int().min(5).max(600).parse(t.timeout??600),i=await A(r),a;if(i){try{a=new x({endpoint:i.endpoint,token:await P(i)})}catch{}if(!t.force&&t.expiresAt===void 0&&t.name===void 0&&(t.storage===void 0||t.storage===i.credential.kind)&&a&&i.endpoint===e&&i.expiresAt>new Date().toISOString()&&[...new Set(l)].sort().join(",")===[...i.scopes].sort().join(","))try{return await U(r,i,a),await a.whoami(),{connected:!0,alreadyConnected:!0,...I(r,i)}}catch(p){if(!(p instanceof y&&p.status===401))throw p}}let u="seshat_"+Z(32).toString("base64url"),E=Y("sha256").update(u).digest("hex"),s=await F(e,"/v1/auth/device",{name:t.name??`${ee().slice(0,60)} \xB7 ${r.slice(0,32)}`,scopes:l,tokenHash:E,...t.expiresAt?{expiresAt:t.expiresAt}:{}},B);if(!URL.canParse(s.verificationUrl))throw new Error("The service returned an invalid authorization URL");let d=new URL(s.verificationUrl);if(d.origin!==e||d.pathname!=="/connect"||d.username||d.password||d.hash||d.searchParams.get("code")!==s.userCode)throw new Error("The service returned an unexpected authorization URL");process.stderr.write(`Approve this terminal in Seshat:
2
+ import{a as U,b as O,d as W,g as G,h as q,i as F,j as X,k as j,l as T,m as A,n as Y,o as Z,p as x,q as D,r as L,s as V,t as H,u as C,v as N,w as Q}from"./chunk-W3JWSMKV.js";import{parseArgs as ge}from"node:util";import{pathToFileURL as we}from"node:url";import{realpathSync as ye}from"node:fs";import{z as $}from"zod";import{stripVTControlCharacters as ce}from"node:util";function b(e){return ce(e).replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g," ").trim()}function P(e){return e?new Date(e).toLocaleDateString(void 0,{month:"short",day:"numeric",year:"numeric"}):"Never"}function d(e,n=[]){return`
3
+ ${b(e)}
4
+ ${n.length?`
5
+ `+n.map(([o,c])=>` ${o.padEnd(10)}${b(c)}`).join(`
6
+ `)+`
7
+ `:""}`}function I(e){let n={"artifacts:read":"Read artifacts","artifacts:write":"Upload and edit","artifacts:delete":"Delete artifacts","tokens:manage":"Manage tokens"};return e.map(o=>n[o]).join(" \xB7 ")}function K(e,n,o){let c=e.profile==="default"?"":` --profile ${e.profile}`;return d(`${n?"\u2713 Already connected to":"\u2713 Connected to"} ${e.space.name}`,[["Profile",e.profile],["Service",e.endpoint],["Access",I(e.scopes)],["Expires",P(e.expiresAt)],["Stored in",e.storage==="keychain"?"OS credential vault":"Private credential file (0600)"],[o?"Agent uses":"Try next",`seshat upload ./report.html${c}${o?" --json":""}`]])}function R(e,n){return d(`${n} ${e.title}`,[["Link",e.url],["File",e.filename],["Size",`${new Intl.NumberFormat().format(e.size)} bytes`],["Expires",P(e.expiresAt)]])}function ee(e,n){return process.env.NO_COLOR!==void 0||process.env.TERM==="dumb"?e:e.replace(/^([✓✕].*)$/m,o=>`\x1B[1;${n}m${o}\x1B[0m`)}function _(e,n,o){process.stdout.write(o?(process.stdout.isTTY?ee(n,32):n)+`
8
+ `:JSON.stringify(e)+`
9
+ `)}function te(e,n){if(!n){process.stderr.write(JSON.stringify({error:{code:e.code,message:e.message},...e.requestId?{requestId:e.requestId}:{}})+`
10
+ `);return}let o=d(`\u2715 ${e.message}`,[["Code",e.code],...e.hint?[["Next step",e.hint]]:[],...e.requestId?[["Retry ID",e.requestId]]:[]]);process.stderr.write((process.stderr.isTTY?ee(o,31):o)+`
11
+ `)}import{randomBytes as pe,createHash as le}from"node:crypto";import{execFile as de}from"node:child_process";import{promisify as ue}from"node:util";import{hostname as fe}from"node:os";import{setTimeout as me}from"node:timers/promises";import{z as re}from"zod";var z=ue(de);async function B(e,n,o,c,h){let r=await fetch(e+n,{method:"POST",redirect:"error",headers:{"content-type":"application/json"},body:JSON.stringify(o),signal:h?AbortSignal.any([h,AbortSignal.timeout(15e3)]):AbortSignal.timeout(15e3)}),a=await r.json();if(!r.ok){let u=G.safeParse(a);throw new T(u.success?u.data.error.code:"connection_failed",u.success?u.data.error.message:"Could not connect to Seshat",r.status)}let f=c.safeParse(a);if(!f.success)throw new Error("Unexpected connection response from the service");return f.data}async function he(e){process.platform==="darwin"?await z("open",[e],{timeout:1e4}):process.platform==="win32"?await z("rundll32",["url.dll,FileProtocolHandler",e],{timeout:1e4}):await z("xdg-open",[e],{timeout:1e4})}async function ne(e){let n=x(e.profile),o=F.parse(e.endpoint??process.env.SESHAT_ENDPOINT??q),c=Y.parse(e.storage??"keychain");if(c==="file"&&process.platform==="win32")throw new Error("Use keychain storage with Windows Credential Manager");let h=re.array(O).min(1).parse((e.scopes??"artifacts:read,artifacts:write").split(",")),r=re.coerce.number().int().min(5).max(600).parse(e.timeout??600),a=await D(n),f;if(a){try{f=new A({endpoint:a.endpoint,token:await L(a)})}catch{}if(!e.force&&e.expiresAt===void 0&&e.name===void 0&&(e.storage===void 0||e.storage===a.credential.kind)&&f&&a.endpoint===o&&a.expiresAt>new Date().toISOString()&&[...new Set(h)].sort().join(",")===[...a.scopes].sort().join(","))try{return await H(n,a,f),await f.whoami(),{connected:!0,alreadyConnected:!0,...N(n,a)}}catch(p){if(!(p instanceof T&&p.status===401))throw p}}let u="seshat_"+pe(32).toString("base64url"),S=le("sha256").update(u).digest("hex"),s=await B(o,"/v1/auth/device",{name:e.name??`${fe().slice(0,60)} \xB7 ${n.slice(0,32)}`,scopes:h,tokenHash:S,...e.expiresAt?{expiresAt:e.expiresAt}:{}},X);if(!URL.canParse(s.verificationUrl))throw new Error("The service returned an invalid authorization URL");let k=new URL(s.verificationUrl);if(k.origin!==o||k.pathname!=="/connect"||k.username||k.password||k.hash||k.searchParams.get("code")!==s.userCode)throw new Error("The service returned an unexpected authorization URL");process.stderr.write(e.human?d("Connect this terminal to Seshat",[["Open",s.verificationUrl],["Code",s.userCode]])+`
12
+ Check the code in your browser, then approve this device.
13
+ Waiting for approval\u2026
14
+
15
+ `:`Approve this terminal in Seshat:
3
16
  ${s.verificationUrl}
4
17
  Code: ${s.userCode}
5
- `),t.noBrowser||await re(s.verificationUrl).catch(()=>process.stderr.write(`Open the URL above in your browser to continue.
6
- `));let w=new AbortController,c=()=>w.abort();process.once("SIGINT",c);let o=Math.min(Date.parse(s.expiresAt),Date.now()+h*1e3),g=!1;try{let p=s.interval;for(;Date.now()<o&&(await te(Math.min(p*1e3,Math.max(0,o-Date.now())),void 0,{signal:w.signal}),!(Date.now()>=o));){let f;try{f=await F(e,"/v1/auth/device/poll",{deviceCode:s.deviceCode},D,w.signal)}catch(C){if(C instanceof y&&C.code==="slow_down"){p+=2;continue}throw C}if(f.status==="denied")throw new Error("Connection was denied. No credential was saved");if(f.status==="pending"){p=f.interval;continue}let k=await new x({endpoint:e,token:u}).whoami({signal:w.signal});if(k.spaceId!==f.space.id||k.tokenId!==f.id||[...new Set(l)].sort().join(",")!==[...k.scopes].sort().join(","))throw new Error("The approved credential does not match the requested space and permissions");w.signal.throwIfAborted();let T=await J({name:r,endpoint:e,tokenId:f.id,token:u,space:f.space,scopes:f.scopes,expiresAt:f.expiresAt,storage:n,pendingConnection:!0,expectedTokenId:i?.tokenId??null,signal:w.signal});g=!0;try{await U(r,T,new x({endpoint:e,token:u}))}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 a&&await a.revokeSelf().catch(()=>process.stderr.write(`The previous credential could not be revoked. Revoke it in Seshat Agent access.
7
- `)),{connected:!0,...I(r,T)}}throw new Error("Connection approval timed out. Run seshat login again")}finally{process.removeListener("SIGINT",c),g||await F(e,"/v1/auth/device/cancel",{deviceCode:s.deviceCode},D).catch(()=>{})}}async function W(t){let r=v(t),e=await A(r);if(!e)return{disconnected:!0,alreadyDisconnected:!0,profile:r};try{await new x({endpoint:e.endpoint,token:await P(e)}).revokeSelf()}catch(n){if(!(n instanceof y&&n.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 K(r,e.tokenId),{disconnected:!0,profile:r}}async function X(t={}){let r=v(t.profile);if(process.env.SESHAT_TOKEN&&!t.profile&&!process.env.SESHAT_PROFILE){let l=await(await S(t)).whoami();return{connected:!0,source:"environment",endpoint:_.parse(t.endpoint??process.env.SESHAT_ENDPOINT??N),...l}}let e=await A(r);if(!e)return{connected:!1,profile:r};let n=await(await S(t)).whoami();return{connected:!0,...I(r,e),...n}}var L={root:`Seshat: publish and manage agent artifacts. All results are JSON.
18
+ `),e.noBrowser||await he(s.verificationUrl).catch(()=>process.stderr.write(`Open the URL above in your browser to continue.
19
+ `));let y=new AbortController,m=()=>y.abort();process.once("SIGINT",m);let v=Math.min(Date.parse(s.expiresAt),Date.now()+r*1e3),E=!1;try{let p=s.interval;for(;Date.now()<v&&(await me(Math.min(p*1e3,Math.max(0,v-Date.now())),void 0,{signal:y.signal}),!(Date.now()>=v));){let i;try{i=await B(o,"/v1/auth/device/poll",{deviceCode:s.deviceCode},j,y.signal)}catch(g){if(g instanceof T&&g.code==="slow_down"){p+=2;continue}throw g}if(i.status==="denied")throw new Error("Connection was denied. No credential was saved");if(i.status==="pending"){p=i.interval;continue}let t=await new A({endpoint:o,token:u}).whoami({signal:y.signal});if(t.spaceId!==i.space.id||t.tokenId!==i.id||[...new Set(h)].sort().join(",")!==[...t.scopes].sort().join(","))throw new Error("The approved credential does not match the requested space and permissions");y.signal.throwIfAborted();let l=await Z({name:n,endpoint:o,tokenId:i.id,token:u,space:i.space,scopes:i.scopes,expiresAt:i.expiresAt,storage:c,pendingConnection:!0,expectedTokenId:a?.tokenId??null,signal:y.signal});E=!0;try{await H(n,l,new A({endpoint:o,token:u}))}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 f&&await f.revokeSelf().catch(()=>process.stderr.write(`The previous credential could not be revoked. Revoke it in Seshat Agent access.
20
+ `)),{connected:!0,...N(n,l)}}throw new Error("Connection approval timed out. Run seshat login again")}finally{process.removeListener("SIGINT",m),E||await B(o,"/v1/auth/device/cancel",{deviceCode:s.deviceCode},j).catch(()=>{})}}async function oe(e){let n=x(e),o=await D(n);if(!o)return{disconnected:!0,alreadyDisconnected:!0,profile:n};try{await new A({endpoint:o.endpoint,token:await L(o)}).revokeSelf()}catch(c){if(!(c instanceof T&&c.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 V(n,o.tokenId),{disconnected:!0,profile:n}}async function se(e={}){let n=x(e.profile);if(process.env.SESHAT_TOKEN&&!e.profile&&!process.env.SESHAT_PROFILE){let h=await(await C(e)).whoami();return{connected:!0,source:"environment",endpoint:F.parse(e.endpoint??process.env.SESHAT_ENDPOINT??q),...h}}let o=await D(n);if(!o)return{connected:!1,profile:n};let c=await(await C(e)).whoami();return{connected:!0,...N(n,o),...c}}var M={root:`Seshat: publish and manage agent artifacts.
21
+
22
+ Commands default to human-readable output. Use --json for structured results and errors.
8
23
 
9
24
  Commands:
10
25
  login Connect this terminal through your browser
@@ -20,7 +35,7 @@ Commands:
20
35
  auth whoami
21
36
  mcp
22
37
 
23
- The default service is https://seshat.djamil.dev. Run seshat login once.
38
+ The default service is https://app.seshat.sh. Run seshat login once.
24
39
  Use --profile NAME for separate user/agent connections. CI may set SESHAT_ENDPOINT and SESHAT_TOKEN.
25
40
  Tokens are never accepted in command-line flags.
26
41
  Run a command with --help for examples.
@@ -53,7 +68,7 @@ Set SESHAT_TOKEN to the deployment's administrator credential. This does not sen
53
68
  Example: seshat spaces create --slug djamil --name Djamil`,"auth login":`Connect a terminal through browser approval. A Seshat invitation is required.
54
69
  Options: --profile (default default), --endpoint, --name, --scopes (default artifacts:read,artifacts:write), --expires-at (default 90 days), --storage keychain|file, --no-browser, --timeout (5-600 seconds), --force.
55
70
  OS credential storage is the default. On headless POSIX machines, --storage file explicitly selects an owner-only file.
56
- The approval URL and code go to stderr. Success is secret-free JSON on stdout.
71
+ The approval URL and code go to stderr. Success shows your space, profile, expiry and storage. Use --json for secret-free structured output.
57
72
  Examples:
58
73
  seshat login
59
74
  seshat login --no-browser --profile remote --storage file
@@ -66,9 +81,14 @@ Examples:
66
81
  seshat agent setup --profile t3 --name "T3 agent"
67
82
  seshat agent setup --profile remote --storage file --no-browser`,"auth whoami":`Inspect the active token's space and scopes.
68
83
  Example: seshat auth whoami`,mcp:`Start the local stdio MCP server. SESHAT_UPLOAD_ROOTS is an optional JSON array of allowed directories.
69
- Example: seshat mcp`},ne={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"}},ie={"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 m(t,r){let e=R.string().min(1).safeParse(t);if(!e.success)throw new Error(`Provide ${r}. Run this command with --help for an example`);return e.data}async function ce(){let t=[],r=0;for await(let e of process.stdin){let n=Buffer.from(e);if(r+=n.length,r>O)throw new Error(`stdin exceeds ${O} bytes`);t.push(n)}return new Uint8Array(Buffer.concat(t))}async function pe(t){let r;try{let{values:e,positionals:n}=ae({args:t,options:ne,allowPositionals:!0}),l={login:"auth login",logout:"auth logout",whoami:"auth whoami",upload:"artifacts upload",list:"artifacts list"},h=n[0]??"",i=Object.hasOwn(l,h)?l[h]:void 0,a=i??(h==="mcp"?"mcp":n.slice(0,2).join(" "));if(e.help||!a){process.stdout.write((L[a]??L.root)+`
70
- `);return}if(!L[a])throw new Error("Unknown command. Run seshat --help");let u=["artifacts upload","artifacts get","artifacts update","artifacts delete","tokens revoke"].includes(a);if(n.length>(a==="mcp"?1:(i?1:2)+(u?1:0)))throw new Error("Unexpected positional argument. Run this command with --help");let E=Object.keys(e).find(o=>!["help","json","profile","endpoint",...ie[a]??[]].includes(o));if(E)throw new Error(`--${E} is not supported by ${a}. Run this command with --help`);if(a==="mcp"){let{startMcp:o}=await import("./mcp.js");await o({profile:e.profile,endpoint:e.endpoint});return}if(e["no-expiry"]&&e["expires-at"])throw new Error("Choose --no-expiry or --expires-at");if(a==="auth login"||a==="agent setup"){let o=a==="agent setup"?v(e.profile??"agent"):v(e.profile),g=await G({profile:o,endpoint:e.endpoint,name:e.name,scopes:e.scopes??(a==="agent setup"?"artifacts:write":void 0),expiresAt:e["expires-at"],storage:e.storage,noBrowser:e["no-browser"],timeout:e.timeout,force:e.force}),p=a==="agent setup"?{...g,agent:{environment:{SESHAT_PROFILE:o},uploadCommand:`pnpx @seshat.sh/cli upload FILE --profile ${o}`,helpCommand:"pnpx @seshat.sh/cli upload --help"}}:g;process.stdout.write(JSON.stringify(p)+`
71
- `);return}if(a==="auth logout"||a==="auth status"){let o=a==="auth logout"?await W(e.profile):await X({profile:e.profile,endpoint:e.endpoint});process.stdout.write(JSON.stringify(o)+`
72
- `);return}let s=await S({profile:e.profile,endpoint:e.endpoint}),d=n[i?1:2],w=e["no-expiry"]?null:e["expires-at"],c;switch(a){case"auth whoami":c=await s.whoami();break;case"spaces create":c=await s.createSpace({slug:m(e.slug,"--slug"),name:m(e.name,"--name")});break;case"tokens create":c=await s.createToken({name:m(e.name,"--name"),scopes:R.array(b).parse((e.scopes??"artifacts:write").split(",")),expiresAt:e["expires-at"]});break;case"tokens revoke":{let o=m(d,"TOKEN_ID");if(e["dry-run"]){c={dryRun:!0,action:"revoke_token",id:o};break}if(!e.yes)throw new Error("Revocation requires --yes. Inspect with seshat tokens revoke TOKEN_ID --dry-run");c=await s.revokeToken(o);break}case"artifacts list":c=await s.list({limit:e.limit===void 0?void 0:R.coerce.number().int().min(1).max(100).parse(e.limit),cursor:e.cursor});break;case"artifacts get":c=await s.get(m(d,"ARTIFACT_ID"));break;case"artifacts update":c=await s.update(m(d,"ARTIFACT_ID"),{title:e.title,description:e.description,expiresAt:w});break;case"artifacts delete":{let o=m(d,"ARTIFACT_ID");if(e["dry-run"]){c={dryRun:!0,action:"delete_artifact",artifact:await s.get(o)};break}if(!e.yes)throw new Error("Deletion requires --yes. Inspect with seshat artifacts delete ARTIFACT_ID --dry-run");c=await s.delete(o);break}case"artifacts upload":{let o=m(d,"FILE or - for stdin"),g=e.title;r=j.parse(e["request-id"]??crypto.randomUUID());let p;if(o==="-"){let k=m(e.filename,"--filename for stdin"),T=m(e["content-type"],"--content-type for stdin");p={bytes:await ce(),filename:k,contentType:T}}else p=await M(o);c={...await s.upload({...p,contentType:e["content-type"]??p.contentType,metadata:{title:g,description:e.description??"",expiresAt:w},requestId:r}),requestId:r};break}}process.stdout.write(JSON.stringify(c)+`
73
- `)}catch(e){let n=e instanceof y?e.code:"invalid_command",l=e instanceof R.ZodError?"Invalid command options. Run this command with --help. Example: seshat login --profile t3 --storage keychain":e instanceof Error?e.message:"Command failed";process.stderr.write(JSON.stringify({error:{code:n,message:l},...r?{requestId:r}:{}})+`
74
- `),process.exitCode=1}}process.argv[1]&&import.meta.url===oe(se(process.argv[1])).href&&await pe(process.argv.slice(2));export{pe as runCli};
84
+ Example: seshat mcp`},ve={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"}},ke={"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 w(e,n){let o=$.string().min(1).safeParse(e);if(!o.success)throw new Error(`Provide ${n}. Run this command with --help for an example`);return o.data}async function xe(){let e=[],n=0;for await(let o of process.stdin){let c=Buffer.from(o);if(n+=c.length,n>U)throw new Error(`stdin exceeds ${U} bytes`);e.push(c)}return new Uint8Array(Buffer.concat(e))}async function Se(e){let n,o=!e.includes("--json"),c="default",h="seshat --help";try{let{values:r,positionals:a}=ge({args:e,options:ve,allowPositionals:!0}),f={login:"auth login",logout:"auth logout",whoami:"auth whoami",upload:"artifacts upload",list:"artifacts list"},u=a[0]??"",S=Object.hasOwn(f,u)?f[u]:void 0,s=S??(u==="mcp"?"mcp":a.slice(0,2).join(" "));if(r.help||!s){process.stdout.write((M[s]??M.root)+(s?`
85
+
86
+ Output: human-readable by default. Use --json for structured results and errors.`:"")+`
87
+ `);return}if(!M[s])throw new Error("Unknown command. Run seshat --help");h=`seshat ${s} --help`,c=x(r.profile??(s==="agent setup"?"agent":void 0));let k=["artifacts upload","artifacts get","artifacts update","artifacts delete","tokens revoke"].includes(s);if(a.length>(s==="mcp"?1:(S?1:2)+(k?1:0)))throw new Error("Unexpected positional argument. Run this command with --help");let y=Object.keys(r).find(t=>!["help","json","profile","endpoint",...ke[s]??[]].includes(t));if(y)throw new Error(`--${y} is not supported by ${s}. Run this command with --help`);if(s==="mcp"){let{startMcp:t}=await import("./mcp.js");await t({profile:r.profile,endpoint:r.endpoint});return}if(r["no-expiry"]&&r["expires-at"])throw new Error("Choose --no-expiry or --expires-at");if(s==="auth login"||s==="agent setup"){let t=s==="agent setup"?x(r.profile??"agent"):x(r.profile),l=await ne({profile:t,endpoint:r.endpoint,name:r.name,scopes:r.scopes??(s==="agent setup"?"artifacts:write":void 0),expiresAt:r["expires-at"],storage:r.storage,noBrowser:r["no-browser"],timeout:r.timeout,force:r.force,human:o}),g=s==="agent setup"?{...l,agent:{environment:{SESHAT_PROFILE:t},uploadCommand:`pnpx @seshat.sh/cli upload FILE --profile ${t} --json`,helpCommand:"pnpx @seshat.sh/cli upload --help"}}:l;_(g,K(l,"alreadyConnected"in l&&!!l.alreadyConnected,s==="agent setup"),o);return}if(s==="auth logout"||s==="auth status"){let t=s==="auth logout"?await oe(r.profile):await se({profile:r.profile,endpoint:r.endpoint}),l="disconnected"in t?d(t.alreadyDisconnected?"\u2713 No saved connection":"\u2713 Device disconnected",[["Profile",t.profile]]):t.connected?"storage"in t?K(t,!0,!1):d("\u2713 Connected with SESHAT_TOKEN",[["Service",t.endpoint],["Space",t.space?.name??t.spaceId],["Access",I(t.scopes)]]):d("No connected device",[["Profile",t.profile],["Next step",`seshat login --profile ${t.profile}`]]);_(t,l,o);return}let m=await C({profile:r.profile,endpoint:r.endpoint}),v=a[S?1:2],E=r["no-expiry"]?null:r["expires-at"],p,i="";switch(s){case"auth whoami":{let t=await m.whoami();p={spaceId:t.spaceId,tokenId:t.tokenId,scopes:t.scopes},i=d("\u2713 Connected to Seshat",[["Space",t.space?.name??t.spaceId],["Access",I(t.scopes)]]);break}case"spaces create":{let t=await m.createSpace({slug:w(r.slug,"--slug"),name:w(r.name,"--name")});p=t,i=d(`\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 m.createToken({name:w(r.name,"--name"),scopes:$.array(O).parse((r.scopes??"artifacts:write").split(",")),expiresAt:r["expires-at"]});p=t,i=d("\u2713 Token created",[["Token",t.token],["Access",I(t.scopes)],["Expires",P(t.expiresAt)],["Keep safe","Copy this token now. It will not be shown again."]]);break}case"tokens revoke":{let t=w(v,"TOKEN_ID");if(r["dry-run"]){p={dryRun:!0,action:"revoke_token",id:t},i=d("Preview: revoke token",[["Token ID",t],["To apply",`seshat tokens revoke ${t} --yes`]]);break}if(!r.yes)throw new Error("Revocation requires --yes. Inspect with seshat tokens revoke TOKEN_ID --dry-run");p=await m.revokeToken(t),i=d("\u2713 Token revoked",[["Token ID",t]]);break}case"artifacts list":{let t=await m.list({limit:r.limit===void 0?void 0:$.coerce.number().int().min(1).max(100).parse(r.limit),cursor:r.cursor});p=t,i=d(t.artifacts.length?`${t.artifacts.length} artifacts`:"No artifacts yet")+t.artifacts.map(l=>`
88
+ ${b(l.title)}
89
+ ${b(l.url)}
90
+ `).join("")+(t.nextCursor?`
91
+ Next page: seshat list --cursor ${b(t.nextCursor)}
92
+ `:"");break}case"artifacts get":{let t=await m.get(w(v,"ARTIFACT_ID"));p=t,i=R(t,"Artifact:");break}case"artifacts update":{let t=await m.update(w(v,"ARTIFACT_ID"),{title:r.title,description:r.description,expiresAt:E});p=t,i=R(t,"\u2713 Updated");break}case"artifacts delete":{let t=w(v,"ARTIFACT_ID");if(r["dry-run"]){let l=await m.get(t);p={dryRun:!0,action:"delete_artifact",artifact:l},i=R(l,"Preview: delete")+`
93
+ To apply: seshat artifacts delete ${t} --yes
94
+ `;break}if(!r.yes)throw new Error("Deletion requires --yes. Inspect with seshat artifacts delete ARTIFACT_ID --dry-run");p=await m.delete(t),i=d("\u2713 Artifact deleted",[["ID",t]]);break}case"artifacts upload":{let t=w(v,"FILE or - for stdin"),l=r.title;n=W.parse(r["request-id"]??crypto.randomUUID());let g;if(t==="-"){let ae=w(r.filename,"--filename for stdin"),ie=w(r["content-type"],"--content-type for stdin");g={bytes:await xe(),filename:ae,contentType:ie}}else g=await Q(t);let J=await m.upload({...g,contentType:r["content-type"]??g.contentType,metadata:{title:l,description:r.description??"",expiresAt:E},requestId:n});p={...J,requestId:n},i=R(J,"\u2713 Uploaded");break}}_(p,i,o)}catch(r){let a=r instanceof T?r.code:"invalid_command",f=r instanceof $.ZodError?`Invalid options: ${r.issues.map(S=>S.message).join("; ")}. Run ${h}`:r instanceof Error?r.message:"Command failed",u=a==="unauthorized"?`Reconnect with seshat login --profile ${c}, or check SESHAT_TOKEN if you use it.`:a==="insufficient_scope"?`Reconnect with the required --scopes using seshat login --profile ${c}.`:a==="connection_failed"?"Check your connection and service endpoint. Retry uploads with the same --request-id.":void 0;te({code:a,message:f,requestId:n,hint:u},o),process.exitCode=1}}process.argv[1]&&import.meta.url===we(ye(process.argv[1])).href&&await Se(process.argv.slice(2));export{Se as runCli};
package/dist/mcp.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{c as s,d as l,e as o,f as c,u,w as p}from"./chunk-ZDGADCDP.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{c as s,d as l,e as o,f as c,u,w as p}from"./chunk-W3JWSMKV.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.
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.
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@seshat.sh/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Publish and share artifacts with Seshat from terminals and agents.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
7
- "homepage": "https://seshat.djamil.dev",
7
+ "homepage": "https://seshat.sh",
8
8
  "engines": {
9
9
  "node": ">=22"
10
10
  },
@@ -1,3 +0,0 @@
1
- import{z as r}from"zod";import{z as Q}from"zod";var A=Q.enum(["never","hour","day","week","month","year","custom"]);var y=10*1024*1024,U=["artifacts:read","artifacts:write","artifacts:delete","tokens:manage"],b=r.enum(U),l=r.string().regex(/^[a-f0-9]{48}$/),D=r.string().min(8).max(128).regex(/^[A-Za-z0-9_-]+$/),f=r.string().regex(/^seshat_[A-Za-z0-9_-]{43}$/),z=r.iso.datetime().transform(e=>new Date(e).toISOString()),u=r.object({title:r.string().trim().min(1).max(200).optional(),description:r.string().max(2e3).default(""),expiresAt:z.nullable().optional(),expirationPreset:A.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"),R=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(y)}).strict(),F=r.object({title:u.shape.title.optional(),description:r.string().max(2e3).optional(),expiresAt:u.shape.expiresAt,expirationPreset:u.shape.expirationPreset,password:u.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"),N=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(),h=r.object({name:r.string().trim().min(1).max(100),scopes:r.array(b).min(1).max(U.length),expiresAt:z.optional()}).strict(),w=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:A.nullable().default(null),url:r.url(),downloadUrl:r.url(),passwordProtected:r.boolean().default(!1),views:r.object({owner:r.number(),others:r.number()})}),_=r.object({artifacts:r.array(w),nextCursor:r.string().nullable()}),E=r.object({id:l,token:f,scopes:r.array(b),expiresAt:r.string().nullable()}),L=r.object({space:r.object({id:l,slug:r.string(),name:r.string()}),credential:E}),$=r.object({error:r.object({code:r.string(),message:r.string()})});import{z as m}from"zod";import{z as i}from"zod";var q="https://seshat.djamil.dev",S=i.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),v=i.string().min(1).max(64).regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/),Te=i.string().max(16).transform(e=>e.toUpperCase().replaceAll("-","").trim()).pipe(i.string().regex(/^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{8}$/)),Ae=h.extend({tokenHash:i.string().regex(/^[a-f0-9]{64}$/)}).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"),Ee=i.strictObject({deviceCode:l}),Pe=i.strictObject({approve:i.boolean()}),k=i.object({id:l,slug:i.string(),name:i.string()}),Oe=i.object({deviceCode:l,userCode:i.string().regex(/^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{4}-[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{4}$/),verificationUrl:i.url(),expiresAt:i.iso.datetime(),interval:i.number().int().min(1).max(60)}),je=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:l,space:k,scopes:h.shape.scopes,expiresAt:i.iso.datetime()})]),Ce=i.object({userCode:i.string(),name:i.string(),scopes:h.shape.scopes,status:i.enum(["pending","approved","denied"]),expiresAt:i.iso.datetime(),tokenExpiresAt:i.iso.datetime(),space:k});var V=m.object({endpoint:S,token:f}).strict(),T=class extends Error{constructor(n,o,a){super(o);this.code=n;this.status=a}code;status},x=class{settings;constructor(t){this.settings=V.parse(t)}async request(t,n,o={}){let a=new Headers(o.headers);a.set("authorization",`Bearer ${this.settings.token}`),typeof o.body=="string"&&a.set("content-type","application/json");let s;try{s=await fetch(new URL(t,this.settings.endpoint),{...o,headers:a,redirect:"error",signal:o.signal?AbortSignal.any([o.signal,AbortSignal.timeout(6e4)]):AbortSignal.timeout(6e4)})}catch{throw new T("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 d=$.safeParse(c);throw new T(d.success?d.data.error.code:"request_failed",d.success?d.data.error.message:`HTTP ${s.status}`,s.status)}return n.parse(c)}async upload(t){let n=R.parse({filename:t.filename,contentType:t.contentType,size:t.bytes.byteLength}),o=u.parse(t.metadata),a=D.parse(t.requestId),s=new FormData;return s.set("metadata",JSON.stringify(o)),s.set("file",new Blob([new Uint8Array(t.bytes)],{type:n.contentType}),n.filename),this.request("/v1/artifacts",w,{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}`,_)}get(t){return this.request(`/v1/artifacts/${l.parse(t)}`,w)}update(t,n){return this.request(`/v1/artifacts/${l.parse(t)}`,w,{method:"PATCH",body:JSON.stringify(F.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.object({spaceId:l,tokenId:l,scopes:h.shape.scopes}),{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",L,{method:"POST",body:JSON.stringify(N.parse(t))})}createToken(t){return this.request("/v1/tokens",E,{method:"POST",body:JSON.stringify(h.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 M}from"node:fs";import{lstat as ee,mkdir as te,open as P,rename as re,unlink as H}from"node:fs/promises";import{homedir as ne}from"node:os";import{join as g,resolve as oe}from"node:path";import{randomUUID as J}from"node:crypto";import{setTimeout as ie}from"node:timers/promises";import{z as p}from"zod";var Be=p.enum(["keychain","file"]),Y=p.strictObject({endpoint:S,tokenId:l,space:k,scopes:p.array(b),expiresAt:p.iso.datetime(),pendingConnection:p.literal(!0).optional(),credential:p.discriminatedUnion("kind",[p.strictObject({kind:p.literal("keychain"),account:p.string().min(1)}),p.strictObject({kind:p.literal("file"),token:f})])}),ae=p.strictObject({version:p.literal(1),profiles:p.record(v,Y)});function B(){return oe(process.env.SESHAT_CONFIG_DIR??g(process.env.XDG_CONFIG_HOME??g(ne(),".config"),"seshat"))}function X(e){return e instanceof Error&&"code"in e&&e.code==="ENOENT"}async function Z(e,t=!1){let n=await ee(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 W(){let e=B();try{await Z(e,!0);let t=g(e,"profiles.json"),n=await P(t,M.O_RDONLY|(M.O_NOFOLLOW??0));try{let o=await n.stat();if(!o.isFile()||o.size>65536||process.platform!=="win32"&&((o.mode&63)!==0||process.getuid&&o.uid!==process.getuid()))throw new Error("Credential file must be a small, owner-only regular file");try{let a=ae.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(X(t))return{version:1,profiles:{}};throw t}}async function j(e,t){let n=B();await te(n,{recursive:!0,mode:448}),await Z(n,!0);let o=g(n,"profiles.lock"),a;for(let c=0;c<40;c++)try{a=await P(o,"wx",384);break}catch(d){if(!(d instanceof Error&&"code"in d&&d.code==="EEXIST"))throw d;await ie(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=g(n,`profiles-${J()}.tmp`);try{t?.throwIfAborted();let c=e(await W()),d=await P(s,"wx",384);try{let I=JSON.stringify(c)+`
2
- `;if(Buffer.byteLength(I)>65536)throw new Error("Too many stored profiles; remove unused connections before adding another");await d.writeFile(I),await d.sync()}finally{await d.close()}t?.throwIfAborted(),await re(s,g(n,"profiles.json"))}finally{try{await H(s).catch(c=>{if(!X(c))throw c})}finally{await a.close(),await H(o)}}}async function C(e){let{AsyncEntry:t}=await import("@napi-rs/keyring");return new t("dev.djamil.seshat.cli",e,{linux:{store:"secret-service"}})}async function O(e){e.credential.kind==="keychain"&&await(await C(e.credential.account)).deletePassword(AbortSignal.timeout(15e3))}async function Xe(e){let t=v.parse(e.name),n=f.parse(e.token);if(e.storage==="file"&&process.platform==="win32")throw new Error("Use keychain storage with Windows Credential Manager");let o=J();if(e.storage==="keychain")try{await(await C(o)).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=Y.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:o}:{kind:"file",token:n}}),s;try{await j(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 O(a).catch(()=>{}),c}return s&&await O(s).catch(()=>{process.stderr.write(`The previous OS credential entry could not be removed. Revoke its token in Seshat Agent access.
3
- `)}),a}function se(e){return v.parse(e??process.env.SESHAT_PROFILE??"default")}async function G(e){let t=await W();return Object.hasOwn(t.profiles,e)?t.profiles[e]:void 0}async function ce(e){if(e.credential.kind==="file")return e.credential.token;try{return f.parse(await(await C(e.credential.account)).getPassword(AbortSignal.timeout(15e3)))}catch{throw new Error("Stored credential is unavailable. Run seshat login again for this profile")}}async function Ze(e,t){let n=await G(e);if(n){if(n.tokenId!==t)throw new Error("This profile changed during logout. The newer connection has been preserved");await O(n),await j(o=>{let a=Object.hasOwn(o.profiles,e)?o.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={...o.profiles};return delete s[e],{...o,profiles:s}})}}async function le(e,t,n){t.pendingConnection&&(await n.completeConnection(),await j(o=>{let a=Object.hasOwn(o.profiles,e)?o.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{...o,profiles:{...o.profiles,[e]:c}}}))}async function We(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??q,token:process.env.SESHAT_TOKEN});let n=se(e.profile),o=await G(n);if(!o)throw new Error(`No credential for profile "${n}". Run seshat login --profile ${n}`);if(t&&S.parse(t)!==o.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:o.endpoint,token:await ce(o)});return await le(n,o,a),a}function Ge(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 K,stat as pe,readFile as de}from"node:fs/promises";import{basename as me,extname as fe,relative as he,isAbsolute as ue}from"node:path";var ge={".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 tt(e,t){let n=await K(e);if(t&&!(await Promise.all(t.map(s=>K(s)))).some(s=>{let c=he(s,n);return c!==".."&&!c.startsWith("../")&&!ue(c)}))throw new Error("File is outside SESHAT_UPLOAD_ROOTS. Configure explicit directories for MCP file access");let o=await pe(n);if(!o.isFile())throw new Error("Upload path must be a regular file");if(o.size<1||o.size>y)throw new Error(`File must contain 1 to ${y} bytes`);return{bytes:new Uint8Array(await de(n)),filename:me(n),contentType:ge[fe(n).toLowerCase()]??"application/octet-stream"}}export{y as a,b,l as c,D as d,u as e,F as f,$ as g,q as h,S as i,Oe as j,je as k,T as l,x as m,Be as n,Xe as o,se as p,G as q,ce as r,Ze as s,le as t,We as u,Ge as v,tt as w};