outis-chat 0.1.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.
Files changed (2) hide show
  1. package/dist-pkg/outis.js +28 -0
  2. package/package.json +46 -0
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import{render as co}from"ink";import{createElement as lo}from"react";var Ie="https://outis.houseofky.xyz";import{EventEmitter as Lt}from"events";import{z as R}from"zod";var T=R.string().regex(/^[A-Za-z0-9_-]{43}$/,"expected a base64url ed25519 public key").brand(),D=R.uuid().brand(),b=R.uuid().brand(),F=R.string().regex(/^[0-9A-HJKMNP-TV-Z]{26}$/,"expected a ULID").brand(),E=R.number().int().nonnegative(),V=R.string().trim().min(1,"pick something").max(32,"keep it under 32 characters").refine(o=>!/[\p{Cc}\p{Cf}]/u.test(o),"that character is not allowed"),ke=R.string().regex(/^[a-z]+-[a-z]+-\d{4}$/,"join codes look like plum-otter-4417").brand();var _e=R.enum(["dm","group"]),ee=R.enum(["full","from_join"]),Te=R.enum(["owner","member"]);function M(o){return o.slice(0,6).toLowerCase()}import{z as u}from"zod";var Ee=u.object({id:D,pubkey:T,displayName:V,createdAt:u.iso.datetime()}),Rt=u.object({identityId:D,pubkey:T,displayName:V,role:Te,joinedAt:u.iso.datetime()}),$=u.object({roomId:b,seq:E,msgId:F,senderId:D,body:u.string().min(1).max(4e3),createdAt:u.iso.datetime()}),St=u.object({id:b,kind:_e,name:u.string().max(64).nullable(),joinCode:ke,historyMode:ee,lastSeq:E,createdAt:u.iso.datetime()}),Me=St.extend({members:u.array(Rt),lastMessage:$.nullable(),lastReadSeq:E,unreadCount:u.number().int().nonnegative()}),wt=u.enum(["pending","completed","declined","expired","invalidated"]),It=u.object({identityId:D,approved:u.boolean(),votedAt:u.iso.datetime()}),te=u.object({id:u.uuid(),roomId:b,requestedBy:D,memberSnapshot:u.array(D),status:wt,votes:u.array(It),expiresAt:u.iso.datetime(),createdAt:u.iso.datetime()});import{z as ye}from"zod";var kt=ye.enum(["challenge_expired","bad_signature","session_expired","key_revoked","client_outdated","room_not_found","not_a_member","already_a_member","join_code_invalid","room_deleted","message_too_long","rate_limited","deletion_already_pending","deletion_not_pending","deletion_expired","not_in_snapshot","bad_request","internal"]),Pe=ye.object({code:kt,message:ye.string()}),z=class extends Error{code;constructor(e,t){super(t),this.code=e,this.name="OutisError"}toWire(){return{code:this.code,message:this.message}}};import{z as m}from"zod";var Do=m.object({pubkey:T}),Co=m.object({nonce:m.string().min(32),expiresAt:m.iso.datetime()}),jo=m.object({pubkey:T,nonce:m.string(),signature:m.string(),issuedAt:m.iso.datetime(),displayName:V.optional()}),Bo=m.object({token:m.string(),expiresAt:m.iso.datetime(),identity:Ee,created:m.boolean()}),Oo=m.object({newPubkey:T,signature:m.string()}),Ao=m.object({displayName:V}),No=m.object({rooms:m.array(Me)}),Ko=m.object({name:m.string().trim().min(1).max(64),historyMode:ee.default("full")}),Uo=m.object({pubkey:T}),Lo=m.object({code:m.string().min(4)}),Vo=m.object({sinceSeq:m.coerce.number().int().nonnegative().default(0),limit:m.coerce.number().int().min(1).max(500).default(200)}),$o=m.object({messages:m.array($),hasMore:m.boolean()}),zo=m.object({requestId:m.uuid(),approved:m.boolean()}),Ho=m.object({request:te}),S={challenge:()=>"/auth/challenge",verify:()=>"/auth/verify",rotateKey:()=>"/identity/rotate",updateIdentity:()=>"/identity",listRooms:()=>"/rooms",createRoom:()=>"/rooms",openDm:()=>"/rooms/dm",joinRoom:()=>"/rooms/join",messages:o=>`/rooms/${o}/messages`,requestDeletion:o=>`/rooms/${o}/deletion`,voteDeletion:o=>`/rooms/${o}/deletion/vote`};import{z as a}from"zod";var Zo=a.discriminatedUnion("type",[a.object({type:a.literal("ping")}),a.object({type:a.literal("room.subscribe"),roomId:b}),a.object({type:a.literal("room.unsubscribe"),roomId:b}),a.object({type:a.literal("message.send"),roomId:b,msgId:F,body:a.string().min(1).max(4e3)}),a.object({type:a.literal("message.sync"),roomId:b,sinceSeq:E}),a.object({type:a.literal("read.mark"),roomId:b,seq:E})]),_t=a.discriminatedUnion("type",[a.object({type:a.literal("pong")}),a.object({type:a.literal("message.new"),message:$}),a.object({type:a.literal("message.ack"),roomId:b,msgId:F,seq:E}),a.object({type:a.literal("message.sync.result"),roomId:b,messages:a.array($),hasMore:a.boolean()}),a.object({type:a.literal("deletion.updated"),request:te}),a.object({type:a.literal("room.deleted"),roomId:b}),a.object({type:a.literal("room.membersChanged"),roomId:b}),a.object({type:a.literal("error"),error:Pe})]);function qe(o){return _t.safeParse(o)}var De="x-outis-client-version";import{ulid as Vt}from"ulid";import{mkdirSync as Tt}from"fs";import{dirname as Et}from"path";import Mt from"better-sqlite3";var oe=class{db;constructor(e){Tt(Et(e),{recursive:!0,mode:448}),this.db=new Mt(e),this.db.pragma("journal_mode = WAL"),this.db.pragma("foreign_keys = ON"),this.migrate()}migrate(){this.db.exec(`
3
+ create table if not exists rooms (
4
+ id text primary key,
5
+ summary_json text not null,
6
+ updated_at integer not null
7
+ );
8
+ create table if not exists messages (
9
+ room_id text not null,
10
+ seq integer not null,
11
+ msg_id text not null,
12
+ sender_id text not null,
13
+ body text not null,
14
+ created_at text not null,
15
+ primary key (room_id, seq)
16
+ );
17
+ create unique index if not exists messages_msgid on messages (room_id, msg_id);
18
+
19
+ -- messages queued while offline, flushed in order on reconnect
20
+ create table if not exists outbox (
21
+ msg_id text primary key,
22
+ room_id text not null,
23
+ body text not null,
24
+ queued_at integer not null
25
+ );
26
+
27
+ create table if not exists meta (key text primary key, value text not null);
28
+ `)}upsertRoom(e){this.db.prepare("insert into rooms (id, summary_json, updated_at) values (?, ?, ?) on conflict(id) do update set summary_json = excluded.summary_json, updated_at = excluded.updated_at").run(e.id,JSON.stringify(e),Date.now())}getRooms(){return this.db.prepare("select summary_json from rooms order by updated_at desc").all().map(t=>JSON.parse(t.summary_json))}removeRoom(e){this.db.transaction(r=>{this.db.prepare("delete from messages where room_id = ?").run(r),this.db.prepare("delete from outbox where room_id = ?").run(r),this.db.prepare("delete from rooms where id = ?").run(r)})(e)}insertMessages(e){let t=this.db.prepare("insert into messages (room_id, seq, msg_id, sender_id, body, created_at) values (@roomId, @seq, @msgId, @senderId, @body, @createdAt) on conflict do nothing");this.db.transaction(s=>{for(let i of s)t.run(i)})(e)}getMessages(e,t=500){return this.db.prepare("select room_id as roomId, seq, msg_id as msgId, sender_id as senderId, body, created_at as createdAt from messages where room_id = ? order by seq desc limit ?").all(e,t).reverse()}maxSeq(e){return this.db.prepare("select max(seq) as m from messages where room_id = ?").get(e).m??0}enqueue(e,t,r){this.db.prepare("insert or ignore into outbox (msg_id, room_id, body, queued_at) values (?, ?, ?, ?)").run(e,t,r,Date.now())}dequeue(e){this.db.prepare("delete from outbox where msg_id = ?").run(e)}pendingOutbox(){return this.db.prepare("select msg_id as msgId, room_id as roomId, body from outbox order by queued_at asc").all()}setMeta(e,t){this.db.prepare("insert into meta (key, value) values (?, ?) on conflict(key) do update set value = excluded.value").run(e,t)}getMeta(e){return this.db.prepare("select value from meta where key = ?").get(e)?.value??null}close(){this.db.close()}};import{homedir as Pt}from"os";import{join as re}from"path";function Ce(o){let e=process.env.XDG_CONFIG_HOME??re(Pt(),".config"),t=o.replace(/[^a-zA-Z0-9.-]/g,"_");return re(e,"outis",t)}function W(o){return re(Ce(o),"identity.json")}function je(o){return re(Ce(o),"cache.db")}import{ed25519 as Be}from"@noble/curves/ed25519";import{generateMnemonic as qt,mnemonicToSeedSync as Dt,validateMnemonic as Oe}from"@scure/bip39";import{wordlist as fe}from"@scure/bip39/wordlists/english";var Ct=new TextEncoder,Ae=o=>Buffer.from(o).toString("base64url");function Ne(){let o=qt(fe,128);return J(o)}function Ke(o){return Oe(Le(o),fe)}function J(o){let e=Le(o);if(!Oe(e,fe))throw new Error("that recovery code is not valid, check the words and try again");let r=Dt(e).slice(0,32),s=Ae(Be.getPublicKey(r));return{mnemonic:e,privateKey:r,pubkey:s}}function Ue(o,e){return Ae(Be.sign(Ct.encode(e),o))}function Le(o){return o.trim().toLowerCase().replace(/\s+/g," ")}import{chmodSync as jt,mkdirSync as Bt,readFileSync as Ot,writeFileSync as At}from"fs";import{dirname as Nt}from"path";import{z as se}from"zod";var Kt=se.object({version:se.literal(1),mnemonic:se.string(),pubkey:se.string()});function be(o){let e;try{e=Ot(o,"utf8")}catch{return null}let t=Kt.parse(JSON.parse(e)),r=J(t.mnemonic);return{keypair:r,pubkey:r.pubkey}}function xe(o,e){Bt(Nt(o),{recursive:!0,mode:448});let t=JSON.stringify({version:1,mnemonic:e.mnemonic,pubkey:e.pubkey},null,2);At(o,t,{mode:384}),jt(o,384)}var ne="0.1.0";var ie=class{constructor(e){this.baseUrl=e}baseUrl;token=null;onOutdated=()=>{};setToken(e){this.token=e}setOutdatedHandler(e){this.onOutdated=e}async request(e,t,r){let s;try{s=await fetch(this.baseUrl+t,{method:e,headers:{"content-type":"application/json",[De]:ne,...this.token?{authorization:`Bearer ${this.token}`}:{}},...r!==void 0?{body:JSON.stringify(r)}:{}})}catch{throw new z("internal","could not reach the server")}let i=await s.json().catch(()=>null);if(!s.ok){let h=i?.error,y=new z(h?.code??"internal",h?.message??`request failed (${s.status})`);throw y.code==="client_outdated"&&this.onOutdated(y.message),y}return i}challenge(e){return this.request("POST",S.challenge(),{pubkey:e})}verify(e){return this.request("POST",S.verify(),e)}updateDisplayName(e){return this.request("PATCH",S.updateIdentity(),{displayName:e})}listRooms(){return this.request("GET",S.listRooms())}createRoom(e){return this.request("POST",S.createRoom(),e)}openDm(e){return this.request("POST",S.openDm(),{pubkey:e})}joinRoom(e){return this.request("POST",S.joinRoom(),{code:e})}requestDeletion(e){return this.request("POST",S.requestDeletion(e),{})}voteDeletion(e,t,r){return this.request("POST",S.voteDeletion(e),{requestId:t,approved:r})}};import Ve from"ws";var ae=class{constructor(e,t){this.wsUrl=e;this.token=t}wsUrl;token;ws=null;state="closed";closedByUser=!1;backoff=500;subscriptions=new Set;listeners=new Set;stateListeners=new Set;onEvent(e){return this.listeners.add(e),()=>this.listeners.delete(e)}onState(e){return this.stateListeners.add(e),()=>this.stateListeners.delete(e)}getState(){return this.state}setState(e){this.state=e;for(let t of this.stateListeners)t(e)}connect(){this.closedByUser=!1,this.open()}open(){this.setState("connecting");let e=new Ve(`${this.wsUrl}?token=${encodeURIComponent(this.token)}&${"v"}=${encodeURIComponent(ne)}`);this.ws=e,e.on("open",()=>{this.backoff=500,this.setState("open");for(let t of this.subscriptions)this.raw({type:"room.subscribe",roomId:t})}),e.on("message",t=>{let r=qe(JSON.parse(t.toString()));if(r.success)for(let s of this.listeners)s(r.data)}),e.on("close",()=>{this.setState("closed"),this.closedByUser||this.scheduleReconnect()}),e.on("error",()=>{})}scheduleReconnect(){let e=Math.floor(Math.random()*300),t=Math.min(this.backoff,15e3)+e;this.backoff=Math.min(this.backoff*2,15e3),setTimeout(()=>{this.closedByUser||this.open()},t).unref()}subscribe(e){this.subscriptions.add(e),this.state==="open"&&this.raw({type:"room.subscribe",roomId:e})}unsubscribe(e){this.subscriptions.delete(e),this.state==="open"&&this.raw({type:"room.unsubscribe",roomId:e})}send(e){return this.raw(e)}raw(e){return this.ws?.readyState===Ve.OPEN?(this.ws.send(JSON.stringify(e)),!0):!1}close(){this.closedByUser=!0,this.ws?.close()}};var me=class extends Lt{constructor(t,r){super();this.baseUrl=t;this.wsUrl=r;this.host=new URL(t).host,this.api=new ie(t),this.api.setOutdatedHandler(s=>this.patch({mustUpdate:s})),this.cache=new oe(je(this.host))}baseUrl;wsUrl;api;cache;host;keypair=null;socket=null;state={connection:"closed",identity:null,rooms:[],activeRoomId:null,messages:[],pending:[],deletion:null,notice:null,error:null,mustUpdate:null};getState(){return this.state}patch(t){this.state={...this.state,...t},this.emit("change",this.state)}hasKeystore(){return be(W(this.host))!==null}async resume(){let t=be(W(this.host));if(!t)throw new Error("no identity on this machine");this.keypair=t.keypair,await this.authenticate()}async createNew(t){let r=Ne();return xe(W(this.host),r),this.keypair=r,await this.authenticate(t),r.mnemonic}async restore(t,r){let s=J(t);xe(W(this.host),s),this.keypair=s,await this.authenticate(r)}async authenticate(t){if(!this.keypair)throw new Error("no keypair loaded");let{pubkey:r,privateKey:s}=this.keypair,i=await this.api.challenge(r),h=new Date().toISOString(),y=`${i.nonce}:outis:${h}`,d=Ue(s,y),c=await this.api.verify({pubkey:r,nonce:i.nonce,signature:d,issuedAt:h,...t?{displayName:t}:{}});this.api.setToken(c.token),this.patch({identity:c.identity}),this.connectSocket(c.token),await this.refreshRooms()}connectSocket(t){let r=new ae(this.wsUrl,t);this.socket=r,r.onState(s=>{this.patch({connection:s}),s==="open"&&this.flushOutbox()}),r.onEvent(s=>this.onServerEvent(s)),r.connect()}onServerEvent(t){switch(t.type){case"message.new":{this.cache.insertMessages([t.message]),t.message.roomId===this.state.activeRoomId&&this.reloadActiveMessages(),this.refreshRooms();return}case"message.ack":{let r=this.state.pending.find(s=>s.msgId===t.msgId);this.cache.dequeue(t.msgId),r&&this.state.identity&&this.cache.insertMessages([{roomId:t.roomId,seq:t.seq,msgId:t.msgId,senderId:this.state.identity.id,body:r.body,createdAt:r.createdAt}]),this.patch({pending:this.state.pending.filter(s=>s.msgId!==t.msgId)}),t.roomId===this.state.activeRoomId&&this.reloadActiveMessages();return}case"message.sync.result":{if(this.cache.insertMessages(t.messages),t.roomId===this.state.activeRoomId&&this.reloadActiveMessages(),t.hasMore&&t.messages.length>0){let r=this.cache.maxSeq(t.roomId);this.socket?.send({type:"message.sync",roomId:t.roomId,sinceSeq:r})}return}case"deletion.updated":{t.request.roomId===this.state.activeRoomId&&this.patch({deletion:t.request.status==="pending"?t.request:null});return}case"room.deleted":{this.cache.removeRoom(t.roomId);let r=this.state.activeRoomId===t.roomId,s="that room was deleted for everyone";this.patch({rooms:this.state.rooms.filter(i=>i.id!==t.roomId),notice:s,...r?{activeRoomId:null,messages:[],deletion:null}:{}}),this.flashClear(s);return}case"room.membersChanged":this.refreshRooms();return;case"error":this.patch({error:t.error.message});return}}async refreshRooms(){this.patch({rooms:this.cache.getRooms()});try{let{rooms:t}=await this.api.listRooms();for(let r of t)this.cache.upsertRoom(r);this.reconcileTombstones(t),this.patch({rooms:this.cache.getRooms()})}catch{}}reconcileTombstones(t){let r=new Set(t.map(s=>s.id));for(let s of this.cache.getRooms())r.has(s.id)||this.cache.removeRoom(s.id)}openRoom(t){let r=this.state.rooms.find(s=>s.id===t)??null;this.patch({activeRoomId:t,messages:this.cache.getMessages(t),deletion:null,error:null,notice:null}),this.socket?.subscribe(t),this.socket?.send({type:"message.sync",roomId:t,sinceSeq:this.cache.maxSeq(t)}),r&&this.socket?.send({type:"read.mark",roomId:t,seq:r.lastSeq})}closeRoom(){this.state.activeRoomId&&this.socket?.unsubscribe(this.state.activeRoomId),this.patch({activeRoomId:null,messages:[],pending:[],deletion:null})}sendMessage(t){let r=this.state.activeRoomId;if(!r||t.trim().length===0)return;let s=Vt(),i=new Date().toISOString();this.cache.enqueue(s,r,t),this.patch({pending:[...this.state.pending,{msgId:s,roomId:r,body:t,createdAt:i}]}),this.socket?.send({type:"message.send",roomId:r,msgId:s,body:t})}flushOutbox(){for(let t of this.cache.pendingOutbox())this.socket?.send({type:"message.send",roomId:t.roomId,msgId:t.msgId,body:t.body})}reloadActiveMessages(){this.state.activeRoomId&&this.patch({messages:this.cache.getMessages(this.state.activeRoomId)})}async createGroup(t,r="full"){let{room:s}=await this.api.createRoom({name:t,historyMode:r});return await this.refreshRooms(),s}async joinByCode(t){let{room:r}=await this.api.joinRoom(t);return await this.refreshRooms(),r}async openDm(t){let{room:r}=await this.api.openDm(t);return await this.refreshRooms(),r}async setDisplayName(t){await this.api.updateDisplayName(t),this.state.identity&&this.patch({identity:{...this.state.identity,displayName:t}})}async requestDeletion(){if(!this.state.activeRoomId)return;let{request:t}=await this.api.requestDeletion(this.state.activeRoomId);this.patch({deletion:t.status==="pending"?t:null})}async voteDeletion(t){let r=this.state.activeRoomId,s=this.state.deletion;!r||!s||await this.api.voteDeletion(r,s.id,t)}myMnemonic(){return this.keypair?.mnemonic??null}clearNotice(){(this.state.notice||this.state.error)&&this.patch({notice:null,error:null})}flashClear(t){setTimeout(()=>{this.state.notice===t&&this.patch({notice:null})},6e3).unref()}shutdown(){this.socket?.close(),this.cache.close()}};import{Box as ct,Text as dt}from"ink";import{useEffect as mo,useState as he}from"react";var n={accent:"green",accentBright:"greenBright",you:"greenBright",other:"green",dim:"gray",text:"white",notice:"yellow",danger:"red",selectionBg:"green",selectionFg:"black",border:"green"},$e="\u25B8";import{useEffect as ze,useState as He}from"react";function ce(o){let[e,t]=He(o.getState());return ze(()=>{let r=s=>t(s);return o.on("change",r),t(o.getState()),()=>{o.off("change",r)}},[o]),e}function Fe(){let[o,e]=He({columns:process.stdout.columns??80,rows:process.stdout.rows??24});return ze(()=>{let t=()=>e({columns:process.stdout.columns??80,rows:process.stdout.rows??24});return process.stdout.on("resize",t),()=>{process.stdout.off("resize",t)}},[]),o}import{Box as j,Text as B,useApp as eo,useInput as to}from"ink";import{useMemo as oo,useState as ot}from"react";var l={appName:"outis",tagline:"chat with no accounts. your identity is a code you carry.",onboard:{prompt:"first time here, or coming back?",create:"start fresh",restore:"i have a recovery code",askName:"what should people call you?",askMnemonic:"paste your recovery code (the 12 words)",badMnemonic:"those words don't check out, take another look"},mnemonic:{heading:"this is your recovery code",body:"write it down somewhere safe. it is the only way back to this identity, and anyone who has it is you. we cannot reset it for you.",confirm:"press enter once you have saved it"},rooms:{empty:"no rooms yet. type /new <name> to start one, or /join <code> to join.",emptyShort:"no rooms yet",header:"rooms"},chat:{placeholder:"message \xB7 /help",deletePending:"delete requested. everyone has to agree. /approve or /decline.",deleted:"this room is gone."},help:["/new <name> start a group room","/join <code> join a room by its code","/dm <pubkey> start a direct message","/name <name> change your display name","/code show this room join code","/whoami show your identity and fingerprint","/delete ask to delete this room for everyone","/approve agree to a pending delete","/decline reject a pending delete","/clear dismiss notices and reset the view","/quit exit","","up / down switch between rooms","esc close this help"],status:{connecting:"connecting",open:"online",closed:"offline"}};import{Box as Je,Text as P}from"ink";import{jsx as ve,jsxs as C}from"react/jsx-runtime";function $t(o,e,t){if(t&&o===t.id)return"you";let r=e?.members.find(s=>s.identityId===o);return r?`${r.displayName}#${M(r.pubkey)}`:"someone"}function We(o){let e=new Date(o);return`${String(e.getHours()).padStart(2,"0")}:${String(e.getMinutes()).padStart(2,"0")}`}function Xe({room:o,messages:e,pending:t,self:r,height:s}){let i=Math.max(3,s-t.length),h=new Set(t.map(c=>c.msgId)),d=e.filter(c=>!h.has(c.msgId)).slice(-i);return C(Je,{flexDirection:"column",flexGrow:1,height:s,paddingX:1,justifyContent:"flex-end",overflow:"hidden",children:[d.length===0&&t.length===0?ve(P,{color:n.dim,children:"no messages yet. say something."}):d.map(c=>C(P,{wrap:"wrap",children:[C(P,{color:n.dim,children:[We(c.createdAt)," "]}),ve(P,{color:r&&c.senderId===r.id?n.you:n.other,bold:!0,children:$t(c.senderId,o,r)}),C(P,{color:n.text,children:[": ",c.body]})]},c.msgId)),t.map(c=>C(P,{color:n.dim,wrap:"wrap",children:[We(c.createdAt)," you: ",c.body," \u22EF"]},c.msgId))]})}function Ge({pendingCount:o}){return ve(Je,{paddingX:1,children:C(P,{color:n.danger,children:[l.chat.deletePending,o>0?C(P,{color:n.dim,children:[" (",o," yet to vote)"]}):null]})})}import{Box as zt,Text as Ht}from"ink";import Ft from"ink-text-input";import{useState as Wt}from"react";import{jsx as Jt,jsxs as Ze}from"react/jsx-runtime";function Ye({onSubmit:o,placeholder:e}){let[t,r]=Wt(""),s=i=>{r(""),o(i)};return Ze(zt,{borderStyle:"round",borderColor:n.border,paddingX:1,children:[Ze(Ht,{color:n.accentBright,children:[$e," "]}),Jt(Ft,{value:t,onChange:r,onSubmit:s,placeholder:e})]})}import{Box as Xt,Text as de}from"ink";import{jsx as Re,jsxs as Qe}from"react/jsx-runtime";function Gt(o,e){if(o.kind==="group")return o.name??"group";let t=o.members.find(r=>r.identityId!==e?.id);return t?`${t.displayName} #${M(t.pubkey)}`:"direct message"}function et({rooms:o,self:e,activeRoomId:t,width:r,height:s}){return Qe(Xt,{flexDirection:"column",width:r,height:s,borderStyle:"single",borderColor:n.border,borderTop:!1,borderBottom:!1,borderLeft:!1,paddingX:1,overflow:"hidden",children:[Re(de,{color:n.dim,children:l.rooms.header}),o.length===0?Re(de,{color:n.dim,wrap:"wrap",children:l.rooms.emptyShort}):o.map(i=>{let h=i.id===t,y=i.unreadCount>0?` (${i.unreadCount})`:"",d=h?{color:n.selectionFg,backgroundColor:n.selectionBg}:{};return Qe(de,{wrap:"truncate",...d,children:[h?"\u25B8 ":" ",Gt(i,e),Re(de,{color:h?n.selectionFg:n.notice,children:y})]},i.id)})]})}import{Box as Zt,Text as X}from"ink";import{jsx as Qt,jsxs as G}from"react/jsx-runtime";var Yt={connecting:n.notice,open:n.accentBright,closed:n.danger};function tt({identity:o,connection:e}){return G(Zt,{justifyContent:"space-between",paddingX:1,borderStyle:"round",borderColor:n.border,children:[G(X,{children:[Qt(X,{color:n.accentBright,bold:!0,children:l.appName}),o?G(X,{color:n.dim,children:[" ",o.displayName,G(X,{color:n.accent,children:["#",M(o.pubkey)]})]}):null]}),G(X,{color:Yt[e],children:["\u25CF ",l.status[e]]})]})}import{Fragment as so,jsx as x,jsxs as O}from"react/jsx-runtime";function rt({client:o}){let e=ce(o),{columns:t,rows:r}=Fe(),{exit:s}=eo(),[i,h]=ot(!1),[y,d]=ot(null),c=oo(()=>e.rooms.find(f=>f.id===e.activeRoomId),[e.rooms,e.activeRoomId]),I=e.rooms.findIndex(f=>f.id===e.activeRoomId),U=Math.max(20,Math.min(32,Math.floor(t*.26))),g=t<60,k=f=>{let p=e.rooms[f];p&&(h(!1),o.openRoom(p.id))};to((f,p)=>{if(p.escape&&i){h(!1);return}e.rooms.length!==0&&(p.upArrow&&k(I<=0?0:I-1),p.downArrow&&k(I<0?0:Math.min(e.rooms.length-1,I+1)))});let L=f=>{d(null),f.then(p=>o.openRoom(p.id)).catch(p=>d(p instanceof Error?p.message:"something went wrong"))},Y=f=>{f.then(()=>d(null)).catch(p=>d(p instanceof Error?p.message:"something went wrong"))},ft=f=>{let[p,...xt]=f.slice(1).trim().split(/\s+/),_=xt.join(" ");switch(o.clearNotice(),d(null),p){case"help":h(vt=>!vt);break;case"new":_?L(o.createGroup(_)):d("usage: /new <name>");break;case"join":_?L(o.joinByCode(_)):d("usage: /join <code>");break;case"dm":_?L(o.openDm(_)):d("usage: /dm <pubkey>");break;case"name":_?Y(o.setDisplayName(_)):d("usage: /name <display name>");break;case"code":d(c?`join code: ${c.joinCode}`:"open a room first");break;case"whoami":d(e.identity?`${e.identity.displayName} #${M(e.identity.pubkey)} \xB7 ${e.identity.pubkey}`:"not signed in");break;case"clear":d(null),h(!1),o.clearNotice();break;case"delete":e.activeRoomId?Y(o.requestDeletion()):d("open a room first");break;case"approve":Y(o.voteDeletion(!0));break;case"decline":Y(o.voteDeletion(!1));break;case"quit":o.shutdown(),s();break;default:d(`unknown command: /${p}`)}},bt=f=>{let p=f.trim();if(p.startsWith("/"))return ft(p);p.length>0&&(h(!1),o.sendMessage(p))},ge=y??e.notice??e.error,Q=Math.max(3,r-(ge?7:6));return O(j,{flexDirection:"column",width:t,height:r,children:[x(tt,{identity:e.identity,connection:e.connection}),O(j,{height:Q,children:[!g&&x(et,{rooms:e.rooms,self:e.identity,activeRoomId:e.activeRoomId,width:U,height:Q}),x(j,{flexDirection:"column",flexGrow:1,height:Q,paddingLeft:g?0:1,overflow:"hidden",children:i?O(j,{flexDirection:"column",paddingX:1,children:[x(B,{color:n.accentBright,bold:!0,children:"commands"}),l.help.map((f,p)=>x(B,{color:n.dim,children:f},p))]}):c?O(so,{children:[O(j,{paddingX:1,children:[x(B,{color:n.accentBright,bold:!0,children:c.kind==="group"?c.name:"direct message"}),O(B,{color:n.dim,children:[" \xB7 ",c.members.length," here"]})]}),x(Xe,{room:c,messages:e.messages,pending:e.pending,self:e.identity,height:Q-(e.deletion?2:1)}),e.deletion?x(Ge,{pendingCount:ro(e)}):null]}):O(j,{paddingX:1,paddingTop:1,flexDirection:"column",children:[x(B,{color:n.dim,children:l.rooms.empty}),g?x(B,{color:n.dim,children:"(terminal is narrow, room list hidden)"}):null]})})]}),ge?x(j,{paddingX:1,children:x(B,{color:e.error?n.danger:n.notice,wrap:"truncate",children:ge})}):null,x(Ye,{onSubmit:bt,placeholder:l.chat.placeholder})]})}function ro(o){if(!o.deletion)return 0;let e=new Set(o.deletion.votes.filter(t=>t.approved).map(t=>t.identityId));return o.deletion.memberSnapshot.filter(t=>!e.has(t)).length}import{Box as le,Text as w,useInput as no}from"ink";import st from"ink-text-input";import{useState as pe}from"react";import{Fragment as io,jsx as v,jsxs as H}from"react/jsx-runtime";function nt({onCreate:o,onRestore:e,busy:t,error:r}){let[s,i]=pe("choose"),[h,y]=pe(""),[d,c]=pe(""),[I,U]=pe(null);return no(g=>{s==="choose"&&(g==="c"&&i("name"),g==="r"&&i("restore"))},{isActive:s==="choose"}),H(le,{flexDirection:"column",padding:1,children:[v(w,{bold:!0,color:n.accentBright,children:l.appName}),v(w,{color:n.dim,children:l.tagline}),H(le,{marginTop:1,flexDirection:"column",children:[s==="choose"&&H(io,{children:[v(w,{children:l.onboard.prompt}),H(w,{children:[v(w,{color:n.accentBright,children:"c"})," ",l.onboard.create," ",v(w,{color:n.accentBright,children:"r"})," ",l.onboard.restore]})]}),s==="name"&&H(le,{flexDirection:"column",children:[v(w,{children:l.onboard.askName}),v(st,{value:h,onChange:y,onSubmit:g=>g.trim()&&o(g.trim())})]}),s==="restore"&&H(le,{flexDirection:"column",children:[v(w,{children:l.onboard.askMnemonic}),v(st,{value:d,onChange:c,onSubmit:g=>{if(!Ke(g)){U(l.onboard.badMnemonic);return}U(null),e(g.trim())}})]}),t?v(w,{color:n.notice,children:"working\u2026"}):null,I??r?v(w,{color:n.danger,children:I??r}):null]})]})}import{Box as Se,Text as A,useInput as ao}from"ink";import{jsx as N,jsxs as we}from"react/jsx-runtime";function it({mnemonic:o,onDone:e}){ao((r,s)=>{s.return&&e()});let t=o.split(" ");return we(Se,{flexDirection:"column",padding:1,children:[N(A,{bold:!0,color:n.accent,children:l.mnemonic.heading}),N(Se,{marginY:1,flexDirection:"column",borderStyle:"round",borderColor:n.accent,paddingX:1,children:[0,1,2].map(r=>N(A,{children:t.slice(r*4,r*4+4).map((s,i)=>we(A,{children:[we(A,{color:n.dim,children:[String(r*4+i+1).padStart(2," "),"."]})," ",N(A,{color:n.accentBright,children:s.padEnd(10)})]},s))},r))}),N(A,{color:n.dim,wrap:"wrap",children:l.mnemonic.body}),N(Se,{marginTop:1,children:N(A,{color:n.accentBright,children:l.mnemonic.confirm})})]})}import{Box as ue,Text as Z}from"ink";import{jsx as K,jsxs as at}from"react/jsx-runtime";function mt({message:o}){return at(ue,{flexDirection:"column",padding:1,children:[K(Z,{bold:!0,color:n.danger,children:"update required"}),K(ue,{marginTop:1,children:K(Z,{color:n.text,wrap:"wrap",children:o})}),at(ue,{marginTop:1,flexDirection:"column",borderStyle:"round",borderColor:n.border,paddingX:1,children:[K(Z,{color:n.dim,children:"run this to get the latest:"}),K(Z,{color:n.accentBright,children:"npx outis-chat@latest"})]}),K(ue,{marginTop:1,children:K(Z,{color:n.dim,children:"press ctrl+c to exit"})})]})}import{jsx as q}from"react/jsx-runtime";function lt({client:o}){let e=ce(o),[t,r]=he("booting"),[s,i]=he(!1),[h,y]=he(null),[d,c]=he(null);mo(()=>{if(!o.hasKeystore()){r("onboarding");return}o.resume().then(()=>r("ready")).catch(g=>{y(g instanceof Error?g.message:"could not sign in"),r("onboarding")})},[o]);let I=g=>{i(!0),y(null),o.createNew(g).then(k=>{c(k),r("show-mnemonic")}).catch(k=>y(k instanceof Error?k.message:"could not create identity")).finally(()=>i(!1))},U=(g,k)=>{i(!0),y(null),o.restore(g,k).then(()=>r("ready")).catch(L=>y(L instanceof Error?L.message:"could not restore")).finally(()=>i(!1))};return e.mustUpdate?q(mt,{message:e.mustUpdate}):t==="booting"?q(ct,{padding:1,children:q(dt,{color:n.dim,children:"connecting\u2026"})}):t==="onboarding"?q(nt,{onCreate:I,onRestore:U,busy:s,error:h}):t==="show-mnemonic"&&d?q(it,{mnemonic:d,onDone:()=>r("ready")}):e.identity?q(rt,{client:o}):q(ct,{padding:1,children:q(dt,{color:n.dim,children:"loading\u2026"})})}function po(){return process.argv.find(e=>e.startsWith("--server="))?.split("=")[1]??process.env.OUTIS_SERVER??Ie}function uo(o){let e=new URL(o);return e.protocol=e.protocol==="https:"?"wss:":"ws:",e.pathname="/ws",e.toString()}var ho="\x1B[?1049h",go="\x1B[?1049l",yo="\x1B[?7l",fo="\x1B[?7h",ht=process.stdout.isTTY===!0,pt=!1,gt=()=>{pt||!ht||(pt=!0,process.stdout.write(fo+go))},ut=po(),yt=new me(ut,uo(ut));ht&&process.stdout.write(ho+yo);var{waitUntilExit:bo}=co(lo(lt,{client:yt}));bo().then(()=>{yt.shutdown(),gt()});process.on("exit",gt);
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "outis-chat",
3
+ "version": "0.1.0",
4
+ "description": "A terminal chat app with no accounts. Your identity is a recovery code you carry anywhere.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": ["chat", "cli", "terminal", "tui", "e2e", "ink"],
8
+ "bin": {
9
+ "outis": "./dist-pkg/outis.js"
10
+ },
11
+ "files": [
12
+ "dist-pkg"
13
+ ],
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "dev": "tsx src/index.tsx",
22
+ "build": "tsc -b",
23
+ "build:pkg": "tsup",
24
+ "prepublishOnly": "npm run build:pkg",
25
+ "start": "node dist/index.js"
26
+ },
27
+ "dependencies": {
28
+ "@noble/curves": "^1.7.0",
29
+ "@scure/bip39": "^1.5.0",
30
+ "better-sqlite3": "^11.7.0",
31
+ "ink": "^5.1.0",
32
+ "ink-text-input": "^6.0.0",
33
+ "react": "^18.3.1",
34
+ "ulid": "^2.3.0",
35
+ "ws": "^8.18.0",
36
+ "zod": "^4.0.5"
37
+ },
38
+ "devDependencies": {
39
+ "@outis/protocol": "*",
40
+ "@types/better-sqlite3": "^7.6.12",
41
+ "@types/react": "^18.3.18",
42
+ "@types/ws": "^8.5.13",
43
+ "tsup": "^8.3.5",
44
+ "tsx": "^4.19.2"
45
+ }
46
+ }