0x0-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/0x0.js ADDED
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ import { program } from 'commander'
3
+ import { cmdInit } from '../src/commands/init.js'
4
+ import { cmdWhoami } from '../src/commands/whoami.js'
5
+ import { cmdRenew } from '../src/commands/renew.js'
6
+ import {
7
+ cmdPinNew, cmdPinList, cmdPinRotate, cmdPinRevoke, cmdPinInfo
8
+ } from '../src/commands/pin.js'
9
+ import { cmdSend } from '../src/commands/send.js'
10
+ import { cmdInbox } from '../src/commands/inbox.js'
11
+ import { cmdRead } from '../src/commands/read.js'
12
+ import { cmdChat } from '../src/commands/chat.js'
13
+ import { cmdWeb } from '../src/commands/web.js'
14
+ import { cmdListen } from '../src/commands/listen.js'
15
+ import { cmdPipe } from '../src/commands/pipe.js'
16
+ import {
17
+ cmdContactAdd, cmdContactList, cmdContactLabel, cmdContactRemove
18
+ } from '../src/commands/contact.js'
19
+ import { cmdQr } from '../src/commands/qr.js'
20
+
21
+ program
22
+ .name('0x0')
23
+ .description('P2P disposable number messenger')
24
+ .version('1.0.0')
25
+
26
+ program
27
+ .command('init')
28
+ .description('initialize 0x0 and generate your number')
29
+ .action(cmdInit)
30
+
31
+ program
32
+ .command('whoami')
33
+ .description('show your number and active pins')
34
+ .action(cmdWhoami)
35
+
36
+ program
37
+ .command('renew')
38
+ .description('renew your number (resets all PINs)')
39
+ .action(cmdRenew)
40
+
41
+ // pin subcommands
42
+ const pin = program.command('pin').description('manage PINs')
43
+
44
+ pin
45
+ .command('new')
46
+ .description('create a new PIN')
47
+ .option('-l, --label <label>', 'label for this PIN')
48
+ .option('-e, --expires <duration>', 'expiry duration (e.g. 24h, 7d, 1w)')
49
+ .option('--once', 'expire after receiving one message')
50
+ .action((opts) => cmdPinNew(opts))
51
+
52
+ pin
53
+ .command('list')
54
+ .description('list all active PINs')
55
+ .action(cmdPinList)
56
+
57
+ pin
58
+ .command('rotate <pin>')
59
+ .description('rotate a PIN (generates new value)')
60
+ .action(cmdPinRotate)
61
+
62
+ pin
63
+ .command('revoke <pin>')
64
+ .description('revoke a PIN immediately')
65
+ .action(cmdPinRevoke)
66
+
67
+ pin
68
+ .command('info <pin>')
69
+ .description('show PIN details')
70
+ .action(cmdPinInfo)
71
+
72
+ program
73
+ .command('chat <number> <pin>')
74
+ .description('start interactive P2P chat')
75
+ .action(cmdChat)
76
+
77
+ program
78
+ .command('send <number> <pin> <message>')
79
+ .description('send a single message and exit')
80
+ .action(cmdSend)
81
+
82
+ program
83
+ .command('inbox')
84
+ .description('show inbox (all pins)')
85
+ .option('--json', 'output as JSON')
86
+ .action((opts) => cmdInbox(opts))
87
+
88
+ program
89
+ .command('read <pin>')
90
+ .description('read messages for a specific PIN')
91
+ .option('--json', 'output as JSON')
92
+ .action((p, opts) => cmdRead(p, opts))
93
+
94
+ program
95
+ .command('listen')
96
+ .description('listen for incoming messages (daemon mode)')
97
+ .option('-p, --pin <pin>', 'listen on a specific PIN only')
98
+ .action((opts) => cmdListen(opts))
99
+
100
+ program
101
+ .command('pipe <number> <pin>')
102
+ .description('stdio mode for agents (JSON stream)')
103
+ .action(cmdPipe)
104
+
105
+ program
106
+ .command('web')
107
+ .description('start browser UI (localhost)')
108
+ .option('-p, --port <port>', 'port number', '3000')
109
+ .option('--no-open', 'do not open browser automatically')
110
+ .action((opts) => cmdWeb({ port: parseInt(opts.port), noOpen: !opts.open }))
111
+
112
+ // contact subcommands
113
+ const contact = program.command('contact').description('manage contacts')
114
+
115
+ contact
116
+ .command('add <uri-or-number> [pin]')
117
+ .description('add a contact (URI or number+pin)')
118
+ .option('-l, --label <label>', 'label for this contact')
119
+ .action((uriOrNumber, pin, opts) => cmdContactAdd(uriOrNumber, pin, opts))
120
+
121
+ contact
122
+ .command('list')
123
+ .description('list all contacts')
124
+ .action(cmdContactList)
125
+
126
+ contact
127
+ .command('label <id> <label>')
128
+ .description('update contact label')
129
+ .action(cmdContactLabel)
130
+
131
+ contact
132
+ .command('remove <id>')
133
+ .description('remove a contact')
134
+ .action(cmdContactRemove)
135
+
136
+ program
137
+ .command('qr <pin>')
138
+ .description('show QR code for a PIN (share to let others connect)')
139
+ .action(cmdQr)
140
+
141
+ program.parse()
@@ -0,0 +1 @@
1
+ :root{--bg: #000000;--surface: #111111;--border: #222222;--border-strong:#444444;--text-muted: #888888;--text-sec: #aaaaaa;--text: #ffffff;--page-bg: #0a0a0a;--mono: "Share Tech Mono", monospace;--sans: "Noto Sans JP", sans-serif}:root.light{--bg: #ffffff;--surface: #f5f5f5;--border: #e0e0e0;--border-strong:#bbbbbb;--text-muted: #666666;--text-sec: #444444;--text: #111111;--page-bg: #eeeeee}*,*:before,*:after{margin:0;padding:0;box-sizing:border-box}html,body{height:100%}body{background:var(--page-bg);color:var(--text);font-family:var(--mono);font-size:13px;cursor:crosshair;overflow:hidden;transition:background .3s,color .3s}body:before{content:"";position:fixed;top:0;right:0;bottom:0;left:0;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,.03) 2px,rgba(0,0,0,.03) 4px);pointer-events:none;z-index:9999}button{cursor:crosshair;font-family:var(--mono)}input,textarea{font-family:var(--sans)}#app{height:100vh;display:flex;flex-direction:column}.topbar{display:flex;align-items:center;justify-content:space-between;padding:0 20px;height:40px;border-bottom:1px solid var(--border);flex-shrink:0;background:var(--bg)}.topbar-logo{font-size:14px;color:var(--text);letter-spacing:.1em}.theme-toggle{display:flex;align-items:center;gap:8px;font-size:10px;color:var(--text-muted);letter-spacing:.2em;background:none;border:none;padding:4px 8px;transition:color .2s}.theme-toggle:hover{color:var(--text)}.main-area{flex:1;display:flex;overflow:hidden}.sidebar{width:280px;flex-shrink:0;border-right:1px solid var(--border);display:flex;flex-direction:column;background:var(--bg);transition:background .3s,border-color .3s}.number-section{padding:16px 20px 12px;border-bottom:1px solid var(--border)}.section-label{font-size:10px;color:var(--border-strong);letter-spacing:.3em;margin-bottom:8px;display:flex;align-items:center;gap:8px}.section-label:after{content:"";flex:1;height:1px;background:var(--border)}.my-number{font-size:12px;color:var(--text);letter-spacing:.05em;margin-bottom:8px;word-break:break-all}.my-number.glitching{color:var(--text-muted)}.number-actions{display:flex;gap:6px;flex-wrap:wrap}.pill-btn{font-size:10px;color:var(--text-muted);border:1px solid var(--border-strong);background:transparent;padding:3px 8px;letter-spacing:.1em;transition:all .2s}.pill-btn:hover{background:var(--border-strong);color:var(--bg)}.inbox-section{flex:1;overflow-y:auto;padding:4px 0}.inbox-item{display:flex;align-items:center;padding:12px 20px;border-bottom:1px solid var(--border);cursor:crosshair;gap:12px;transition:background .15s;position:relative}.inbox-item:hover,.inbox-item.active{background:var(--surface)}.inbox-pin{font-size:11px;color:var(--text-sec);width:34px;flex-shrink:0}.inbox-info{flex:1;min-width:0}.inbox-label{font-family:var(--sans);font-size:12px;color:var(--text);font-weight:400;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.inbox-preview{font-family:var(--sans);font-size:11px;color:var(--text-muted);font-weight:300;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.inbox-meta{text-align:right;flex-shrink:0}.inbox-time{font-size:10px;color:var(--border-strong);margin-bottom:4px}.inbox-badge{width:16px;height:16px;border-radius:50%;background:var(--text);color:var(--bg);font-size:9px;display:flex;align-items:center;justify-content:center;margin-left:auto;transition:background .3s,color .3s}.inbox-badge[data-count="0"]{visibility:hidden}.peer-dot{width:6px;height:6px;border-radius:50%;background:var(--border-strong);flex-shrink:0;transition:background .3s}.peer-dot.connected{background:var(--text)}.new-pin-btn{margin:12px 20px;padding:10px;border:1px solid var(--border-strong);background:transparent;color:var(--text-muted);font-size:11px;letter-spacing:.2em;width:calc(100% - 40px);transition:all .2s;position:relative;overflow:hidden}.new-pin-btn:before{content:"";position:absolute;top:0;left:-100%;width:100%;height:100%;background:var(--text);transition:left .2s;z-index:-1}.new-pin-btn:hover:before{left:0}.new-pin-btn:hover{color:var(--bg);border-color:var(--text)}.panel{flex:1;display:flex;flex-direction:column;background:var(--bg);overflow:hidden;transition:background .3s}.welcome{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--border-strong);gap:12px;padding:40px;text-align:center}.welcome-logo{font-size:clamp(8px,1.5vw,14px);line-height:1.15;color:var(--border);animation:flicker 8s infinite}@keyframes flicker{0%,95%,to{opacity:1}96%{opacity:.4}97%{opacity:1}98%{opacity:.6}}.welcome-hint{font-size:11px;letter-spacing:.3em;color:var(--border-strong)}.chat-header{padding:12px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;flex-shrink:0}.chat-name{font-family:var(--sans);font-size:13px;font-weight:400;flex:1}.chat-pin-label{font-size:10px;color:var(--text-muted);letter-spacing:.2em}.chat-menu-btn{background:none;border:none;color:var(--text-muted);font-size:13px;letter-spacing:.1em;padding:4px;transition:color .2s}.chat-menu-btn:hover{color:var(--text)}.messages-area{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:10px}.msg{max-width:75%;display:flex;flex-direction:column}.msg.mine{align-self:flex-end;align-items:flex-end}.msg.theirs{align-self:flex-start;align-items:flex-start}.msg-bubble{font-family:var(--sans);font-size:13px;font-weight:300;line-height:1.6;padding:8px 12px;border:1px solid var(--border);border-radius:0;transition:border-color .3s,background .3s}.msg.mine .msg-bubble{border-color:var(--border-strong)}.msg.theirs .msg-bubble{background:var(--surface);color:var(--text-sec)}.msg-time{font-size:10px;color:var(--border-strong);margin-top:3px}.msg-status-queued{opacity:.5;font-style:italic}.msg-status-delivered{opacity:.7}.chat-input-area{padding:10px 16px 14px;border-top:1px solid var(--border);display:flex;gap:8px;align-items:flex-end;flex-shrink:0}.chat-input{flex:1;background:var(--surface);border:1px solid var(--border);color:var(--text);font-size:13px;font-weight:300;padding:8px 12px;resize:none;outline:none;min-height:38px;max-height:100px;transition:border-color .2s;line-height:1.5;border-radius:0}.chat-input:focus{border-color:var(--border-strong)}.chat-input::placeholder{color:var(--border-strong)}.send-btn{width:38px;height:38px;border:1px solid var(--border-strong);background:transparent;color:var(--text-muted);font-size:12px;flex-shrink:0;transition:all .2s}.send-btn:hover{border-color:var(--text);color:var(--text)}.newpin-panel{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:20px;max-width:480px}.pin-display{border:1px solid var(--border);padding:28px;text-align:center;position:relative}.pin-display:before{content:"YOUR NEW PIN";position:absolute;top:-.5rem;left:1rem;background:var(--bg);padding:0 6px;font-size:10px;color:var(--border-strong);letter-spacing:.3em}.pin-value{font-size:2.5rem;letter-spacing:.3em;color:var(--text);margin-bottom:6px}.pin-hint{font-family:var(--sans);font-size:11px;color:var(--text-muted);font-weight:300}.form-group{display:flex;flex-direction:column;gap:6px}.form-label{font-size:10px;color:var(--border-strong);letter-spacing:.3em}.form-input{background:var(--surface);border:1px solid var(--border);color:var(--text);font-size:13px;padding:10px 12px;outline:none;width:100%;transition:border-color .2s;border-radius:0}.form-input:focus{border-color:var(--border-strong)}.form-input::placeholder{color:var(--border-strong)}.expiry-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:2px;background:var(--border)}.expiry-opt{background:var(--bg);padding:10px 6px;font-size:11px;color:var(--text-muted);text-align:center;letter-spacing:.05em;border:none;transition:all .2s}.expiry-opt:hover,.expiry-opt.selected{background:var(--surface);color:var(--text)}.expiry-opt.selected{box-shadow:inset 0 0 0 1px var(--border-strong)}.primary-btn{font-size:12px;padding:12px;border:1px solid var(--text);background:transparent;color:var(--text);letter-spacing:.2em;transition:all .2s;position:relative;overflow:hidden;width:100%}.primary-btn:before{content:"";position:absolute;top:0;left:-100%;width:100%;height:100%;background:var(--text);transition:left .2s;z-index:-1}.primary-btn:hover:before{left:0}.primary-btn:hover{color:var(--bg)}.ghost-btn{font-size:12px;padding:12px;border:1px solid var(--border-strong);background:transparent;color:var(--text-muted);letter-spacing:.2em;transition:all .2s;width:100%}.ghost-btn:hover{border-color:var(--text);color:var(--text)}.menu-panel{flex:1;overflow-y:auto;max-width:480px}.menu-pin-header{padding:16px 24px;border-bottom:1px solid var(--border)}.menu-pin-name{font-family:var(--sans);font-size:14px;font-weight:400;margin-bottom:2px}.menu-pin-sub{font-size:10px;color:var(--text-muted);letter-spacing:.2em}.menu-section-label{font-size:10px;color:var(--border-strong);letter-spacing:.4em;padding:16px 24px 6px}.menu-item{padding:16px 24px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;cursor:crosshair;transition:background .15s}.menu-item:hover{background:var(--surface)}.menu-item-label{font-size:12px;letter-spacing:.05em;margin-bottom:2px}.menu-item-sub{font-family:var(--sans);font-size:11px;color:var(--text-muted);font-weight:300}.menu-item-arrow{font-size:12px;color:var(--border-strong)}.menu-item.danger .menu-item-label{color:var(--text-muted)}.menu-item.danger:hover .menu-item-label{color:var(--text)}::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:var(--bg)}::-webkit-scrollbar-thumb{background:var(--border-strong)}@media (max-width: 600px){.sidebar{width:100%;border-right:none}.main-area{flex-direction:column}.panel{display:none}.panel.active{display:flex}}
@@ -0,0 +1,176 @@
1
+ (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))s(a);new MutationObserver(a=>{for(const o of a)if(o.type==="childList")for(const m of o.addedNodes)m.tagName==="LINK"&&m.rel==="modulepreload"&&s(m)}).observe(document,{childList:!0,subtree:!0});function i(a){const o={};return a.integrity&&(o.integrity=a.integrity),a.referrerPolicy&&(o.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?o.credentials="include":a.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function s(a){if(a.ep)return;a.ep=!0;const o=i(a);fetch(a.href,o)}})();class B{constructor(){this.ws=null,this.handlers=new Map,this.reconnectTimer=null}connect(){this.ws=new WebSocket(`ws://localhost:${location.port||3e3}`),this.ws.onmessage=t=>{try{const i=JSON.parse(t.data);(this.handlers.get(i.event)||[]).forEach(a=>a(i))}catch{}},this.ws.onclose=()=>{this.reconnectTimer=setTimeout(()=>this.connect(),2e3)},this.ws.onerror=()=>{var t;(t=this.ws)==null||t.close()}}on(t,i){this.handlers.has(t)||this.handlers.set(t,[]),this.handlers.get(t).push(i)}send(t,i={}){var s;((s=this.ws)==null?void 0:s.readyState)===WebSocket.OPEN&&this.ws.send(JSON.stringify({cmd:t,...i}))}destroy(){var t;this.reconnectTimer&&clearTimeout(this.reconnectTimer),(t=this.ws)==null||t.close()}}const c=new B,e={number:"",inbox:[],contacts:[],activePin:null,activeContact:null,messages:{},contactMessages:{},peerStatus:{},theme:"dark",screen:"welcome",newPinValue:g(),newPinExpiry:"none"};function g(){return Math.floor(Math.random()*65536).toString(16).padStart(4,"0")}function d(){const n=document.getElementById("app");n.innerHTML=T(),K()}function T(){return`
2
+ <div class="topbar">
3
+ <span class="topbar-logo">0x0</span>
4
+ <button class="theme-toggle" id="theme-toggle">
5
+ // ${e.theme==="dark"?"dark":"light"}
6
+ </button>
7
+ </div>
8
+ <div class="main-area">
9
+ ${D()}
10
+ <div class="panel ${e.screen!=="welcome"?"active":""}">
11
+ ${A()}
12
+ </div>
13
+ </div>
14
+ `}function D(){const n=e.inbox,t=e.contacts;return`
15
+ <div class="sidebar">
16
+ <div class="number-section">
17
+ <div class="section-label">// my_number</div>
18
+ <div class="my-number" id="my-number">${e.number||"..."}</div>
19
+ <div class="number-actions">
20
+ <button class="pill-btn" id="btn-copy">// COPY</button>
21
+ <button class="pill-btn" id="btn-renew">// RENEW</button>
22
+ </div>
23
+ </div>
24
+ <div class="inbox-section">
25
+ <div style="padding: 12px 20px 6px">
26
+ <div class="section-label">// inbox</div>
27
+ </div>
28
+ ${n.map(i=>O(i)).join("")}
29
+ </div>
30
+ <button class="new-pin-btn" id="btn-new-pin">+ // NEW_PIN</button>
31
+ <div class="contacts-section">
32
+ <div style="padding: 12px 20px 6px; display:flex; justify-content:space-between; align-items:center">
33
+ <div class="section-label">// contacts</div>
34
+ <button class="pill-btn" id="btn-connect">+ CONNECT</button>
35
+ </div>
36
+ ${t.length===0?'<div style="padding:8px 20px; font-size:11px; opacity:0.4">// no contacts yet</div>':t.map(i=>_(i)).join("")}
37
+ </div>
38
+ </div>
39
+ `}function O(n){var o;const t=((o=e.activePin)==null?void 0:o.id)===n.id&&e.screen==="chat",i=e.peerStatus[n.id]||"disconnected",s=n.latest?L(n.latest.timestamp):"",a=n.latest?n.latest.content.slice(0,35)+(n.latest.content.length>35?"…":""):"(no messages)";return`
40
+ <div class="inbox-item ${t?"active":""}" data-pin-id="${n.id}">
41
+ <div class="peer-dot ${i==="connected"?"connected":""}"></div>
42
+ <div class="inbox-pin">${n.value}</div>
43
+ <div class="inbox-info">
44
+ <div class="inbox-label">${n.label||"(no label)"}</div>
45
+ <div class="inbox-preview">${a}</div>
46
+ </div>
47
+ <div class="inbox-meta">
48
+ <div class="inbox-time">${s}</div>
49
+ <div class="inbox-badge" data-count="${n.unread}">${n.unread||""}</div>
50
+ </div>
51
+ </div>
52
+ `}function _(n){var o;const t=((o=e.activeContact)==null?void 0:o.id)===n.id&&e.screen==="chat",i=e.peerStatus[n.id]||"disconnected",s=n.latest?L(n.latest.timestamp):"",a=n.latest?n.latest.content.slice(0,35)+(n.latest.content.length>35?"…":""):"(no messages)";return`
53
+ <div class="inbox-item ${t?"active":""}" data-contact-id="${n.id}">
54
+ <div class="peer-dot ${i==="connected"?"connected":""}"></div>
55
+ <div class="inbox-pin">${n.theirPin}</div>
56
+ <div class="inbox-info">
57
+ <div class="inbox-label">${n.label||n.theirNumber}</div>
58
+ <div class="inbox-preview">${k(a)}</div>
59
+ </div>
60
+ <div class="inbox-meta">
61
+ <div class="inbox-time">${s}</div>
62
+ <div class="inbox-badge" data-count="${n.unread}">${n.unread||""}</div>
63
+ </div>
64
+ </div>
65
+ `}function A(){switch(e.screen){case"welcome":return q();case"chat":return j();case"newPin":return V();case"pinMenu":return J();case"connect":return H()}}function q(){return`
66
+ <div class="welcome">
67
+ <pre class="welcome-logo">
68
+ ██████╗ ██╗ ██╗██████╗
69
+ ██╔═══██╗╚██╗██╔╝██╔══██╗
70
+ ██║ ██║ ╚███╔╝ ██║ ██║
71
+ ██║ ██║ ██╔██╗ ██║ ██║
72
+ ╚██████╔╝██╔╝ ██╗██████╔╝
73
+ ╚═════╝ ╚═╝ ╚═╝╚═════╝</pre>
74
+ <div class="welcome-hint">// select a pin from inbox to start chatting</div>
75
+ </div>
76
+ `}function j(){const n=e.activePin,t=e.activeContact,i=n?e.messages[n.id]||[]:t?e.contactMessages[t.id]||[]:[],s=n?n.id:(t==null?void 0:t.id)||"",a=e.peerStatus[s]||"disconnected",o=n?n.label||n.value:t?t.label||t.theirNumber:"",m=n?`// pin: ${n.value} &nbsp;·&nbsp; ${a}`:t?`// ${t.theirNumber} &nbsp;·&nbsp; pin: ${t.theirPin} &nbsp;·&nbsp; ${a}`:"";return`
77
+ <div style="display:flex;flex-direction:column;height:100%">
78
+ <div class="chat-header">
79
+ <div style="flex:1">
80
+ <div class="chat-name">${o}</div>
81
+ <div class="chat-pin-label">${m}</div>
82
+ </div>
83
+ ${n?'<button class="chat-menu-btn" id="btn-pin-menu">⋯</button>':'<button class="chat-menu-btn" id="btn-contact-remove" title="削除">✕</button>'}
84
+ </div>
85
+ <div class="messages-area" id="messages-area">
86
+ ${i.map(b=>W(b)).join("")}
87
+ </div>
88
+ <div class="chat-input-area">
89
+ <textarea
90
+ class="chat-input"
91
+ id="chat-input"
92
+ placeholder="メッセージ..."
93
+ rows="1"
94
+ ></textarea>
95
+ <button class="send-btn" id="btn-send">▶</button>
96
+ </div>
97
+ </div>
98
+ `}function W(n){const t=n.isMine?"mine":"theirs",i=new Date(n.timestamp).toLocaleTimeString("ja-JP",{hour:"2-digit",minute:"2-digit"});let s="";return n.isMine&&(n.status==="queued"?s='<span class="msg-status-queued"> // waiting…</span>':n.status==="delivered"&&(s='<span class="msg-status-delivered"> ✓</span>')),`
99
+ <div class="msg ${t}" data-local-id="${n.localId||""}">
100
+ <div class="msg-bubble">${k(n.content)}</div>
101
+ <div class="msg-time">${i}${s}</div>
102
+ </div>
103
+ `}function V(){const n=[{value:"none",label:"なし"},{value:"24h",label:"24時間"},{value:"1w",label:"1週間"},{value:"once",label:"1回のみ"}];return`
104
+ <div class="newpin-panel">
105
+ <div class="section-label">// new_pin</div>
106
+ <div class="pin-display">
107
+ <div class="pin-value" id="new-pin-display">${e.newPinValue}</div>
108
+ <div class="pin-hint">相手に渡すPINです</div>
109
+ </div>
110
+ <div class="form-group">
111
+ <div class="form-label">// label(任意)</div>
112
+ <input class="form-input" id="pin-label-input" type="text" placeholder="例: フリマ用、田中さん...">
113
+ </div>
114
+ <div class="form-group">
115
+ <div class="form-label">// expiry</div>
116
+ <div class="expiry-grid">
117
+ ${n.map(t=>`
118
+ <button class="expiry-opt ${e.newPinExpiry===t.value?"selected":""}"
119
+ data-expiry="${t.value}">${t.label}</button>
120
+ `).join("")}
121
+ </div>
122
+ </div>
123
+ <button class="ghost-btn" id="btn-regen-pin">// GENERATE_NEW →</button>
124
+ <button class="primary-btn" id="btn-save-pin">// SAVE_AND_USE →</button>
125
+ </div>
126
+ `}function H(){return`
127
+ <div class="newpin-panel">
128
+ <div class="section-label">// connect_to_peer</div>
129
+ <div class="form-group">
130
+ <div class="form-label">// their_number</div>
131
+ <input class="form-input" id="connect-number" type="text" placeholder="0x0-NNN-NNNN-NNNN" autocomplete="off">
132
+ </div>
133
+ <div class="form-group">
134
+ <div class="form-label">// pin</div>
135
+ <input class="form-input" id="connect-pin" type="text" placeholder="a3f9" maxlength="4" autocomplete="off">
136
+ </div>
137
+ <div class="form-group">
138
+ <div class="form-label">// label(任意)</div>
139
+ <input class="form-input" id="connect-label" type="text" placeholder="例: 田中さん">
140
+ </div>
141
+ <button class="primary-btn" id="btn-do-connect">// CONNECT →</button>
142
+ </div>
143
+ `}function J(){const n=e.activePin;return`
144
+ <div class="menu-panel">
145
+ <div class="menu-pin-header">
146
+ <div class="menu-pin-name">${n.label||n.value}</div>
147
+ <div class="menu-pin-sub">// pin: ${n.value}</div>
148
+ </div>
149
+ <div class="menu-section-label">// pin_settings</div>
150
+ <div class="menu-item" id="menu-rotate">
151
+ <div>
152
+ <div class="menu-item-label">PINを変更する</div>
153
+ <div class="menu-item-sub">新しいPINを生成して渡し直す</div>
154
+ </div>
155
+ <div class="menu-item-arrow">›</div>
156
+ </div>
157
+ <div class="menu-item" id="menu-label">
158
+ <div>
159
+ <div class="menu-item-label">ラベルを編集</div>
160
+ <div class="menu-item-sub">現在: ${n.label||"(なし)"}</div>
161
+ </div>
162
+ <div class="menu-item-arrow">›</div>
163
+ </div>
164
+ <div class="menu-section-label">// danger_zone</div>
165
+ <div class="menu-item danger" id="menu-revoke">
166
+ <div>
167
+ <div class="menu-item-label">このPINを今すぐ無効化</div>
168
+ <div class="menu-item-sub">相手からの受信が即時停止される</div>
169
+ </div>
170
+ <div class="menu-item-arrow">›</div>
171
+ </div>
172
+ <div style="padding: 16px 24px">
173
+ <button class="ghost-btn" id="menu-back">← back</button>
174
+ </div>
175
+ </div>
176
+ `}function K(){var i,s,a,o,m,b,h,f,y,I,x,w,E,P,$;(i=document.getElementById("theme-toggle"))==null||i.addEventListener("click",()=>{e.theme=e.theme==="dark"?"light":"dark",document.documentElement.classList.toggle("light",e.theme==="light"),d()}),(s=document.getElementById("btn-copy"))==null||s.addEventListener("click",()=>{navigator.clipboard.writeText(e.number).catch(()=>{})}),(a=document.getElementById("btn-renew"))==null||a.addEventListener("click",()=>{confirm("番号を再発行しますか?全てのPINが無効になります。")&&c.send("number.renew")}),(o=document.getElementById("btn-new-pin"))==null||o.addEventListener("click",()=>{e.screen="newPin",e.newPinValue=g(),d()}),(m=document.getElementById("btn-connect"))==null||m.addEventListener("click",()=>{e.screen="connect",e.activePin=null,e.activeContact=null,d()}),document.querySelectorAll(".inbox-item[data-pin-id]").forEach(l=>{l.addEventListener("click",()=>{const r=l.dataset.pinId,u=e.inbox.find(p=>p.id===r);u&&(e.activePin=u,e.activeContact=null,e.screen="chat",d(),c.send("messages.list",{pinId:r}),v())})}),document.querySelectorAll(".inbox-item[data-contact-id]").forEach(l=>{l.addEventListener("click",()=>{const r=l.dataset.contactId,u=e.contacts.find(p=>p.id===r);u&&(e.activeContact=u,e.activePin=null,e.screen="chat",d(),c.send("chat.start",{theirNumber:u.theirNumber,theirPin:u.theirPin,label:u.label}),v())})}),(b=document.getElementById("btn-pin-menu"))==null||b.addEventListener("click",()=>{e.screen="pinMenu",d()}),(h=document.getElementById("btn-contact-remove"))==null||h.addEventListener("click",()=>{e.activeContact&&confirm(`${e.activeContact.label||e.activeContact.theirNumber} を削除しますか?`)&&(c.send("contact.remove",{contactId:e.activeContact.id}),e.activeContact=null,e.screen="welcome",d())});const n=document.getElementById("chat-input"),t=()=>{const l=n==null?void 0:n.value.trim();if(l){if(e.activePin){const r=e.activePin.id,u=crypto.randomUUID(),p=Date.now();e.messages[r]||(e.messages[r]=[]),e.messages[r].push({localId:u,content:l,isMine:!0,timestamp:p,status:"queued"}),c.send("message.send",{pinId:r,content:l,localId:u}),d(),v()}else e.activeContact&&c.send("contact.message.send",{contactId:e.activeContact.id,content:l});n.value="",n.style.height="auto"}};(f=document.getElementById("btn-send"))==null||f.addEventListener("click",t),n==null||n.addEventListener("keydown",l=>{l.key==="Enter"&&!l.shiftKey&&(l.preventDefault(),t())}),n==null||n.addEventListener("input",()=>{n&&(n.style.height="auto",n.style.height=Math.min(n.scrollHeight,100)+"px")}),document.querySelectorAll(".expiry-opt").forEach(l=>{l.addEventListener("click",()=>{e.newPinExpiry=l.dataset.expiry||"none",d()})}),(y=document.getElementById("btn-regen-pin"))==null||y.addEventListener("click",()=>{e.newPinValue=g();const l=document.getElementById("new-pin-display");l&&S(l,e.newPinValue)}),(I=document.getElementById("btn-save-pin"))==null||I.addEventListener("click",()=>{var r;const l=((r=document.getElementById("pin-label-input"))==null?void 0:r.value.trim())||"";c.send("pin.create",{label:l,expiry:e.newPinExpiry}),e.screen="welcome",e.newPinExpiry="none",d()}),(x=document.getElementById("btn-do-connect"))==null||x.addEventListener("click",()=>{var p,N,C;const l=(p=document.getElementById("connect-number"))==null?void 0:p.value.trim(),r=(N=document.getElementById("connect-pin"))==null?void 0:N.value.trim(),u=((C=document.getElementById("connect-label"))==null?void 0:C.value.trim())||"";!l||!r||c.send("chat.start",{theirNumber:l,theirPin:r,label:u})}),(w=document.getElementById("menu-rotate"))==null||w.addEventListener("click",()=>{e.activePin&&(c.send("pin.rotate",{pinId:e.activePin.id}),e.screen="welcome",e.activePin=null,d())}),(E=document.getElementById("menu-revoke"))==null||E.addEventListener("click",()=>{e.activePin&&(c.send("pin.revoke",{pinId:e.activePin.id}),e.screen="welcome",e.activePin=null,d())}),(P=document.getElementById("menu-back"))==null||P.addEventListener("click",()=>{e.screen="chat",d()}),($=document.getElementById("menu-label"))==null||$.addEventListener("click",()=>{var l;prompt("新しいラベル:",((l=e.activePin)==null?void 0:l.label)||"")})}c.on("init",n=>{var i;const t=n;e.number=t.data.number,e.inbox=t.data.inbox,e.contacts=t.data.contacts||[],((i=t.data.prefs)==null?void 0:i.theme)==="light"&&(e.theme="light",document.documentElement.classList.add("light")),d(),M()});c.on("inbox.list",n=>{const t=n;if(e.inbox=t.data,e.activePin){const i=t.data.find(s=>s.id===e.activePin.id);i&&(e.activePin=i)}d()});c.on("contacts.list",n=>{const t=n;if(e.contacts=t.data,e.activeContact){const i=t.data.find(s=>s.id===e.activeContact.id);i&&(e.activeContact=i)}d()});c.on("messages.list",n=>{var i;const t=n;e.messages[t.pinId]=t.data,e.screen==="chat"&&((i=e.activePin)==null?void 0:i.id)===t.pinId&&(d(),v())});c.on("chat.started",n=>{const t=n,i=e.contacts.findIndex(s=>s.id===t.contactId);i===-1?e.contacts.push(t.data):e.contacts[i]={...e.contacts[i],...t.data},e.activeContact=e.contacts.find(s=>s.id===t.contactId)||t.data,e.activePin=null,e.screen="chat",d(),v()});c.on("contact.messages.list",n=>{var i;const t=n;e.contactMessages[t.contactId]=t.data,e.screen==="chat"&&((i=e.activeContact)==null?void 0:i.id)===t.contactId&&(d(),v())});c.on("message.received",n=>{var s;const t=n;e.messages[t.pinId]||(e.messages[t.pinId]=[]),e.messages[t.pinId].push(t.data);const i=e.inbox.find(a=>a.id===t.pinId);i&&(i.unread++,i.latest=t.data),e.screen==="chat"&&((s=e.activePin)==null?void 0:s.id)===t.pinId?(d(),v()):d()});c.on("contact.message.received",n=>{var s;const t=n;e.contactMessages[t.contactId]||(e.contactMessages[t.contactId]=[]),e.contactMessages[t.contactId].push(t.data);const i=e.contacts.find(a=>a.id===t.contactId);i&&(i.unread++,i.latest=t.data),e.screen==="chat"&&((s=e.activeContact)==null?void 0:s.id)===t.contactId?(d(),v()):d()});c.on("message.sent",n=>{var s;const t=n,i=e.messages[t.pinId]||[];if(t.localId){const a=i.findIndex(o=>o.localId===t.localId);a!==-1&&(i[a]={...i[a],status:"delivered"},e.messages[t.pinId]=i)}else e.messages[t.pinId]||(e.messages[t.pinId]=[]),e.messages[t.pinId].push(t.data);e.screen==="chat"&&((s=e.activePin)==null?void 0:s.id)===t.pinId&&(d(),v())});c.on("message.queued",n=>{});c.on("message.delivered",n=>{var a;const t=n,i=e.messages[t.pinId]||[],s=i.findIndex(o=>o.localId===t.localId);s!==-1&&(i[s]={...i[s],status:"delivered"},e.messages[t.pinId]=i,e.screen==="chat"&&((a=e.activePin)==null?void 0:a.id)===t.pinId&&d())});c.on("contact.message.sent",n=>{var i;const t=n;e.contactMessages[t.contactId]||(e.contactMessages[t.contactId]=[]),e.contactMessages[t.contactId].push(t.data),e.screen==="chat"&&((i=e.activeContact)==null?void 0:i.id)===t.contactId&&(d(),v())});c.on("peer.status",n=>{const t=n,i=t.pinId||t.contactId||"";i&&(e.peerStatus[i]=t.status),d()});c.on("pin.created",()=>{c.send("inbox.list")});c.on("pin.rotated",()=>{c.send("inbox.list")});c.on("pin.revoked",()=>{c.send("inbox.list")});c.on("contact.removed",n=>{const t=n;e.contacts=e.contacts.filter(i=>i.id!==t.contactId),delete e.contactMessages[t.contactId],delete e.peerStatus[t.contactId],d()});c.on("number.renewed",n=>{const t=n;e.number=t.data.number,d(),M()});function v(){setTimeout(()=>{const n=document.getElementById("messages-area");n&&(n.scrollTop=n.scrollHeight)},0)}function L(n){const t=new Date(n),i=new Date;if(t.toDateString()===i.toDateString())return t.toLocaleTimeString("ja-JP",{hour:"2-digit",minute:"2-digit"});const s=new Date(i);return s.setDate(s.getDate()-1),t.toDateString()===s.toDateString()?"昨日":t.toLocaleDateString("ja-JP",{month:"numeric",day:"numeric"})}function k(n){return n.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;").replace(/\n/g,"<br>")}function S(n,t){const i="0123456789abcdef";let s=0;const a=setInterval(()=>{let o="";for(const m of t)o+=Math.random()<.3?i[Math.floor(Math.random()*i.length)]:m;n.textContent=o,++s>6&&(clearInterval(a),n.textContent=t)},70)}function M(){const n=document.getElementById("my-number");n&&e.number&&S(n,e.number)}c.connect();d();
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>0x0</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Noto+Sans+JP:wght@300;400&display=swap" rel="stylesheet">
9
+ <script type="module" crossorigin src="/assets/index-Ci89CtgQ.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-BrxSM83h.css">
11
+ </head>
12
+ <body>
13
+ <div id="app"></div>
14
+ </body>
15
+ </html>
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "0x0-cli",
3
+ "version": "1.0.0",
4
+ "description": "P2P disposable number messenger",
5
+ "type": "module",
6
+ "bin": {
7
+ "0x0": "./bin/0x0.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "dist/web/"
13
+ ],
14
+ "scripts": {
15
+ "start": "node bin/0x0.js",
16
+ "build:web": "vite build web-ui",
17
+ "dev:web": "vite web-ui"
18
+ },
19
+ "dependencies": {
20
+ "chalk": "^5.3.0",
21
+ "commander": "^12.0.0",
22
+ "express": "^4.18.0",
23
+ "hyperswarm": "^4.7.0",
24
+ "open": "^10.1.0",
25
+ "ora": "^8.0.1",
26
+ "qrcode-terminal": "^0.12.0",
27
+ "uuid": "^9.0.0",
28
+ "ws": "^8.16.0"
29
+ },
30
+ "devDependencies": {
31
+ "typescript": "^5.3.0",
32
+ "vite": "^5.1.0"
33
+ },
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ }
37
+ }
@@ -0,0 +1,204 @@
1
+ import readline from 'readline'
2
+ import chalk from 'chalk'
3
+ import ora from 'ora'
4
+ import Hyperswarm from 'hyperswarm'
5
+ import { channelSecret } from '../core/channel.js'
6
+ import { createMessage, parseMessage } from '../core/message.js'
7
+ import { generatePin } from '../core/pin.js'
8
+ import * as identityStore from '../storage/identity.js'
9
+ import * as pinsStore from '../storage/pins.js'
10
+ import * as messagesStore from '../storage/messages.js'
11
+ import * as contactsStore from '../storage/contacts.js'
12
+
13
+ export async function cmdChat(theirNumber, pin) {
14
+ const identity = identityStore.load()
15
+ if (!identity) {
16
+ console.log(chalk.gray('// not initialized. run: 0x0 init'))
17
+ process.exit(1)
18
+ }
19
+
20
+ const localPin = pinsStore.findByValue(pin)
21
+ const swarm = new Hyperswarm()
22
+ const topic = channelSecret(theirNumber, pin)
23
+ let activeConn = null
24
+ let rl = null
25
+
26
+ const spinner = ora({
27
+ text: chalk.gray(`connecting to ${theirNumber}...`),
28
+ color: 'white'
29
+ }).start()
30
+
31
+ swarm.join(topic, { server: true, client: true })
32
+
33
+ swarm.on('connection', (conn) => {
34
+ if (activeConn) return // 既に接続済みなら無視
35
+ activeConn = conn
36
+
37
+ // 公開鍵で連絡先を自動識別・保存
38
+ const pubKeyHex = conn.remotePublicKey?.toString('hex') ?? null
39
+ if (pubKeyHex) {
40
+ let c = contactsStore.findByPublicKey(pubKeyHex)
41
+ if (!c) {
42
+ c = contactsStore.create({ theirNumber, theirPin: pin, peerPublicKey: pubKeyHex })
43
+ } else if (!c.peerPublicKey) {
44
+ contactsStore.updatePublicKey(c.id, pubKeyHex)
45
+ }
46
+ }
47
+
48
+ spinner.stop()
49
+ printHeader(pin)
50
+
51
+ conn.on('error', () => {})
52
+ conn.on('data', (data) => {
53
+ const msg = parseMessage(data)
54
+ if (!msg || msg.type !== 'message') return
55
+
56
+ const time = new Date().toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit' })
57
+ // readline の入力行をクリアして上書き表示
58
+ if (rl) process.stdout.write('\r\x1b[K')
59
+ console.log(
60
+ chalk.gray(`[${time}]`) + ' ' +
61
+ chalk.hex('#888888')(theirNumber.slice(0, 16) + '…: ') +
62
+ chalk.hex('#aaaaaa')(msg.content)
63
+ )
64
+ if (rl) rl.prompt(true)
65
+
66
+ if (localPin) {
67
+ messagesStore.append(localPin.value, {
68
+ from: theirNumber, content: msg.content, isMine: false
69
+ })
70
+ }
71
+ })
72
+
73
+ conn.on('close', () => {
74
+ console.log()
75
+ console.log(chalk.gray('[peer disconnected]'))
76
+ activeConn = null
77
+ })
78
+
79
+ if (!rl) rl = startRepl()
80
+ })
81
+
82
+ // 3秒後もつながっていなければ待機状態でREPLを開始
83
+ setTimeout(() => {
84
+ if (!activeConn) {
85
+ spinner.stop()
86
+ console.log(chalk.gray('[waiting for peer...]'))
87
+ printHeader(pin)
88
+ if (!rl) rl = startRepl()
89
+ }
90
+ }, 3000)
91
+
92
+ function printHeader(p) {
93
+ console.log()
94
+ console.log(chalk.gray(`// pin: ${p} | type :help for commands`))
95
+ console.log()
96
+ }
97
+
98
+ function startRepl() {
99
+ const r = readline.createInterface({
100
+ input: process.stdin,
101
+ output: process.stdout,
102
+ prompt: chalk.gray('> ')
103
+ })
104
+
105
+ r.prompt()
106
+
107
+ r.on('line', async (line) => {
108
+ const input = line.trim()
109
+ if (!input) { r.prompt(); return }
110
+
111
+ if (input.startsWith(':')) {
112
+ await handleCommand(input.slice(1).trim(), r)
113
+ r.prompt()
114
+ return
115
+ }
116
+
117
+ if (!activeConn) {
118
+ console.log(chalk.gray('// not connected yet'))
119
+ r.prompt()
120
+ return
121
+ }
122
+
123
+ activeConn.write(createMessage(input))
124
+
125
+ const time = new Date().toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit' })
126
+ process.stdout.write('\x1b[1A\x1b[K')
127
+ console.log(chalk.gray(`[${time}]`) + chalk.gray(' you: ') + chalk.white(input))
128
+
129
+ if (localPin) {
130
+ messagesStore.append(localPin.value, {
131
+ from: identity.number, content: input, isMine: true
132
+ })
133
+ }
134
+
135
+ r.prompt()
136
+ })
137
+
138
+ r.on('close', async () => {
139
+ console.log(chalk.gray('[disconnected]'))
140
+ await swarm.destroy()
141
+ process.exit(0)
142
+ })
143
+
144
+ return r
145
+ }
146
+
147
+ async function handleCommand(cmd, r) {
148
+ switch (cmd) {
149
+ case 'help':
150
+ console.log(chalk.gray(':pin rotate — change pin and get new one'))
151
+ console.log(chalk.gray(':pin info — show current pin info'))
152
+ console.log(chalk.gray(':history — show message history'))
153
+ console.log(chalk.gray(':clear — clear screen'))
154
+ console.log(chalk.gray(':quit / :q — disconnect and exit'))
155
+ break
156
+
157
+ case 'pin rotate':
158
+ if (localPin) {
159
+ const newValue = generatePin()
160
+ pinsStore.rotate(localPin.id, newValue)
161
+ console.log(chalk.gray('rotating pin...'))
162
+ console.log(chalk.gray('new pin: ') + chalk.hex('#aaaaaa')(newValue))
163
+ console.log(chalk.gray('// share the new pin with your contact to continue'))
164
+ } else {
165
+ console.log(chalk.gray('// pin not found in your list'))
166
+ }
167
+ break
168
+
169
+ case 'pin info':
170
+ console.log(chalk.gray(`pin: ${pin}`))
171
+ if (localPin) console.log(chalk.gray(`label: ${localPin.label || '(none)'}`))
172
+ break
173
+
174
+ case 'history':
175
+ if (localPin) {
176
+ const msgs = messagesStore.list(localPin.value)
177
+ if (msgs.length === 0) {
178
+ console.log(chalk.gray('(no history)'))
179
+ } else {
180
+ for (const msg of msgs) {
181
+ const t = new Date(msg.timestamp).toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit' })
182
+ const prefix = msg.isMine
183
+ ? chalk.gray(`[${t}] you: `)
184
+ : chalk.gray(`[${t}] them: `)
185
+ console.log(prefix + chalk.white(msg.content))
186
+ }
187
+ }
188
+ }
189
+ break
190
+
191
+ case 'clear':
192
+ process.stdout.write('\x1b[2J\x1b[0f')
193
+ break
194
+
195
+ case 'quit':
196
+ case 'q':
197
+ r.close()
198
+ break
199
+
200
+ default:
201
+ console.log(chalk.gray(`// unknown command: :${cmd}`))
202
+ }
203
+ }
204
+ }