outis-chat 0.1.0 → 0.1.2
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/LICENSE +21 -0
- package/README.md +108 -0
- package/dist-pkg/outis.js +2 -2
- package/package.json +9 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rbayuokt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://ik.imagekit.io/rb4cxb5f5/personal-projects/outis-cli.png" alt="outis" width="100%">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# outis
|
|
6
|
+
|
|
7
|
+
A terminal chat app with no accounts. You pick a name and start talking. Your identity is a 12 word recovery code you can carry to any machine, and there is no email, no password, and nothing to sign up for.
|
|
8
|
+
|
|
9
|
+
The name comes from the Odyssey. Odysseus tells the Cyclops his name is "Outis", which means "nobody", so that when the Cyclops cries out that "Nobody" is attacking him, the other Cyclopes hear "nobody is attacking me" and leave him alone. A name that identifies you while giving nothing away. That is the whole idea here: you are a keypair, and the recovery code is the key.
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<img src="https://ik.imagekit.io/rb4cxb5f5/personal-projects/demo.gif" alt="Two identities talking to each other in the terminal" width="100%">
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx outis-chat
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Node 20 or newer, nothing else to install. To keep it around:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g outis-chat
|
|
25
|
+
outis
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The installed command is `outis`. The package is `outis-chat` only because npm will not accept the shorter name.
|
|
29
|
+
|
|
30
|
+
## First run
|
|
31
|
+
|
|
32
|
+
It asks whether you are starting fresh or restoring. Pick fresh, choose a display name, and it shows your recovery code once. Write those 12 words down somewhere real. They are the only way back into your identity, and nobody can reset them for you.
|
|
33
|
+
|
|
34
|
+
That is the entire signup.
|
|
35
|
+
|
|
36
|
+
To talk to someone, one of you creates a room and shares the join code:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
/new weekend-plans creates a group, then /code shows its join code
|
|
40
|
+
/join plum-otter-4417 the other person joins with that code
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For a direct message you need their public key. They run `/whoami` and send you the long string, then you run `/dm <that key>`.
|
|
44
|
+
|
|
45
|
+
## Commands
|
|
46
|
+
|
|
47
|
+
| | |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `/new <name>` | start a group |
|
|
50
|
+
| `/join <code>` | join a group by its code |
|
|
51
|
+
| `/dm <pubkey>` | open a direct message |
|
|
52
|
+
| `/code` | show the current room's join code |
|
|
53
|
+
| `/whoami` | your display name, fingerprint, and full public key |
|
|
54
|
+
| `/name <name>` | change your display name |
|
|
55
|
+
| `/delete` | ask to delete the room, everyone has to agree |
|
|
56
|
+
| `/approve` `/decline` | vote on a pending deletion |
|
|
57
|
+
| `/clear` | dismiss notices and close help |
|
|
58
|
+
| `/help` | toggle the help panel, esc closes it |
|
|
59
|
+
| `/quit` | exit |
|
|
60
|
+
|
|
61
|
+
Arrow keys move through the room list, and enter on an empty input opens the selected room. The dot in the top right is your connection: yellow connecting, green online, red offline. Anything you type while offline queues up and sends itself when the socket comes back.
|
|
62
|
+
|
|
63
|
+
## Using your own server
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx outis-chat --server=https://chat.example.com
|
|
67
|
+
# or
|
|
68
|
+
OUTIS_SERVER=https://chat.example.com npx outis-chat
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The server is open source and runs as two Docker containers. See the project repository if you would rather host your own than use the default.
|
|
72
|
+
|
|
73
|
+
## Where your data lives
|
|
74
|
+
|
|
75
|
+
Everything the client stores is under `~/.config/outis/<server-host>/`:
|
|
76
|
+
|
|
77
|
+
- `identity.json`, your keystore, written `0600`
|
|
78
|
+
- `cache.db`, a SQLite copy of your rooms and messages
|
|
79
|
+
|
|
80
|
+
Deleting `cache.db` is safe and it rebuilds on the next sync. Deleting `identity.json` loses that identity unless you have the recovery code. Identities are keyed by server host, so pointing at a different server gives you a separate identity.
|
|
81
|
+
|
|
82
|
+
## Worth knowing before you rely on it
|
|
83
|
+
|
|
84
|
+
**The recovery code is a bearer credential.** Anyone who reads your 12 words is you. Not "can request a reset", is. Treat it like an SSH private key.
|
|
85
|
+
|
|
86
|
+
**Lose the code and the identity is gone.** There is no reset, no support email, no recovery flow. That is a property of the model rather than a missing feature.
|
|
87
|
+
|
|
88
|
+
**Message content is not end to end encrypted yet.** TLS protects it in transit, but the server operator can read it. The message envelope already has room for a ciphertext field, so this can be added without breaking the protocol.
|
|
89
|
+
|
|
90
|
+
**Display names are not unique.** Impersonating a name is trivial, which is why the client always shows a short key fingerprint next to it, like `ari#k7q2mx`, and why invites work by code and never by name lookup.
|
|
91
|
+
|
|
92
|
+
**"Delete for everyone" is not erasure.** It removes the room from the server and tells connected clients to purge their local copy. It cannot un-screenshot anything.
|
|
93
|
+
|
|
94
|
+
## Updating
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npx outis-chat@latest
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`npx` reuses a cached version otherwise. The server also enforces a minimum client version, so if yours is too old to connect it will tell you exactly what to run.
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
Created by [@rbayuokt](https://github.com/rbayuokt), made with love ❤️ and music 🎵
|
package/dist-pkg/outis.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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
|
|
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 a}from"zod";var Do=a.object({pubkey:T}),Co=a.object({nonce:a.string().min(32),expiresAt:a.iso.datetime(),serverId:a.string().optional()}),jo=a.object({pubkey:T,nonce:a.string(),signature:a.string(),issuedAt:a.iso.datetime(),displayName:V.optional()}),Bo=a.object({token:a.string(),expiresAt:a.iso.datetime(),identity:Ee,created:a.boolean()}),Oo=a.object({newPubkey:T,signature:a.string()}),Ao=a.object({displayName:V}),No=a.object({rooms:a.array(Me)}),Ko=a.object({name:a.string().trim().min(1).max(64),historyMode:ee.default("full")}),Uo=a.object({pubkey:T}),Lo=a.object({code:a.string().min(4)}),Vo=a.object({sinceSeq:a.coerce.number().int().nonnegative().default(0),limit:a.coerce.number().int().min(1).max(500).default(200)}),$o=a.object({messages:a.array($),hasMore:a.boolean()}),zo=a.object({requestId:a.uuid(),approved:a.boolean()}),Ho=a.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 m}from"zod";var Zo=m.discriminatedUnion("type",[m.object({type:m.literal("ping")}),m.object({type:m.literal("room.subscribe"),roomId:b}),m.object({type:m.literal("room.unsubscribe"),roomId:b}),m.object({type:m.literal("message.send"),roomId:b,msgId:F,body:m.string().min(1).max(4e3)}),m.object({type:m.literal("message.sync"),roomId:b,sinceSeq:E}),m.object({type:m.literal("read.mark"),roomId:b,seq:E})]),_t=m.discriminatedUnion("type",[m.object({type:m.literal("pong")}),m.object({type:m.literal("message.new"),message:$}),m.object({type:m.literal("message.ack"),roomId:b,msgId:F,seq:E}),m.object({type:m.literal("message.sync.result"),roomId:b,messages:m.array($),hasMore:m.boolean()}),m.object({type:m.literal("deletion.updated"),request:te}),m.object({type:m.literal("room.deleted"),roomId:b}),m.object({type:m.literal("room.membersChanged"),roomId:b}),m.object({type:m.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
3
|
create table if not exists rooms (
|
|
4
4
|
id text primary key,
|
|
5
5
|
summary_json text not null,
|
|
@@ -25,4 +25,4 @@ import{render as co}from"ink";import{createElement as lo}from"react";var Ie="htt
|
|
|
25
25
|
);
|
|
26
26
|
|
|
27
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);
|
|
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.2";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}:${i.serverId??"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
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "outis-chat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "A terminal chat app with no accounts. Your identity is a recovery code you carry anywhere.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"keywords": [
|
|
7
|
+
"keywords": [
|
|
8
|
+
"chat",
|
|
9
|
+
"cli",
|
|
10
|
+
"terminal",
|
|
11
|
+
"tui",
|
|
12
|
+
"e2e",
|
|
13
|
+
"ink"
|
|
14
|
+
],
|
|
8
15
|
"bin": {
|
|
9
16
|
"outis": "./dist-pkg/outis.js"
|
|
10
17
|
},
|