pikiloom 0.4.21 → 0.4.23
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/dashboard/dist/assets/{AgentTab-Crg9GEUv.js → AgentTab-hzdSX7gP.js} +1 -1
- package/dashboard/dist/assets/ConnectionModal-Dj2eoHWz.js +1 -0
- package/dashboard/dist/assets/{DirBrowser-Do4XzF2X.js → DirBrowser-C4DPPZ1V.js} +1 -1
- package/dashboard/dist/assets/ExtensionsTab-BdGHiEnP.js +1 -0
- package/dashboard/dist/assets/{IMAccessTab-BX90Aiuq.js → IMAccessTab-C4RlXeCP.js} +1 -1
- package/dashboard/dist/assets/{Modal-Dd5twuoE.js → Modal-D22Z00Ux.js} +1 -1
- package/dashboard/dist/assets/{Modals--Xl6dsUf.js → Modals-CHXLG4V2.js} +1 -1
- package/dashboard/dist/assets/{Select-Bp6SwWv6.js → Select-D4JIewGZ.js} +1 -1
- package/dashboard/dist/assets/{SessionPanel-CHaOUor-.js → SessionPanel-Ba93D9jh.js} +1 -1
- package/dashboard/dist/assets/{SystemTab-MVpMgFFD.js → SystemTab-M8r-o2KI.js} +1 -1
- package/dashboard/dist/assets/index-C02W4e24.js +3 -0
- package/dashboard/dist/assets/index-CyCspgtc.css +1 -0
- package/dashboard/dist/assets/{index-7pyJSn4B.js → index-p2XCgnzw.js} +17 -17
- package/dashboard/dist/assets/{shared-CxR9M3rn.js → shared-DW8iXiGd.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/index.js +1 -1
- package/dist/agent/skill-installer.js +95 -0
- package/dist/catalog/skill-repos.js +12 -0
- package/dist/cli/main.js +31 -1
- package/dist/core/secrets/ref.js +8 -0
- package/dist/core/secrets/store.js +31 -11
- package/dist/dashboard/routes/extensions.js +137 -24
- package/dist/dashboard/server.js +27 -64
- package/dist/pikichannel/adapter-pikiloom.js +220 -0
- package/dist/pikichannel/code.js +35 -0
- package/dist/pikichannel/codec.js +50 -0
- package/dist/pikichannel/host.js +252 -0
- package/dist/pikichannel/protocol.js +105 -0
- package/dist/pikichannel/rendezvous-broker.js +114 -0
- package/dist/pikichannel/rendezvous-host.js +138 -0
- package/dist/pikichannel/server.js +284 -0
- package/dist/pikichannel/transport.js +49 -0
- package/dist/pikichannel/transports/webrtc-host.js +61 -0
- package/dist/pikichannel/transports/webrtc-shared.js +132 -0
- package/dist/pikichannel/transports/websocket-host.js +78 -0
- package/dist/pikichannel/web/demo.html +246 -0
- package/dist/pikichannel/web/sdk.js +361 -0
- package/package.json +4 -2
- package/dashboard/dist/assets/ExtensionsTab-j77Yqoz1.js +0 -1
- package/dashboard/dist/assets/index-BiPI4uFE.js +0 -3
- package/dashboard/dist/assets/index-dzfjF9Js.css +0 -1
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>pikichannel · pluggable transport demo</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0b0f14; --panel: #131922; --panel2: #0e141c; --line: #1f2733;
|
|
10
|
+
--ink: #e6edf3; --dim: #8b97a7; --accent: #29d3c2; --accent2: #3b82f6;
|
|
11
|
+
--warn: #f0883e; --bad: #f85149; --good: #3fb950;
|
|
12
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
13
|
+
}
|
|
14
|
+
* { box-sizing: border-box; }
|
|
15
|
+
body { margin: 0; background: var(--bg); color: var(--ink); height: 100vh; display: flex; flex-direction: column; font-size: 13px; }
|
|
16
|
+
header { padding: 10px 16px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
|
|
17
|
+
.brand { font-weight: 700; color: var(--accent); letter-spacing: .5px; }
|
|
18
|
+
.brand small { color: var(--dim); font-weight: 400; margin-left: 8px; }
|
|
19
|
+
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
|
|
20
|
+
.seg button { background: var(--panel); color: var(--dim); border: 0; padding: 6px 12px; cursor: pointer; font: inherit; }
|
|
21
|
+
.seg button.on { background: var(--accent); color: #04121a; font-weight: 700; }
|
|
22
|
+
.btn { background: var(--panel); color: var(--ink); border: 1px solid var(--line); border-radius: 8px; padding: 6px 14px; cursor: pointer; font: inherit; }
|
|
23
|
+
.btn:hover { border-color: var(--accent); }
|
|
24
|
+
.btn.primary { background: var(--accent2); border-color: var(--accent2); color: #fff; }
|
|
25
|
+
.stats { margin-left: auto; display: flex; gap: 14px; align-items: center; color: var(--dim); flex-wrap: wrap; }
|
|
26
|
+
.stat b { color: var(--ink); font-weight: 600; }
|
|
27
|
+
.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--dim); display: inline-block; }
|
|
28
|
+
.dot.open { background: var(--good); } .dot.connecting { background: var(--warn); } .dot.closed { background: var(--bad); }
|
|
29
|
+
main { flex: 1; display: grid; grid-template-columns: 250px 1fr; min-height: 0; }
|
|
30
|
+
aside { border-right: 1px solid var(--line); overflow-y: auto; background: var(--panel2); }
|
|
31
|
+
.sess { padding: 10px 12px; border-bottom: 1px solid var(--line); cursor: pointer; }
|
|
32
|
+
.sess:hover { background: var(--panel); } .sess.sel { background: var(--panel); border-left: 3px solid var(--accent); }
|
|
33
|
+
.sess .k { color: var(--dim); font-size: 11px; } .sess .t { margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
34
|
+
.badge { font-size: 10px; padding: 1px 6px; border-radius: 6px; background: var(--line); color: var(--dim); }
|
|
35
|
+
.badge.streaming { background: #10303a; color: var(--accent); } .badge.done { background: #16301c; color: var(--good); } .badge.queued { background: #2a2410; color: var(--warn); }
|
|
36
|
+
section.view { display: flex; flex-direction: column; min-height: 0; }
|
|
37
|
+
.scroll { flex: 1; overflow-y: auto; padding: 16px; }
|
|
38
|
+
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; margin-bottom: 12px; }
|
|
39
|
+
.card h4 { margin: 0 0 8px; color: var(--dim); font-size: 11px; text-transform: uppercase; letter-spacing: .6px; }
|
|
40
|
+
.meta { display: flex; gap: 14px; color: var(--dim); flex-wrap: wrap; margin-bottom: 10px; }
|
|
41
|
+
.meta b { color: var(--ink); }
|
|
42
|
+
pre { white-space: pre-wrap; word-break: break-word; margin: 0; font-family: inherit; line-height: 1.5; }
|
|
43
|
+
.prompt { color: var(--accent2); }
|
|
44
|
+
.reason { color: var(--dim); } details summary { cursor: pointer; color: var(--dim); }
|
|
45
|
+
.step { padding: 2px 0; } .step.completed { color: var(--good); } .step.inProgress { color: var(--accent); } .step.pending { color: var(--dim); }
|
|
46
|
+
.tool { border-left: 2px solid var(--line); padding: 2px 0 2px 8px; margin: 3px 0; color: var(--dim); }
|
|
47
|
+
.tool b { color: var(--ink); }
|
|
48
|
+
.interaction { border: 1px solid var(--accent); border-radius: 8px; padding: 10px; margin-bottom: 10px; }
|
|
49
|
+
.choice { display: inline-block; margin: 4px 6px 0 0; }
|
|
50
|
+
footer { border-top: 1px solid var(--line); padding: 10px 16px; display: flex; gap: 10px; align-items: flex-end; }
|
|
51
|
+
textarea { flex: 1; background: var(--panel2); color: var(--ink); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; font: inherit; resize: none; height: 46px; }
|
|
52
|
+
select, input { background: var(--panel2); color: var(--ink); border: 1px solid var(--line); border-radius: 8px; padding: 8px; font: inherit; }
|
|
53
|
+
.empty { color: var(--dim); text-align: center; margin-top: 60px; }
|
|
54
|
+
a { color: var(--accent); }
|
|
55
|
+
</style>
|
|
56
|
+
</head>
|
|
57
|
+
<body>
|
|
58
|
+
<header>
|
|
59
|
+
<div class="brand">pikichannel <small>pluggable agent transport — live demo</small></div>
|
|
60
|
+
<div class="seg" id="seg">
|
|
61
|
+
<button data-k="websocket" class="on">WebSocket</button>
|
|
62
|
+
<button data-k="webrtc">WebRTC (P2P)</button>
|
|
63
|
+
</div>
|
|
64
|
+
<input id="endpoint" placeholder="host (blank = this machine)" title="Direct target, e.g. 192.168.1.5:3940 — for a reachable host." style="width:130px" />
|
|
65
|
+
<input id="rdv" placeholder="rendezvous (NAT)" title="Broker ws(s):// URL for NAT traversal, e.g. wss://broker/pikichannel/rendezvous" style="width:140px" />
|
|
66
|
+
<input id="node" placeholder="nodeId (NAT)" title="The host's NodeID to dial through the rendezvous." style="width:110px" />
|
|
67
|
+
<input id="token" placeholder="token (remote)" title="Access token — required for remote/NAT clients. Get it from the local dashboard / GET /pikichannel/pair." style="width:120px" />
|
|
68
|
+
<button class="btn primary" id="connect">Connect</button>
|
|
69
|
+
<button class="btn" id="disconnect">Disconnect</button>
|
|
70
|
+
<div class="stats">
|
|
71
|
+
<span class="stat"><span class="dot" id="dot"></span> <b id="state">idle</b></span>
|
|
72
|
+
<span class="stat">via <b id="via">—</b></span>
|
|
73
|
+
<span class="stat">RTT <b id="rtt">—</b></span>
|
|
74
|
+
<span class="stat">frames <b id="frames">0/0</b></span>
|
|
75
|
+
<span class="stat">bytes <b id="bytes">0/0</b></span>
|
|
76
|
+
<span class="stat">host <b id="host">—</b></span>
|
|
77
|
+
</div>
|
|
78
|
+
</header>
|
|
79
|
+
<main>
|
|
80
|
+
<aside id="sessions"></aside>
|
|
81
|
+
<section class="view">
|
|
82
|
+
<div class="scroll" id="view"><div class="empty">Connect, then send a prompt to start a session.<br/>Flip the transport toggle and reconnect to compare WebSocket vs WebRTC carrying the identical protocol.</div></div>
|
|
83
|
+
<footer>
|
|
84
|
+
<select id="agent" title="agent (blank = host default)">
|
|
85
|
+
<option value="">default agent</option>
|
|
86
|
+
<option value="claude">claude</option>
|
|
87
|
+
<option value="codex">codex</option>
|
|
88
|
+
<option value="gemini">gemini</option>
|
|
89
|
+
<option value="hermes">hermes</option>
|
|
90
|
+
</select>
|
|
91
|
+
<textarea id="composer" placeholder="Type a prompt — Enter to send to a NEW session (or the selected one), Shift+Enter for newline"></textarea>
|
|
92
|
+
<button class="btn primary" id="send">Send</button>
|
|
93
|
+
<button class="btn" id="stop">Stop</button>
|
|
94
|
+
<button class="btn" id="tunnel" title="Call /api/agents on the connected host over the channel (control-plane tunnel)">Tunnel /api/agents</button>
|
|
95
|
+
</footer>
|
|
96
|
+
</section>
|
|
97
|
+
</main>
|
|
98
|
+
|
|
99
|
+
<script type="module">
|
|
100
|
+
import '/pikichannel/sdk.js';
|
|
101
|
+
|
|
102
|
+
const $ = (id) => document.getElementById(id);
|
|
103
|
+
let client = null;
|
|
104
|
+
let kind = 'websocket';
|
|
105
|
+
let selected = null;
|
|
106
|
+
|
|
107
|
+
// transport toggle
|
|
108
|
+
$('seg').addEventListener('click', (e) => {
|
|
109
|
+
const b = e.target.closest('button'); if (!b) return;
|
|
110
|
+
kind = b.dataset.k;
|
|
111
|
+
for (const x of $('seg').children) x.classList.toggle('on', x === b);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
$('connect').onclick = async () => {
|
|
115
|
+
if (client) client.disconnect();
|
|
116
|
+
client = new window.Pikichannel.Client({
|
|
117
|
+
transport: kind,
|
|
118
|
+
host: $('endpoint').value.trim() || undefined,
|
|
119
|
+
rendezvous: $('rdv').value.trim() || undefined,
|
|
120
|
+
nodeId: $('node').value.trim() || undefined,
|
|
121
|
+
token: $('token').value.trim() || undefined,
|
|
122
|
+
});
|
|
123
|
+
wire(client);
|
|
124
|
+
try { await client.connect(); client.subscribeAll(); }
|
|
125
|
+
catch (err) { /* status reflects failure */ }
|
|
126
|
+
};
|
|
127
|
+
$('disconnect').onclick = () => { if (client) client.disconnect(); };
|
|
128
|
+
|
|
129
|
+
// Control-plane tunnel demo: manage the connected host over the channel.
|
|
130
|
+
$('tunnel').onclick = async () => {
|
|
131
|
+
if (!client || client.state !== 'open') { alert('Not connected'); return; }
|
|
132
|
+
try {
|
|
133
|
+
const res = await client.request('GET', '/api/agents');
|
|
134
|
+
const data = res.json();
|
|
135
|
+
const names = (data.agents || []).map((a) => a.agent + (a.installed ? '' : '(missing)')).join(', ');
|
|
136
|
+
$('view').innerHTML = '';
|
|
137
|
+
$('view').appendChild(card('Control-plane tunnel · GET /api/agents (via channel, no REST exposure)',
|
|
138
|
+
preEl('status ' + res.status + '\nagents: ' + (names || '(none)'))));
|
|
139
|
+
} catch (e) { alert('tunnel failed: ' + e.message); }
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
$('send').onclick = sendPrompt;
|
|
143
|
+
$('composer').addEventListener('keydown', (e) => {
|
|
144
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendPrompt(); }
|
|
145
|
+
});
|
|
146
|
+
$('stop').onclick = () => { if (client && selected) client.stop(selected); };
|
|
147
|
+
|
|
148
|
+
function sendPrompt() {
|
|
149
|
+
if (!client || client.state !== 'open') { alert('Not connected'); return; }
|
|
150
|
+
const text = $('composer').value.trim();
|
|
151
|
+
if (!text) return;
|
|
152
|
+
client.prompt({ prompt: text, sessionKey: selected || undefined, agent: $('agent').value || undefined });
|
|
153
|
+
$('composer').value = '';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function wire(c) {
|
|
157
|
+
c.on('status', renderStats);
|
|
158
|
+
c.on('welcome', (host) => { $('host').textContent = (host && host.name + ' ' + host.version) || '—'; renderSessions(); });
|
|
159
|
+
c.on('sessions', renderSessions);
|
|
160
|
+
c.on('session', (snap) => { if (!selected) selected = snap.sessionKey; renderSessions(); if (snap.sessionKey === selected) renderView(snap); });
|
|
161
|
+
c.on('accepted', (a) => { selected = a.sessionKey; renderSessions(); });
|
|
162
|
+
c.on('error', (e) => { console.warn('pikichannel error', e); });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function renderStats() {
|
|
166
|
+
if (!client) return;
|
|
167
|
+
const s = client.stats();
|
|
168
|
+
$('state').textContent = s.state;
|
|
169
|
+
$('dot').className = 'dot ' + s.state;
|
|
170
|
+
$('via').textContent = (s.host && s.host.transport) || s.transport;
|
|
171
|
+
$('rtt').textContent = s.rtt == null ? '—' : (s.rtt + ' ms');
|
|
172
|
+
$('frames').textContent = s.framesIn + '/' + s.framesOut;
|
|
173
|
+
$('bytes').textContent = s.bytesIn + '/' + s.bytesOut;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function renderSessions() {
|
|
177
|
+
if (!client) return;
|
|
178
|
+
const el = $('sessions');
|
|
179
|
+
const keys = Array.from(client.sessions.keys());
|
|
180
|
+
if (!keys.length) { el.innerHTML = '<div class="empty" style="margin-top:20px">no active sessions</div>'; return; }
|
|
181
|
+
el.innerHTML = '';
|
|
182
|
+
for (const k of keys) {
|
|
183
|
+
const snap = client.sessions.get(k);
|
|
184
|
+
const div = document.createElement('div');
|
|
185
|
+
div.className = 'sess' + (k === selected ? ' sel' : '');
|
|
186
|
+
div.onclick = () => { selected = k; renderSessions(); renderView(client.sessions.get(k)); };
|
|
187
|
+
const phase = (snap && snap.phase) || 'idle';
|
|
188
|
+
div.innerHTML = '<div class="k">' + esc(k) + '</div><div class="t">' + esc((snap && (snap.prompt || snap.text)) || '(new)') + '</div>';
|
|
189
|
+
const badge = document.createElement('span');
|
|
190
|
+
badge.className = 'badge ' + phase; badge.textContent = phase;
|
|
191
|
+
div.querySelector('.k').appendChild(document.createTextNode(' ')); div.querySelector('.k').appendChild(badge);
|
|
192
|
+
el.appendChild(div);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function renderView(snap) {
|
|
197
|
+
const v = $('view');
|
|
198
|
+
if (!snap) { v.innerHTML = '<div class="empty">no snapshot</div>'; return; }
|
|
199
|
+
const frag = document.createElement('div');
|
|
200
|
+
|
|
201
|
+
const meta = document.createElement('div'); meta.className = 'meta';
|
|
202
|
+
meta.innerHTML = 'phase <b>' + esc(snap.phase) + '</b> · agent <b>' + esc(snap.agent || '—') + '</b> · model <b>' + esc(snap.model || '—') + '</b> · effort <b>' + esc(snap.effort || '—') + '</b>';
|
|
203
|
+
if (snap.usage) meta.innerHTML += ' · ctx <b>' + (snap.usage.contextPercent != null ? snap.usage.contextPercent + '%' : '—') + '</b> · out <b>' + (snap.usage.turnOutputTokens || snap.usage.outputTokens || 0) + '</b> tok';
|
|
204
|
+
frag.appendChild(meta);
|
|
205
|
+
|
|
206
|
+
if (snap.prompt) frag.appendChild(card('Prompt', preEl(snap.prompt, 'prompt')));
|
|
207
|
+
if (snap.interactions && snap.interactions.length) snap.interactions.forEach((it) => frag.appendChild(interactionEl(it)));
|
|
208
|
+
if (snap.text) frag.appendChild(card('Response', preEl(snap.text)));
|
|
209
|
+
if (snap.reasoning) { const d = document.createElement('details'); d.innerHTML = '<summary>Thinking</summary>'; d.appendChild(preEl(snap.reasoning, 'reason')); frag.appendChild(card('Reasoning', d)); }
|
|
210
|
+
if (snap.plan && snap.plan.steps && snap.plan.steps.length) frag.appendChild(card('Plan', planEl(snap.plan)));
|
|
211
|
+
if (snap.toolCalls && snap.toolCalls.length) frag.appendChild(card('Tool calls', toolsEl(snap.toolCalls)));
|
|
212
|
+
else if (snap.activity) frag.appendChild(card('Activity', preEl(snap.activity, 'reason')));
|
|
213
|
+
if (snap.subAgents && snap.subAgents.length) frag.appendChild(card('Sub-agents', preEl(snap.subAgents.map((s) => '• ' + (s.kind || '?') + ': ' + (s.description || '') + ' [' + s.status + ']').join('\n'), 'reason')));
|
|
214
|
+
if (snap.artifacts && snap.artifacts.length) frag.appendChild(card('Artifacts', artifactsEl(snap.artifacts)));
|
|
215
|
+
if (snap.error) frag.appendChild(card('Error', preEl(snap.error)));
|
|
216
|
+
|
|
217
|
+
v.innerHTML = ''; v.appendChild(frag);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function card(title, body) { const c = document.createElement('div'); c.className = 'card'; const h = document.createElement('h4'); h.textContent = title; c.appendChild(h); c.appendChild(body); return c; }
|
|
221
|
+
function preEl(text, cls) { const p = document.createElement('pre'); if (cls) p.className = cls; p.textContent = String(text); return p; }
|
|
222
|
+
function planEl(plan) { const d = document.createElement('div'); if (plan.explanation) d.appendChild(preEl(plan.explanation, 'reason')); plan.steps.forEach((s) => { const r = document.createElement('div'); r.className = 'step ' + s.status; r.textContent = (s.status === 'completed' ? '✓ ' : s.status === 'inProgress' ? '▶ ' : '○ ') + s.text; d.appendChild(r); }); return d; }
|
|
223
|
+
function toolsEl(tools) { const d = document.createElement('div'); tools.forEach((t) => { const r = document.createElement('div'); r.className = 'tool'; r.innerHTML = '<b>' + esc(t.name) + '</b> ' + esc(t.summary || '') + ' <span class="badge ' + (t.status === 'done' ? 'done' : t.status === 'failed' ? 'queued' : 'streaming') + '">' + t.status + '</span>'; d.appendChild(r); }); return d; }
|
|
224
|
+
function artifactsEl(arts) { const d = document.createElement('div'); arts.forEach((a) => { const r = document.createElement('div'); const link = document.createElement('a'); link.href = a.url; link.target = '_blank'; link.textContent = a.fileName + ' (' + a.mime + ', ' + a.fileSize + 'B)'; r.appendChild(link); d.appendChild(r); }); return d; }
|
|
225
|
+
function interactionEl(it) {
|
|
226
|
+
const d = document.createElement('div'); d.className = 'interaction';
|
|
227
|
+
const q = it.questions && it.questions[it.currentIndex || 0];
|
|
228
|
+
d.innerHTML = '<b>' + esc(it.title || 'Agent asks') + '</b>' + (it.hint ? '<div class="reason">' + esc(it.hint) + '</div>' : '');
|
|
229
|
+
if (q) {
|
|
230
|
+
const qt = document.createElement('div'); qt.style.margin = '6px 0'; qt.textContent = q.text; d.appendChild(qt);
|
|
231
|
+
if (q.choices && q.choices.length) {
|
|
232
|
+
q.choices.forEach((c) => { const b = document.createElement('button'); b.className = 'btn choice'; b.textContent = c.label; b.onclick = () => client.interact(it.promptId, 'select', c.label); d.appendChild(b); });
|
|
233
|
+
} else {
|
|
234
|
+
const inp = document.createElement('input'); inp.placeholder = 'type answer + Enter'; inp.style.width = '60%';
|
|
235
|
+
inp.onkeydown = (e) => { if (e.key === 'Enter') { client.interact(it.promptId, 'text', inp.value); inp.value = ''; } };
|
|
236
|
+
d.appendChild(inp);
|
|
237
|
+
}
|
|
238
|
+
const skip = document.createElement('button'); skip.className = 'btn choice'; skip.textContent = 'skip'; skip.onclick = () => client.interact(it.promptId, 'skip'); d.appendChild(skip);
|
|
239
|
+
}
|
|
240
|
+
return d;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function esc(s) { const d = document.createElement('div'); d.textContent = String(s == null ? '' : s); return d.innerHTML; }
|
|
244
|
+
</script>
|
|
245
|
+
</body>
|
|
246
|
+
</html>
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pikichannel-sdk.js — the reference browser client SDK (vanilla ESM, zero deps).
|
|
3
|
+
*
|
|
4
|
+
* This is the embeddable client: drop it into any web app (or wrap it in a
|
|
5
|
+
* WebView for mobile) and you get a live, bidirectional agent session over
|
|
6
|
+
* either transport. It mirrors the L2 protocol in src/pikichannel/protocol.ts —
|
|
7
|
+
* keep the message `type` literals in lockstep with that file.
|
|
8
|
+
*
|
|
9
|
+
* Layering mirrors the host:
|
|
10
|
+
* - Transport (L1): WsTransport | RtcTransport — both expose connect/send and
|
|
11
|
+
* onopen/onmessage/onclose. The client is blind to which one it holds.
|
|
12
|
+
* - Client (L2): PikichannelClient — speaks the protocol, keeps a reactive
|
|
13
|
+
* `sessions` store, exposes prompt/stop/steer/interact, and emits events.
|
|
14
|
+
*
|
|
15
|
+
* Public surface:
|
|
16
|
+
* const c = new Pikichannel.Client({ transport: 'websocket' | 'webrtc' })
|
|
17
|
+
* c.on('status'|'welcome'|'session'|'sessions'|'accepted'|'error', cb)
|
|
18
|
+
* await c.connect(); c.subscribeAll();
|
|
19
|
+
* c.prompt({ prompt, sessionKey?, agent?, model?, effort? })
|
|
20
|
+
* c.stop(key) / c.steer(taskId) / c.recall(taskId) / c.interact(promptId, action, value)
|
|
21
|
+
* c.sessions // Map<sessionKey, UniversalSnapshot>
|
|
22
|
+
* c.stats() // { transport, state, rtt, framesIn, framesOut, bytesIn, bytesOut }
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export const PROTOCOL_VERSION = 1;
|
|
26
|
+
|
|
27
|
+
const ICE_SERVERS = [{ urls: 'stun:stun.l.google.com:19302' }];
|
|
28
|
+
|
|
29
|
+
// Resolve the ws(s):// base for a target host. `host` may be a bare authority
|
|
30
|
+
// ("192.168.1.5:3940"), a full ws(s)/http(s) URL, or empty (same origin). This
|
|
31
|
+
// is what makes a client repointable: pass `host` to talk to a different node.
|
|
32
|
+
function wsBase(host) {
|
|
33
|
+
const loc = window.location;
|
|
34
|
+
const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
35
|
+
if (!host) return proto + '//' + loc.host;
|
|
36
|
+
if (/^wss?:\/\//.test(host)) return host.replace(/\/$/, '');
|
|
37
|
+
if (/^https:\/\//.test(host)) return 'wss://' + host.slice('https://'.length).replace(/\/$/, '');
|
|
38
|
+
if (/^http:\/\//.test(host)) return 'ws://' + host.slice('http://'.length).replace(/\/$/, '');
|
|
39
|
+
return proto + '//' + host.replace(/\/$/, '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// L1 transports (browser side)
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
class WsTransport {
|
|
47
|
+
constructor(base) {
|
|
48
|
+
this.kind = 'websocket';
|
|
49
|
+
this.base = base;
|
|
50
|
+
this.ws = null;
|
|
51
|
+
this.onopen = null; this.onmessage = null; this.onclose = null; this.onerror = null;
|
|
52
|
+
}
|
|
53
|
+
connect() {
|
|
54
|
+
const ws = new WebSocket(this.base + '/pikichannel/ws');
|
|
55
|
+
this.ws = ws;
|
|
56
|
+
ws.onopen = () => this.onopen && this.onopen();
|
|
57
|
+
ws.onmessage = (e) => this.onmessage && this.onmessage(e.data);
|
|
58
|
+
ws.onclose = () => this.onclose && this.onclose();
|
|
59
|
+
ws.onerror = () => this.onerror && this.onerror(new Error('websocket error'));
|
|
60
|
+
}
|
|
61
|
+
send(frame) { if (this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.send(frame); }
|
|
62
|
+
close() { if (this.ws) { try { this.ws.close(); } catch (e) {} } }
|
|
63
|
+
isOpen() { return !!this.ws && this.ws.readyState === WebSocket.OPEN; }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The browser is always the WebRTC offerer (creates the datachannel + offer).
|
|
67
|
+
// Two signaling paths share the same PC setup:
|
|
68
|
+
// - direct: SDP/ICE over `${base}/pikichannel/signal` (reachable host).
|
|
69
|
+
// - rendezvous: dial a NodeID through a broker both peers reach (NAT traversal).
|
|
70
|
+
class RtcTransport {
|
|
71
|
+
constructor(cfg) {
|
|
72
|
+
this.kind = 'webrtc';
|
|
73
|
+
this.base = cfg.base;
|
|
74
|
+
this.rendezvous = cfg.rendezvous || null; // broker ws(s):// URL
|
|
75
|
+
this.nodeId = cfg.nodeId || null; // host NodeID to dial
|
|
76
|
+
this.pc = null; this.dc = null; this.signal = null;
|
|
77
|
+
this.onopen = null; this.onmessage = null; this.onclose = null; this.onerror = null;
|
|
78
|
+
}
|
|
79
|
+
async connect() {
|
|
80
|
+
const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS });
|
|
81
|
+
this.pc = pc;
|
|
82
|
+
const dc = pc.createDataChannel('piki', { ordered: true });
|
|
83
|
+
this.dc = dc;
|
|
84
|
+
|
|
85
|
+
let remoteSet = false;
|
|
86
|
+
const pendingRemote = [];
|
|
87
|
+
const onAnswer = async (sdp) => { await pc.setRemoteDescription({ type: 'answer', sdp }); remoteSet = true; for (const c of pendingRemote.splice(0)) { try { await pc.addIceCandidate(c); } catch (e) {} } };
|
|
88
|
+
const onCandidate = async (cand) => { if (remoteSet) { try { await pc.addIceCandidate(cand); } catch (e) {} } else pendingRemote.push(cand); };
|
|
89
|
+
|
|
90
|
+
dc.onopen = () => { this.onopen && this.onopen(); try { this.signal && this.signal.close(); } catch (e) {} };
|
|
91
|
+
dc.onmessage = (e) => this.onmessage && this.onmessage(e.data);
|
|
92
|
+
dc.onclose = () => this.onclose && this.onclose();
|
|
93
|
+
pc.onconnectionstatechange = () => {
|
|
94
|
+
const s = pc.connectionState;
|
|
95
|
+
if (s === 'failed' || s === 'closed' || s === 'disconnected') this.onclose && this.onclose();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const useRendezvous = !!(this.rendezvous && this.nodeId);
|
|
99
|
+
const signal = new WebSocket(useRendezvous ? this.rendezvous : this.base + '/pikichannel/signal');
|
|
100
|
+
this.signal = signal;
|
|
101
|
+
|
|
102
|
+
if (useRendezvous) {
|
|
103
|
+
// NAT path: dial the NodeID through the broker; signaling is relayed.
|
|
104
|
+
let sessionId = null;
|
|
105
|
+
pc.onicecandidate = (e) => { if (e.candidate && sessionId && signal.readyState === WebSocket.OPEN) signal.send(JSON.stringify({ t: 'signal', sessionId, data: { kind: 'candidate', candidate: e.candidate.toJSON() } })); };
|
|
106
|
+
signal.onopen = () => signal.send(JSON.stringify({ t: 'dial', nodeId: this.nodeId }));
|
|
107
|
+
signal.onmessage = async (m) => {
|
|
108
|
+
let msg; try { msg = JSON.parse(m.data); } catch (e) { return; }
|
|
109
|
+
if (msg.t === 'dialed') {
|
|
110
|
+
sessionId = msg.sessionId;
|
|
111
|
+
const offer = await pc.createOffer(); await pc.setLocalDescription(offer);
|
|
112
|
+
signal.send(JSON.stringify({ t: 'signal', sessionId, data: { kind: 'offer', type: offer.type, sdp: offer.sdp } }));
|
|
113
|
+
} else if (msg.t === 'signal' && msg.data) {
|
|
114
|
+
const d = msg.data;
|
|
115
|
+
if (d.kind === 'answer') await onAnswer(d.sdp);
|
|
116
|
+
else if (d.kind === 'candidate' && d.candidate) await onCandidate(d.candidate);
|
|
117
|
+
else if (d.kind === 'error') this.onerror && this.onerror(new Error(d.message || 'signaling error'));
|
|
118
|
+
} else if (msg.t === 'error') { this.onerror && this.onerror(new Error(msg.message || 'rendezvous error')); }
|
|
119
|
+
else if (msg.t === 'close') { this.onclose && this.onclose(); }
|
|
120
|
+
};
|
|
121
|
+
signal.onerror = () => this.onerror && this.onerror(new Error('rendezvous socket error'));
|
|
122
|
+
} else {
|
|
123
|
+
// Direct path: SDP/ICE straight to the reachable host.
|
|
124
|
+
pc.onicecandidate = (e) => { if (e.candidate && signal.readyState === WebSocket.OPEN) signal.send(JSON.stringify({ kind: 'candidate', candidate: e.candidate.toJSON() })); };
|
|
125
|
+
signal.onmessage = async (m) => {
|
|
126
|
+
let msg; try { msg = JSON.parse(m.data); } catch (e) { return; }
|
|
127
|
+
if (msg.kind === 'answer') await onAnswer(msg.sdp);
|
|
128
|
+
else if (msg.kind === 'candidate' && msg.candidate) await onCandidate(msg.candidate);
|
|
129
|
+
else if (msg.kind === 'error') this.onerror && this.onerror(new Error(msg.message || 'signaling error'));
|
|
130
|
+
};
|
|
131
|
+
signal.onerror = () => this.onerror && this.onerror(new Error('signaling socket error'));
|
|
132
|
+
signal.onopen = async () => {
|
|
133
|
+
const offer = await pc.createOffer(); await pc.setLocalDescription(offer);
|
|
134
|
+
signal.send(JSON.stringify({ kind: 'offer', type: offer.type, sdp: offer.sdp }));
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
send(frame) { if (this.dc && this.dc.readyState === 'open') this.dc.send(frame); }
|
|
139
|
+
close() {
|
|
140
|
+
try { this.dc && this.dc.close(); } catch (e) {}
|
|
141
|
+
try { this.pc && this.pc.close(); } catch (e) {}
|
|
142
|
+
try { this.signal && this.signal.close(); } catch (e) {}
|
|
143
|
+
}
|
|
144
|
+
isOpen() { return !!this.dc && this.dc.readyState === 'open'; }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function makeTransport(kind, cfg) {
|
|
148
|
+
if (kind === 'webrtc') return new RtcTransport(cfg);
|
|
149
|
+
return new WsTransport(cfg.base);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Mirror of applySnapshotPatch() in src/pikichannel/protocol.ts — keep in lockstep.
|
|
153
|
+
function applyPatch(prev, patch) {
|
|
154
|
+
if (patch.full) return patch.full;
|
|
155
|
+
const next = prev ? Object.assign({}, prev) : { phase: 'idle', updatedAt: 0 };
|
|
156
|
+
if (patch.appendText) next.text = (next.text || '') + patch.appendText;
|
|
157
|
+
if (patch.appendReasoning) next.reasoning = (next.reasoning || '') + patch.appendReasoning;
|
|
158
|
+
if (patch.set) Object.assign(next, patch.set);
|
|
159
|
+
return next;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// L2 client
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
export class PikichannelClient {
|
|
167
|
+
constructor(opts) {
|
|
168
|
+
opts = opts || {};
|
|
169
|
+
this.rendezvous = opts.rendezvous || null; // broker URL for NAT traversal
|
|
170
|
+
this.nodeId = opts.nodeId || null; // host NodeID to dial via the broker
|
|
171
|
+
// A rendezvous dial implies WebRTC (the broker only brokers P2P signaling).
|
|
172
|
+
this.transportKind = (this.rendezvous && this.nodeId) ? 'webrtc' : (opts.transport === 'webrtc' ? 'webrtc' : 'websocket');
|
|
173
|
+
this.token = opts.token || null;
|
|
174
|
+
this.endpoint = opts.host || null; // target node authority/URL (null = same origin)
|
|
175
|
+
this._base = wsBase(this.endpoint); // resolved ws(s):// base
|
|
176
|
+
this.transport = null;
|
|
177
|
+
this.state = 'idle'; // idle | connecting | open | closed
|
|
178
|
+
this.host = null; // HostInfo from welcome
|
|
179
|
+
this.sessions = new Map(); // sessionKey -> UniversalSnapshot (reconstructed from patches)
|
|
180
|
+
this.sessionMetas = []; // SessionMeta[]
|
|
181
|
+
this._seqs = new Map(); // sessionKey -> last applied seq (gap detection)
|
|
182
|
+
this._listeners = new Map();
|
|
183
|
+
this._pending = new Map(); // request id -> {resolve,reject} (control-plane tunnel)
|
|
184
|
+
this._reqSeq = 0;
|
|
185
|
+
this._pingTimer = null;
|
|
186
|
+
this._rtt = null;
|
|
187
|
+
this._stats = { framesIn: 0, framesOut: 0, bytesIn: 0, bytesOut: 0, connectedAt: null };
|
|
188
|
+
this._pendingResolve = null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// -- event emitter --
|
|
192
|
+
on(type, cb) {
|
|
193
|
+
let set = this._listeners.get(type);
|
|
194
|
+
if (!set) { set = new Set(); this._listeners.set(type, set); }
|
|
195
|
+
set.add(cb);
|
|
196
|
+
return () => this.off(type, cb);
|
|
197
|
+
}
|
|
198
|
+
off(type, cb) { const set = this._listeners.get(type); if (set) set.delete(cb); }
|
|
199
|
+
_emit(type, payload) { const set = this._listeners.get(type); if (set) for (const cb of set) { try { cb(payload); } catch (e) {} } }
|
|
200
|
+
|
|
201
|
+
_setState(state) { this.state = state; this._emit('status', this.stats()); }
|
|
202
|
+
|
|
203
|
+
// -- lifecycle --
|
|
204
|
+
connect() {
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
this._pendingResolve = resolve;
|
|
207
|
+
this._setState('connecting');
|
|
208
|
+
const t = makeTransport(this.transportKind, { base: this._base, rendezvous: this.rendezvous, nodeId: this.nodeId });
|
|
209
|
+
this.transport = t;
|
|
210
|
+
t.onopen = () => {
|
|
211
|
+
this._stats.connectedAt = Date.now();
|
|
212
|
+
const hello = { type: 'hello', v: PROTOCOL_VERSION, client: { name: 'pikichannel-web-sdk', platform: navigator.userAgent } };
|
|
213
|
+
if (this.token) hello.token = this.token;
|
|
214
|
+
this._send(hello);
|
|
215
|
+
this._setState('open');
|
|
216
|
+
this._startPing();
|
|
217
|
+
};
|
|
218
|
+
t.onmessage = (data) => this._onFrame(data);
|
|
219
|
+
t.onclose = () => { this._stopPing(); this._failPending('connection closed'); this._setState('closed'); this._emit('close', null); };
|
|
220
|
+
t.onerror = (err) => { this._emit('error', { message: err && err.message ? err.message : 'transport error' }); if (this._pendingResolve) { reject(err); this._pendingResolve = null; } };
|
|
221
|
+
// resolve connect() on welcome
|
|
222
|
+
const off = this.on('welcome', () => { off(); if (this._pendingResolve) { this._pendingResolve(this.host); this._pendingResolve = null; } });
|
|
223
|
+
Promise.resolve(t.connect()).catch((e) => { this._emit('error', { message: e && e.message ? e.message : 'connect failed' }); reject(e); });
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
disconnect() { this._stopPing(); if (this.transport) this.transport.close(); this._setState('closed'); }
|
|
228
|
+
|
|
229
|
+
// -- outbound commands --
|
|
230
|
+
subscribeAll() { this._send({ type: 'subscribe', sessionKey: '*' }); }
|
|
231
|
+
subscribe(sessionKey) { this._send({ type: 'subscribe', sessionKey: sessionKey }); }
|
|
232
|
+
unsubscribe(sessionKey) { this._send({ type: 'unsubscribe', sessionKey: sessionKey }); }
|
|
233
|
+
getSnapshot(sessionKey) { this._send({ type: 'getSnapshot', sessionKey: sessionKey }); }
|
|
234
|
+
listSessions() { this._send({ type: 'listSessions' }); }
|
|
235
|
+
prompt(opts) {
|
|
236
|
+
const ref = 'r' + Math.random().toString(36).slice(2, 9);
|
|
237
|
+
this._send(Object.assign({ type: 'prompt', clientRef: ref }, opts));
|
|
238
|
+
return ref;
|
|
239
|
+
}
|
|
240
|
+
stop(sessionKey) { this._send({ type: 'stop', sessionKey: sessionKey }); }
|
|
241
|
+
steer(taskId) { this._send({ type: 'steer', taskId: taskId }); }
|
|
242
|
+
recall(taskId) { this._send({ type: 'recall', taskId: taskId }); }
|
|
243
|
+
interact(promptId, action, value, requestFreeform) {
|
|
244
|
+
this._send({ type: 'interact', promptId: promptId, action: action, value: value, requestFreeform: requestFreeform });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Control-plane HTTP over the channel — full management of the connected host
|
|
249
|
+
* WITHOUT it exposing REST publicly (no CORS, rides the authenticated channel).
|
|
250
|
+
* Only `/api/*` is allowed by the host. Resolves with a fetch-like response:
|
|
251
|
+
* { status, ok, headers, text(), json(), base64(), bytes() }.
|
|
252
|
+
*/
|
|
253
|
+
request(method, path, opts) {
|
|
254
|
+
opts = opts || {};
|
|
255
|
+
let body = opts.body;
|
|
256
|
+
if (body != null && typeof body !== 'string') body = JSON.stringify(body);
|
|
257
|
+
const headers = opts.headers || (body != null ? { 'content-type': 'application/json' } : undefined);
|
|
258
|
+
const id = 'q' + (++this._reqSeq);
|
|
259
|
+
return new Promise((resolve, reject) => {
|
|
260
|
+
this._pending.set(id, { resolve, reject });
|
|
261
|
+
this._send({ type: 'request', id: id, method: method || 'GET', path: path, headers: headers, body: body, encoding: 'utf8' });
|
|
262
|
+
setTimeout(() => { if (this._pending.has(id)) { this._pending.delete(id); reject(new Error('request timeout')); } }, opts.timeout || 20000);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
_failPending(reason) {
|
|
267
|
+
for (const [, p] of this._pending) { try { p.reject(new Error(reason)); } catch (e) {} }
|
|
268
|
+
this._pending.clear();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// -- inbound --
|
|
272
|
+
_onFrame(data) {
|
|
273
|
+
this._stats.framesIn++;
|
|
274
|
+
this._stats.bytesIn += (data && data.length) ? data.length : 0;
|
|
275
|
+
let msg; try { msg = JSON.parse(data); } catch (e) { return; }
|
|
276
|
+
switch (msg.type) {
|
|
277
|
+
case 'welcome':
|
|
278
|
+
this.host = msg.host;
|
|
279
|
+
this.sessionMetas = msg.sessions || [];
|
|
280
|
+
this._emit('welcome', msg.host);
|
|
281
|
+
this._emit('sessions', this.sessionMetas);
|
|
282
|
+
break;
|
|
283
|
+
case 'session': {
|
|
284
|
+
const key = msg.sessionKey;
|
|
285
|
+
const prev = this.sessions.get(key) || null;
|
|
286
|
+
const lastSeq = this._seqs.get(key);
|
|
287
|
+
const contiguous = lastSeq === undefined || msg.seq === lastSeq + 1 || !!msg.patch.full;
|
|
288
|
+
if (!msg.patch.full && (prev === null || !contiguous)) {
|
|
289
|
+
this.getSnapshot(key); // missing baseline or seq gap → ask for a full resync
|
|
290
|
+
break; // don't apply a delta we can't anchor
|
|
291
|
+
}
|
|
292
|
+
const next = applyPatch(prev, msg.patch);
|
|
293
|
+
this.sessions.set(key, next);
|
|
294
|
+
this._seqs.set(key, msg.seq);
|
|
295
|
+
this._emit('session', Object.assign({ sessionKey: key }, next));
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
case 'sessions':
|
|
299
|
+
this.sessionMetas = msg.sessions || [];
|
|
300
|
+
this._emit('sessions', this.sessionMetas);
|
|
301
|
+
break;
|
|
302
|
+
case 'accepted':
|
|
303
|
+
this._emit('accepted', msg);
|
|
304
|
+
break;
|
|
305
|
+
case 'response': {
|
|
306
|
+
const p = this._pending.get(msg.id);
|
|
307
|
+
if (!p) break;
|
|
308
|
+
this._pending.delete(msg.id);
|
|
309
|
+
const enc = msg.encoding || 'utf8';
|
|
310
|
+
const raw = msg.body || '';
|
|
311
|
+
if (msg.error && (msg.status === undefined || msg.status === 0)) { p.reject(new Error(msg.error)); break; }
|
|
312
|
+
p.resolve({
|
|
313
|
+
status: msg.status, ok: msg.status >= 200 && msg.status < 300, headers: msg.headers || {}, error: msg.error || null,
|
|
314
|
+
text: () => raw,
|
|
315
|
+
json: () => JSON.parse(raw),
|
|
316
|
+
base64: () => raw,
|
|
317
|
+
bytes: () => (enc === 'base64' ? Uint8Array.from(atob(raw), (c) => c.charCodeAt(0)) : new TextEncoder().encode(raw)),
|
|
318
|
+
});
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
case 'error':
|
|
322
|
+
this._emit('error', { message: msg.message, code: msg.code, clientRef: msg.clientRef });
|
|
323
|
+
break;
|
|
324
|
+
case 'pong':
|
|
325
|
+
if (typeof msg.t === 'number') { this._rtt = Math.max(0, Math.round(performance.now() - msg.t)); this._emit('status', this.stats()); }
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
_send(msg) {
|
|
331
|
+
if (!this.transport || !this.transport.isOpen()) return;
|
|
332
|
+
const frame = JSON.stringify(msg);
|
|
333
|
+
this._stats.framesOut++;
|
|
334
|
+
this._stats.bytesOut += frame.length;
|
|
335
|
+
this.transport.send(frame);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
_startPing() {
|
|
339
|
+
this._stopPing();
|
|
340
|
+
this._pingTimer = setInterval(() => { this._send({ type: 'ping', t: performance.now() }); }, 3000);
|
|
341
|
+
}
|
|
342
|
+
_stopPing() { if (this._pingTimer) { clearInterval(this._pingTimer); this._pingTimer = null; } }
|
|
343
|
+
|
|
344
|
+
stats() {
|
|
345
|
+
return {
|
|
346
|
+
transport: this.transportKind,
|
|
347
|
+
state: this.state,
|
|
348
|
+
rtt: this._rtt,
|
|
349
|
+
framesIn: this._stats.framesIn,
|
|
350
|
+
framesOut: this._stats.framesOut,
|
|
351
|
+
bytesIn: this._stats.bytesIn,
|
|
352
|
+
bytesOut: this._stats.bytesOut,
|
|
353
|
+
sessions: this.sessions.size,
|
|
354
|
+
host: this.host,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (typeof window !== 'undefined') {
|
|
360
|
+
window.Pikichannel = { Client: PikichannelClient, PROTOCOL_VERSION: PROTOCOL_VERSION };
|
|
361
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pikiloom",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.23",
|
|
4
4
|
"description": "Put the world's smartest AI agents in your pocket. Command local Claude & Gemini via IM. | 让最好用的 IM 变成你电脑上的顶级 Agent 控制台",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"command": "set -ae && . ./.env && set +a && tsx src/cli/run.ts",
|
|
33
33
|
"build:dashboard": "vite build --config dashboard/vite.config.ts",
|
|
34
34
|
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
35
|
-
"
|
|
35
|
+
"copy:pikichannel-web": "node -e \"require('fs').cpSync('src/pikichannel/web','dist/pikichannel/web',{recursive:true})\"",
|
|
36
|
+
"build": "npm run clean && npm run build:dashboard && tsc && npm run copy:pikichannel-web",
|
|
36
37
|
"postbuild": "node -e \"require('fs').chmodSync('dist/cli/main.js',0o755)\"",
|
|
37
38
|
"prepack": "npm run build",
|
|
38
39
|
"test": "vitest run",
|
|
@@ -74,6 +75,7 @@
|
|
|
74
75
|
"qrcode": "^1.5.4",
|
|
75
76
|
"remark-breaks": "^4.0.0",
|
|
76
77
|
"undici": "^7.22.0",
|
|
78
|
+
"werift": "^0.23.0",
|
|
77
79
|
"which": "^6.0.1",
|
|
78
80
|
"ws": "^8.18.0"
|
|
79
81
|
},
|