crisp-tui 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js DELETED
@@ -1,1506 +0,0 @@
1
- // @bun
2
- var __defProp = Object.defineProperty;
3
- var __returnValue = (v) => v;
4
- function __exportSetter(name, newValue) {
5
- this[name] = __returnValue.bind(null, newValue);
6
- }
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, {
10
- get: all[name],
11
- enumerable: true,
12
- configurable: true,
13
- set: __exportSetter.bind(all, name)
14
- });
15
- };
16
- var __esm = (fn, res, err) => () => {
17
- if (fn)
18
- try {
19
- res = fn(fn = 0);
20
- } catch (e) {
21
- err = [e];
22
- }
23
- if (err)
24
- throw err[0];
25
- return res;
26
- };
27
-
28
- // src/crispctl.ts
29
- import { existsSync } from "fs";
30
- import { resolve } from "path";
31
- import { fileURLToPath } from "url";
32
- function command(env = process.env) {
33
- if (env.CRISPCTL_BIN)
34
- return [env.CRISPCTL_BIN];
35
- try {
36
- return [Bun.which("node") || "node", fileURLToPath(import.meta.resolve("crispctl/dist/index.js"))];
37
- } catch {}
38
- const installed = Bun.which("crispctl");
39
- if (installed)
40
- return [installed];
41
- const sibling = resolve(import.meta.dir, "../../crisp-cli/dist/index.js");
42
- if (existsSync(sibling))
43
- return [Bun.which("node") || "node", sibling];
44
- throw new Error("crispctl not found. Build ../crisp-cli (npm ci && npm run build), install crispctl, or set CRISPCTL_BIN to its executable.");
45
- }
46
- function runner(prefix, global = [], env = process.env) {
47
- return async (args) => {
48
- const child = Bun.spawn([...prefix, ...global, "--json", ...args], {
49
- stdin: "ignore",
50
- stdout: "pipe",
51
- stderr: "pipe",
52
- env
53
- });
54
- const timer = setTimeout(() => child.kill(), 30000);
55
- try {
56
- const [out, err, code] = await Promise.all([
57
- new Response(child.stdout).text(),
58
- new Response(child.stderr).text(),
59
- child.exited
60
- ]);
61
- if (code !== 0) {
62
- let message = "crispctl failed or timed out";
63
- try {
64
- message = JSON.parse(err).message || message;
65
- } catch {}
66
- throw new CliError(message, code || 1);
67
- }
68
- try {
69
- return JSON.parse(out);
70
- } catch {
71
- throw new CliError("crispctl returned invalid JSON");
72
- }
73
- } finally {
74
- clearTimeout(timer);
75
- }
76
- };
77
- }
78
- function array(value, key) {
79
- if (!Array.isArray(value) || value.some((v) => !v || typeof v !== "object" || !(key in v))) {
80
- throw new Error(`Unexpected crispctl response (expected array with ${key})`);
81
- }
82
- return value;
83
- }
84
- function createClient(run, profile, subscribe) {
85
- return {
86
- label: profile,
87
- subscribe,
88
- list: async (page, query) => array(await run(query ? ["conversations", "search", query, "--page", String(page)] : ["conversations", "list", "--page", String(page)]), "session_id"),
89
- async get(session) {
90
- const value = await run(["conversations", "get", session]);
91
- if (!value || value.session_id !== session)
92
- throw new Error("Unexpected conversation response");
93
- return value;
94
- },
95
- messages: async (session) => array(await run(["messages", "list", session]), "content").sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0)),
96
- reply: (session, text, note) => run(["reply", session, `${note ? "--note" : "--text"}=${text}`]),
97
- state: (session, resolved) => run([resolved ? "resolve" : "reopen", session]),
98
- read: (session) => run(["read", session])
99
- };
100
- }
101
- var CliError;
102
- var init_crispctl = __esm(() => {
103
- CliError = class CliError extends Error {
104
- code;
105
- constructor(message, code = 1) {
106
- super(message);
107
- this.code = code;
108
- }
109
- };
110
- });
111
-
112
- // src/control.ts
113
- import { createConnection, createServer } from "net";
114
- import { chmod, lstat, mkdir, unlink } from "fs/promises";
115
- import { dirname, join } from "path";
116
- import { createHash } from "crypto";
117
- function socketPath(profile = "sandbox", website = "") {
118
- const key = createHash("sha256").update(`${profile}:${website}`).digest("hex").slice(0, 12);
119
- return process.env.CRISP_TUI_SOCKET || join("/tmp", `crisp-tui-${process.getuid?.() ?? "user"}`, `${key}.sock`);
120
- }
121
- function request(path, method, params = {}, timeout = 70000) {
122
- return new Promise((resolve, reject) => {
123
- const socket = createConnection(path);
124
- let buffer = "";
125
- const finish = (error, value) => {
126
- clearTimeout(timer);
127
- socket.destroy();
128
- error ? reject(error) : resolve(value);
129
- };
130
- const timer = setTimeout(() => finish(new Error("TUI control request timed out")), timeout);
131
- socket.setEncoding("utf8");
132
- socket.on("connect", () => socket.write(JSON.stringify({ id: 1, method, params }) + `
133
- `));
134
- socket.on("error", (e) => finish(e.code === "ENOENT" || e.code === "ECONNREFUSED" ? new NotRunning("No TUI running for this profile/socket") : e));
135
- socket.on("end", () => finish(new Error("TUI closed the connection")));
136
- socket.on("data", (chunk) => {
137
- buffer += chunk;
138
- if (Buffer.byteLength(buffer) > LIMIT)
139
- return finish(new Error("Control response too large"));
140
- if (!buffer.includes(`
141
- `))
142
- return;
143
- try {
144
- const reply = JSON.parse(buffer.slice(0, buffer.indexOf(`
145
- `)));
146
- if (reply.id !== 1 || typeof reply.ok !== "boolean")
147
- throw new Error("Invalid control response");
148
- finish(reply.ok ? undefined : new Error(reply.error), reply.result);
149
- } catch (e) {
150
- finish(e instanceof Error ? e : new Error("Invalid control response"));
151
- }
152
- });
153
- });
154
- }
155
- async function serve(path, handle) {
156
- const parent = dirname(path);
157
- await mkdir(parent, { recursive: true, mode: 448 });
158
- const info = await lstat(parent);
159
- if (!info.isDirectory() || info.uid !== process.getuid?.() || info.mode & 63) {
160
- throw new Error("Control socket requires a private directory owned by you (mode 0700)");
161
- }
162
- try {
163
- const file = await lstat(path);
164
- if (!file.isSocket() || file.uid !== process.getuid?.())
165
- throw new Error("Refusing to replace a non-socket or another user's socket");
166
- await new Promise((resolve, reject) => {
167
- const probe = createConnection(path);
168
- probe.setTimeout(1000);
169
- probe.once("connect", () => {
170
- probe.destroy();
171
- reject(new Error("Another TUI is already running for this profile/socket"));
172
- });
173
- probe.once("timeout", () => {
174
- probe.destroy();
175
- reject(new Error("Existing control socket is unresponsive"));
176
- });
177
- probe.once("error", (e) => e.code === "ECONNREFUSED" ? resolve() : reject(e));
178
- });
179
- await unlink(path);
180
- } catch (e) {
181
- if (e.code !== "ENOENT")
182
- throw e;
183
- }
184
- const sockets = new Set;
185
- const server = createServer((socket) => {
186
- sockets.add(socket);
187
- socket.on("close", () => sockets.delete(socket));
188
- socket.on("error", () => socket.destroy());
189
- socket.setEncoding("utf8");
190
- socket.setTimeout(75000, () => socket.destroy());
191
- let buffer = "";
192
- let received = false;
193
- socket.on("data", (chunk) => {
194
- if (received)
195
- return;
196
- buffer += chunk;
197
- if (Buffer.byteLength(buffer) > LIMIT) {
198
- socket.destroy();
199
- return;
200
- }
201
- const end = buffer.indexOf(`
202
- `);
203
- if (end < 0)
204
- return;
205
- received = true;
206
- (async () => {
207
- let id = null;
208
- try {
209
- const input = JSON.parse(buffer.slice(0, end));
210
- if (!input || typeof input.method !== "string" || !Number.isSafeInteger(input.id))
211
- throw new Error("Invalid request");
212
- id = input.id;
213
- const params = input.params ?? {};
214
- if (typeof params !== "object" || Array.isArray(params))
215
- throw new Error("Invalid params");
216
- const result = await handle(input.method, params);
217
- socket.end(JSON.stringify({ id, ok: true, result: result ?? null }) + `
218
- `);
219
- } catch (e) {
220
- socket.end(JSON.stringify({ id, ok: false, error: e instanceof Error ? e.message : "Control error" }) + `
221
- `);
222
- }
223
- })();
224
- });
225
- });
226
- await new Promise((resolve, reject) => {
227
- server.once("error", reject);
228
- server.listen(path, () => {
229
- server.off("error", reject);
230
- resolve();
231
- });
232
- });
233
- await chmod(path, 384);
234
- return { async stop() {
235
- for (const socket of sockets)
236
- socket.destroy();
237
- await new Promise((resolve, reject) => server.close((e) => e ? reject(e) : resolve()));
238
- await unlink(path).catch(() => {});
239
- } };
240
- }
241
- function string(params, key) {
242
- const value = params[key];
243
- if (typeof value !== "string" || !value.trim())
244
- throw new Error(`${key} must be a non-empty string`);
245
- return value;
246
- }
247
- function controller(store, focus = () => {}) {
248
- let pending = Promise.resolve(null);
249
- const handle = async (method, params) => {
250
- switch (method) {
251
- case "state":
252
- return store.snapshot();
253
- case "screen":
254
- return store.screen();
255
- case "conversations":
256
- return store.state.conversations;
257
- case "messages":
258
- return store.state.messages;
259
- case "goto": {
260
- await store.open(string(params, "session"));
261
- store.update({ status: "Agent opened a conversation" });
262
- return store.snapshot();
263
- }
264
- case "draft": {
265
- if (store.state.readOnly)
266
- throw new Error("Read-only mode: agent drafts are disabled");
267
- const session = string(params, "session");
268
- const text = string(params, "text");
269
- if (params.note !== undefined && typeof params.note !== "boolean")
270
- throw new Error("note must be boolean");
271
- if (params.replace !== undefined && typeof params.replace !== "boolean")
272
- throw new Error("replace must be boolean");
273
- const check = () => {
274
- if (store.state.sending)
275
- throw new Error("A send is in progress");
276
- if (store.state.drafts[session]?.text && params.replace !== true)
277
- throw new Error("Draft already exists; use --replace to overwrite it");
278
- };
279
- check();
280
- if (store.state.active?.session_id !== session)
281
- await store.open(session);
282
- if (store.state.active?.session_id !== session)
283
- throw new Error("Conversation changed while preparing draft; retry");
284
- check();
285
- store.setDraft(text, params.note === true);
286
- store.update({ status: "Agent draft ready \xB7 review and press Enter to send" });
287
- focus();
288
- return { session, draft: store.draft(), sent: false };
289
- }
290
- case "refresh":
291
- await store.refresh();
292
- return store.snapshot();
293
- default:
294
- throw new Error(`Unknown control method: ${method}`);
295
- }
296
- };
297
- return (method, params) => {
298
- const task = pending.then(() => handle(method, params));
299
- pending = task.catch(() => {});
300
- return task;
301
- };
302
- }
303
- var LIMIT = 1048576, NotRunning;
304
- var init_control = __esm(() => {
305
- NotRunning = class NotRunning extends Error {
306
- };
307
- });
308
-
309
- // src/types.ts
310
- function messageText(m) {
311
- if (typeof m.content === "string")
312
- return clean(m.content);
313
- const c = m.content;
314
- if (c && typeof c === "object") {
315
- const title = typeof c.name === "string" ? c.name : m.type || "attachment";
316
- if (typeof c.url === "string")
317
- return clean(`[${title}] ${c.url}`);
318
- }
319
- return clean(`[${m.type || "message"}] ${JSON.stringify(m.content) ?? ""}`);
320
- }
321
- var label = (c) => c.meta?.nickname || c.meta?.email || c.session_id, clean = (text) => text.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, "");
322
-
323
- // src/readonly.ts
324
- async function refuseWrite() {
325
- throw new Error(READ_ONLY_ERROR);
326
- }
327
- function readOnlyClient(client) {
328
- return {
329
- label: client.label,
330
- readOnly: true,
331
- subscribe: client.subscribe,
332
- list: (page, query) => client.list(page, query),
333
- get: (session) => client.get(session),
334
- messages: (session) => client.messages(session),
335
- reply: refuseWrite,
336
- state: refuseWrite,
337
- read: refuseWrite
338
- };
339
- }
340
- var READ_ONLY_ERROR = "Read-only mode: replies, notes, state changes and mark-read are disabled";
341
-
342
- // src/store.ts
343
- class Store {
344
- client;
345
- state;
346
- listeners = new Set;
347
- selection = 0;
348
- listing = 0;
349
- messageVersion = 0;
350
- refreshTask;
351
- constructor(client) {
352
- this.client = client;
353
- if (client.readOnly)
354
- this.client = readOnlyClient(client);
355
- this.state = { realtime: "off", readOnly: !!client.readOnly, source: client.label, conversations: [], active: null, messages: [], drafts: {}, query: "", page: 1, loading: false, sending: false, error: "", status: client.readOnly ? "Read-only \xB7 no changes will be sent to Crisp" : "Ready", revision: 0 };
356
- }
357
- subscribe(fn) {
358
- this.listeners.add(fn);
359
- return () => {
360
- this.listeners.delete(fn);
361
- };
362
- }
363
- update(patch) {
364
- this.state = { ...this.state, ...patch, revision: this.state.revision + 1 };
365
- for (const fn of this.listeners)
366
- fn();
367
- }
368
- async perform(action) {
369
- try {
370
- await action();
371
- } catch (e) {
372
- this.update({ error: clean(e instanceof Error ? e.message : String(e)) });
373
- }
374
- }
375
- async list(query = this.state.query, page = this.state.page) {
376
- const version = ++this.listing;
377
- this.update({ loading: true, error: "", query, page });
378
- try {
379
- const conversations = await this.client.list(page, query);
380
- if (version !== this.listing)
381
- return;
382
- this.update({ conversations });
383
- if (!this.state.active && conversations[0])
384
- await this.open(conversations[0].session_id);
385
- } finally {
386
- if (version === this.listing)
387
- this.update({ loading: false });
388
- }
389
- }
390
- async open(session) {
391
- const version = ++this.selection;
392
- this.update({ active: null, messages: [], error: "", status: "Loading conversation\u2026" });
393
- const [active, messages] = await Promise.all([this.client.get(session), this.client.messages(session)]);
394
- if (version !== this.selection)
395
- return;
396
- this.update({ active, messages, status: `Opened ${clean(label(active))}` });
397
- }
398
- draft() {
399
- return this.state.drafts[this.state.active?.session_id ?? ""] ?? { text: "", note: false };
400
- }
401
- setDraft(text, note = this.draft().note) {
402
- const id = this.state.active?.session_id;
403
- if (!id)
404
- throw new Error("Open a conversation first");
405
- if (this.state.sending)
406
- throw new Error("Wait for the current send to finish");
407
- this.update({ drafts: { ...this.state.drafts, [id]: { text, note } } });
408
- }
409
- async send() {
410
- const id = this.state.active?.session_id;
411
- const draft = this.draft();
412
- if (!id || !draft.text.trim() || this.state.sending)
413
- return;
414
- this.messageVersion++;
415
- this.update({ sending: true, error: "" });
416
- try {
417
- await this.client.reply(id, draft.text, draft.note);
418
- this.update({ drafts: { ...this.state.drafts, [id]: { ...draft, text: "" } }, status: draft.note ? "Internal note saved" : "Reply sent" });
419
- try {
420
- await this.refreshMessages();
421
- } catch {
422
- this.update({ error: "Sent successfully; refresh failed. Do not resend." });
423
- }
424
- } finally {
425
- this.update({ sending: false });
426
- }
427
- }
428
- async changeState() {
429
- const active = this.state.active;
430
- if (!active)
431
- return;
432
- const resolved = active.state !== "resolved";
433
- await this.client.state(active.session_id, resolved);
434
- this.messageVersion++;
435
- const state = resolved ? "resolved" : "unresolved";
436
- this.update({
437
- active: this.state.active?.session_id === active.session_id ? { ...this.state.active, state } : this.state.active,
438
- conversations: this.state.conversations.map((c) => c.session_id === active.session_id ? { ...c, state } : c),
439
- status: resolved ? "Conversation resolved" : "Conversation reopened"
440
- });
441
- }
442
- async markRead() {
443
- const id = this.state.active?.session_id;
444
- if (!id)
445
- return;
446
- await this.client.read(id);
447
- this.update({ conversations: this.state.conversations.map((c) => c.session_id === id ? { ...c, unread: { operator: 0 } } : c), status: "Marked read" });
448
- }
449
- async refreshMessages() {
450
- const id = this.state.active?.session_id;
451
- const version = this.selection;
452
- if (!id)
453
- return;
454
- const messageVersion = ++this.messageVersion;
455
- const [active, messages] = await Promise.all([this.client.get(id), this.client.messages(id)]);
456
- if (version === this.selection && messageVersion === this.messageVersion)
457
- this.update({ active, messages });
458
- }
459
- refresh() {
460
- if (this.refreshTask)
461
- return this.refreshTask;
462
- this.refreshTask = (async () => {
463
- await this.list();
464
- await this.refreshMessages();
465
- this.update({ error: "" });
466
- })().finally(() => {
467
- this.refreshTask = undefined;
468
- });
469
- return this.refreshTask;
470
- }
471
- async refreshAfterCurrent() {
472
- if (this.refreshTask)
473
- await this.refreshTask.catch(() => {});
474
- return this.refresh();
475
- }
476
- snapshot() {
477
- return { ...this.state, draft: this.draft(), protocol: 1 };
478
- }
479
- screen() {
480
- const s = this.state;
481
- return clean([
482
- `Crisp | ${s.source}${s.readOnly ? " | READ ONLY" : ""}`,
483
- `Inbox \xB7 page ${s.page} \xB7 ${s.query || "all"}`,
484
- ...s.conversations.map((c) => `${c.session_id === s.active?.session_id ? ">" : " "} ${label(c)} [${c.state || "unknown"}] ${c.session_id}`),
485
- s.active ? `Conversation: ${label(s.active)} (${s.active.session_id})` : "No conversation open",
486
- ...s.messages.map((m) => `${m.user?.nickname || m.from || "unknown"}${m.type === "note" ? " [internal note]" : ""}: ${messageText(m)}`),
487
- `${this.draft().note ? "Internal note" : "Reply"} draft: ${this.draft().text}`,
488
- s.error || s.status
489
- ].join(`
490
- `));
491
- }
492
- }
493
- var init_store = () => {};
494
-
495
- // src/rtm.ts
496
- function listen(prefix, flags, env = process.env) {
497
- return (onEvent, onStatus) => {
498
- let stopped = false;
499
- let failed = false;
500
- const child = Bun.spawn([
501
- ...prefix,
502
- ...flags,
503
- "--read-only",
504
- "--json",
505
- "listen",
506
- "--events",
507
- "message:send,message:received,message:updated,message:removed,session:set_state"
508
- ], { env, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
509
- onStatus({ state: "connecting" });
510
- const error = (message) => {
511
- if (stopped)
512
- return;
513
- failed = true;
514
- onStatus({ state: "error", message });
515
- };
516
- const read = async (stream, status) => {
517
- const reader = stream.getReader(), decoder = new TextDecoder;
518
- let buffer = "";
519
- try {
520
- while (!stopped) {
521
- const { value, done } = await reader.read();
522
- if (done)
523
- break;
524
- buffer += decoder.decode(value, { stream: true });
525
- if (Buffer.byteLength(buffer) > 1048576)
526
- throw new Error("Oversized stream");
527
- let end;
528
- while ((end = buffer.indexOf(`
529
- `)) !== -1) {
530
- const line = buffer.slice(0, end);
531
- buffer = buffer.slice(end + 1);
532
- if (!line.trim())
533
- continue;
534
- const item = JSON.parse(line);
535
- if (status) {
536
- if (item.status === "authenticated" || item.status === "reconnecting")
537
- onStatus({ state: item.status });
538
- else if (item.ok === false)
539
- error("crispctl RTM reported an error; check token access and configuration");
540
- } else if (typeof item.event === "string" && item.data && typeof item.data === "object" && !Array.isArray(item.data)) {
541
- onEvent({ event: item.event, data: item.data, received_at: item.received_at });
542
- }
543
- }
544
- }
545
- } catch {
546
- error("crispctl RTM stream could not be read");
547
- child.kill("SIGTERM");
548
- } finally {
549
- reader.releaseLock();
550
- }
551
- };
552
- Promise.all([read(child.stdout, false), read(child.stderr, true), child.exited]).then(() => {
553
- if (!stopped && !failed)
554
- error("crispctl RTM stream ended");
555
- });
556
- return () => {
557
- stopped = true;
558
- child.kill("SIGTERM");
559
- };
560
- };
561
- }
562
- function attachRealtime(store, minRefreshMs = 5000) {
563
- if (!store.client.subscribe)
564
- return () => {};
565
- let timer;
566
- let running = false, dirty = false, stopped = false, last = 0;
567
- const schedule = () => {
568
- if (stopped || running || timer)
569
- return;
570
- timer = setTimeout(() => {
571
- timer = undefined;
572
- if (stopped)
573
- return;
574
- dirty = false;
575
- running = true;
576
- last = Date.now();
577
- store.perform(() => store.refreshAfterCurrent()).finally(() => {
578
- running = false;
579
- if (dirty)
580
- schedule();
581
- });
582
- }, Math.max(200, minRefreshMs - (Date.now() - last)));
583
- };
584
- const stop = store.client.subscribe(() => {
585
- dirty = true;
586
- schedule();
587
- }, (status) => {
588
- if (stopped)
589
- return;
590
- store.update({ realtime: status.state, ...status.message ? { error: clean(status.message) } : {} });
591
- if (status.state === "authenticated") {
592
- dirty = true;
593
- schedule();
594
- }
595
- });
596
- return () => {
597
- stopped = true;
598
- clearTimeout(timer);
599
- stop();
600
- };
601
- }
602
- var init_rtm = () => {};
603
-
604
- // src/onepassword.ts
605
- function websiteCredentials(item, override) {
606
- const field = (label) => item.fields?.find((f) => f.label === label)?.value?.trim();
607
- const identifier = field("API Identifier"), key = field("API Key");
608
- const websiteId = override || field("website_id") || field("Website ID") || "";
609
- if (!identifier || !key)
610
- throw new Error("1Password item needs API Identifier and API Key fields");
611
- if (!/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(websiteId))
612
- throw new Error("Provide --website with the Crisp workspace UUID");
613
- return { identifier, key, websiteId };
614
- }
615
- async function loadWebsiteCredentials(item, websiteId) {
616
- const child = Bun.spawn(["op", "item", "get", item, "--format", "json"], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
617
- const timer = setTimeout(() => child.kill(), 60000);
618
- try {
619
- const [out, , status] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
620
- if (status)
621
- throw new Error("1Password could not read the item. Unlock the desktop app and authorize op.");
622
- let itemData;
623
- try {
624
- itemData = JSON.parse(out);
625
- } catch {
626
- throw new Error("1Password returned invalid JSON");
627
- }
628
- return websiteCredentials(itemData, websiteId);
629
- } finally {
630
- clearTimeout(timer);
631
- }
632
- }
633
-
634
- // src/session.ts
635
- function credentialEnvironment(credentials, base = process.env) {
636
- return {
637
- ...base,
638
- CRISPCTL_IDENTIFIER: credentials.identifier,
639
- CRISPCTL_KEY: credentials.key,
640
- CRISPCTL_WEBSITE_ID: credentials.websiteId,
641
- CRISPCTL_TIER: "website",
642
- CRISPCTL_READ_ONLY: "1"
643
- };
644
- }
645
- function credentialClient(credentials, prefix = command()) {
646
- const env = credentialEnvironment(credentials);
647
- const flags = ["--read-only", "--website", credentials.websiteId];
648
- const invoke = runner(prefix, flags, env);
649
- const commands = [];
650
- const client = readOnlyClient(createClient((args) => {
651
- commands.push(args.slice(0, 2).join(" "));
652
- return invoke(args);
653
- }, `website ${credentials.websiteId}`, listen(prefix, flags, env)));
654
- return { client, commands };
655
- }
656
- var init_session = __esm(() => {
657
- init_crispctl();
658
- init_rtm();
659
- });
660
-
661
- // src/rtm-check.ts
662
- function observeRealtime(subscribe, timeoutMs) {
663
- return new Promise((resolve, reject) => {
664
- let authenticated = false, received = 0, stop = () => {};
665
- const events = new Set;
666
- const timer = setTimeout(() => {
667
- stop();
668
- reject(new Error(authenticated ? "RTM authenticated; timed out waiting for a real event" : "RTM check timed out before authentication"));
669
- }, timeoutMs);
670
- const finish = () => {
671
- if (!authenticated || !received)
672
- return;
673
- clearTimeout(timer);
674
- stop();
675
- resolve({ authenticated: true, received, events: [...events] });
676
- };
677
- stop = subscribe((event) => {
678
- received++;
679
- events.add(event.event);
680
- finish();
681
- }, (status) => {
682
- if (status.state === "error") {
683
- clearTimeout(timer);
684
- stop();
685
- reject(new Error(status.message || "RTM check failed"));
686
- return;
687
- }
688
- if (status.state === "authenticated") {
689
- authenticated = true;
690
- finish();
691
- }
692
- });
693
- });
694
- }
695
-
696
- // src/ui/App.tsx
697
- var exports_App = {};
698
- __export(exports_App, {
699
- App: () => App
700
- });
701
- import { memo as _$memo } from "@opentui/solid";
702
- import { use as _$use } from "@opentui/solid";
703
- import { insert as _$insert } from "@opentui/solid";
704
- import { createComponent as _$createComponent } from "@opentui/solid";
705
- import { effect as _$effect } from "@opentui/solid";
706
- import { createTextNode as _$createTextNode } from "@opentui/solid";
707
- import { insertNode as _$insertNode } from "@opentui/solid";
708
- import { setProp as _$setProp } from "@opentui/solid";
709
- import { createElement as _$createElement } from "@opentui/solid";
710
- import { createEffect, createSignal, For, onCleanup, Show } from "solid-js";
711
- import { useKeyboard, useTerminalDimensions } from "@opentui/solid";
712
- function App(props) {
713
- const store = props.store;
714
- const [state, setState] = createSignal(store.state);
715
- const unsubscribe = store.subscribe(() => setState(store.state));
716
- onCleanup(unsubscribe);
717
- const [pane, setPane] = createSignal("inbox");
718
- const [cursor, setCursor] = createSignal(0);
719
- const [query, setQuery] = createSignal("");
720
- const dims = useTerminalDimensions();
721
- let textarea;
722
- let history;
723
- let inbox;
724
- const draft = () => {
725
- state();
726
- return store.draft();
727
- };
728
- const run = (fn) => void store.perform(fn);
729
- props.bindFocus?.(() => setPane(state().readOnly ? "messages" : "composer"));
730
- createEffect(() => {
731
- const text = draft().text;
732
- if (textarea && textarea.plainText !== text) {
733
- textarea.setText(text);
734
- textarea.gotoBufferEnd();
735
- }
736
- });
737
- createEffect(() => {
738
- const length = state().conversations.length;
739
- if (cursor() >= length)
740
- setCursor(Math.max(0, length - 1));
741
- });
742
- createEffect(() => {
743
- const index = cursor();
744
- inbox?.scrollTo(Math.max(0, index * 3 - 3));
745
- });
746
- const open = (id) => run(async () => {
747
- await store.open(id);
748
- setPane(state().readOnly ? "messages" : "composer");
749
- });
750
- useKeyboard((key) => {
751
- if (key.ctrl) {
752
- if (key.name === "r") {
753
- key.preventDefault();
754
- run(() => store.refresh());
755
- return;
756
- }
757
- if (key.name === "n") {
758
- key.preventDefault();
759
- if (state().active && !state().sending && !state().readOnly)
760
- store.setDraft(draft().text, !draft().note);
761
- return;
762
- }
763
- if (key.name === "e") {
764
- key.preventDefault();
765
- run(() => store.changeState());
766
- return;
767
- }
768
- if (key.name === "u") {
769
- key.preventDefault();
770
- run(() => store.markRead());
771
- return;
772
- }
773
- }
774
- if (key.name === "escape") {
775
- setPane("inbox");
776
- return;
777
- }
778
- if (key.name === "tab") {
779
- key.preventDefault();
780
- const order = state().readOnly ? ["inbox", "messages"] : ["inbox", "messages", "composer"];
781
- setPane(order[(order.indexOf(pane()) + (key.shift ? order.length - 1 : 1)) % order.length]);
782
- return;
783
- }
784
- if (pane() === "composer" || pane() === "search")
785
- return;
786
- if (key.name === "/") {
787
- key.preventDefault();
788
- setQuery(state().query);
789
- setPane("search");
790
- return;
791
- }
792
- if (key.name === "[") {
793
- run(() => store.list(state().query, Math.max(1, state().page - 1)));
794
- return;
795
- }
796
- if (key.name === "]") {
797
- run(() => store.list(state().query, state().page + 1));
798
- return;
799
- }
800
- if (pane() === "inbox") {
801
- if (key.name === "down" || key.name === "j")
802
- setCursor((i) => Math.min(state().conversations.length - 1, i + 1));
803
- if (key.name === "up" || key.name === "k")
804
- setCursor((i) => Math.max(0, i - 1));
805
- if (key.name === "return") {
806
- const c = state().conversations[cursor()];
807
- if (c)
808
- open(c.session_id);
809
- }
810
- }
811
- if (pane() === "messages") {
812
- if (["down", "j", "pagedown"].includes(key.name))
813
- history?.scrollBy(key.name === "pagedown" ? 10 : 1);
814
- if (["up", "k", "pageup"].includes(key.name))
815
- history?.scrollBy(key.name === "pageup" ? -10 : -1);
816
- }
817
- });
818
- return (() => {
819
- var _el$ = _$createElement("box"), _el$2 = _$createElement("box"), _el$3 = _$createElement("text"), _el$4 = _$createElement("b"), _el$6 = _$createElement("text"), _el$1 = _$createElement("box"), _el$10 = _$createElement("text"), _el$11 = _$createElement("box"), _el$12 = _$createElement("box"), _el$13 = _$createElement("text"), _el$15 = _$createElement("scrollbox"), _el$18 = _$createElement("text"), _el$20 = _$createElement("box"), _el$21 = _$createElement("box"), _el$22 = _$createElement("text"), _el$23 = _$createElement("b"), _el$24 = _$createElement("text"), _el$25 = _$createElement("scrollbox"), _el$30 = _$createElement("text"), _el$31 = _$createElement("text");
820
- _$insertNode(_el$, _el$2);
821
- _$insertNode(_el$, _el$11);
822
- _$insertNode(_el$, _el$30);
823
- _$insertNode(_el$, _el$31);
824
- _$setProp(_el$, "flexDirection", "column");
825
- _$setProp(_el$, "width", "100%");
826
- _$setProp(_el$, "height", "100%");
827
- _$insertNode(_el$2, _el$3);
828
- _$insertNode(_el$2, _el$6);
829
- _$insertNode(_el$2, _el$1);
830
- _$insertNode(_el$2, _el$10);
831
- _$setProp(_el$2, "height", 3);
832
- _$setProp(_el$2, "flexShrink", 0);
833
- _$setProp(_el$2, "flexDirection", "row");
834
- _$setProp(_el$2, "alignItems", "center");
835
- _$setProp(_el$2, "paddingLeft", 2);
836
- _$setProp(_el$2, "paddingRight", 2);
837
- _$insertNode(_el$3, _el$4);
838
- _$insertNode(_el$4, _$createTextNode(`crisp`));
839
- _$insertNode(_el$6, _$createTextNode(` / support inbox`));
840
- _$insert(_el$2, _$createComponent(Show, {
841
- get when() {
842
- return state().readOnly;
843
- },
844
- get children() {
845
- var _el$8 = _$createElement("text");
846
- _$insertNode(_el$8, _$createTextNode(` READ ONLY`));
847
- _$effect((_$p) => _$setProp(_el$8, "fg", color.note, _$p));
848
- return _el$8;
849
- }
850
- }), _el$1);
851
- _$insert(_el$2, _$createComponent(Show, {
852
- get when() {
853
- return state().realtime !== "off";
854
- },
855
- get children() {
856
- var _el$0 = _$createElement("text");
857
- _$insert(_el$0, () => ` RTM ${state().realtime === "authenticated" ? "live" : state().realtime}`);
858
- _$effect((_$p) => _$setProp(_el$0, "fg", state().realtime === "authenticated" ? color.green : color.note, _$p));
859
- return _el$0;
860
- }
861
- }), _el$1);
862
- _$setProp(_el$1, "flexGrow", 1);
863
- _$setProp(_el$10, "truncate", true);
864
- _$setProp(_el$10, "wrapMode", "none");
865
- _$insert(_el$10, () => clean(state().source));
866
- _$insertNode(_el$11, _el$12);
867
- _$insertNode(_el$11, _el$20);
868
- _$setProp(_el$11, "flexGrow", 1);
869
- _$setProp(_el$11, "minHeight", 0);
870
- _$setProp(_el$11, "flexDirection", "row");
871
- _$insertNode(_el$12, _el$13);
872
- _$insertNode(_el$12, _el$15);
873
- _$insertNode(_el$12, _el$18);
874
- _$setProp(_el$12, "flexShrink", 0);
875
- _$setProp(_el$12, "flexDirection", "column");
876
- _$setProp(_el$12, "border", ["right"]);
877
- _$setProp(_el$13, "paddingLeft", 1);
878
- _$setProp(_el$13, "height", 2);
879
- _$insert(_el$13, () => `INBOX \xB7 page ${state().page}${state().loading ? " \xB7 loading" : ""}`);
880
- _$insert(_el$12, _$createComponent(Show, {
881
- get when() {
882
- return pane() === "search";
883
- },
884
- get fallback() {
885
- return (() => {
886
- var _el$32 = _$createElement("text");
887
- _$setProp(_el$32, "paddingLeft", 1);
888
- _$setProp(_el$32, "height", 2);
889
- _$insert(_el$32, (() => {
890
- var _c$3 = _$memo(() => !!state().query);
891
- return () => _c$3() ? `Search: ${clean(state().query)}` : "/ Search conversations";
892
- })());
893
- _$effect((_$p) => _$setProp(_el$32, "fg", color.dim, _$p));
894
- return _el$32;
895
- })();
896
- },
897
- get children() {
898
- var _el$14 = _$createElement("input");
899
- _$setProp(_el$14, "onInput", setQuery);
900
- _$setProp(_el$14, "onSubmit", () => {
901
- setPane("inbox");
902
- setCursor(0);
903
- run(() => store.list(query(), 1));
904
- });
905
- _$setProp(_el$14, "placeholder", "Search \xB7 Enter to submit");
906
- _$effect((_p$) => {
907
- var _v$ = query(), _v$2 = pane() === "search";
908
- _v$ !== _p$.e && (_p$.e = _$setProp(_el$14, "value", _v$, _p$.e));
909
- _v$2 !== _p$.t && (_p$.t = _$setProp(_el$14, "focused", _v$2, _p$.t));
910
- return _p$;
911
- }, {
912
- e: undefined,
913
- t: undefined
914
- });
915
- return _el$14;
916
- }
917
- }), _el$15);
918
- var _ref$ = inbox;
919
- typeof _ref$ === "function" ? _$use(_ref$, _el$15) : inbox = _el$15;
920
- _$setProp(_el$15, "flexGrow", 1);
921
- _$setProp(_el$15, "minHeight", 0);
922
- _$insert(_el$15, _$createComponent(For, {
923
- get each() {
924
- return state().conversations;
925
- },
926
- children: (c, i) => (() => {
927
- var _el$33 = _$createElement("box"), _el$34 = _$createElement("text"), _el$35 = _$createElement("text");
928
- _$insertNode(_el$33, _el$34);
929
- _$insertNode(_el$33, _el$35);
930
- _$setProp(_el$33, "height", 3);
931
- _$setProp(_el$33, "flexShrink", 0);
932
- _$setProp(_el$33, "paddingLeft", 1);
933
- _$setProp(_el$33, "paddingRight", 1);
934
- _$setProp(_el$33, "flexDirection", "column");
935
- _$setProp(_el$33, "onMouseUp", () => {
936
- setCursor(i());
937
- open(c.session_id);
938
- });
939
- _$setProp(_el$34, "truncate", true);
940
- _$setProp(_el$34, "wrapMode", "none");
941
- _$insert(_el$34, () => c.state === "resolved" ? "\u2713 " : "\u25CF ", null);
942
- _$insert(_el$34, () => clean(label(c)), null);
943
- _$insert(_el$34, (() => {
944
- var _c$4 = _$memo(() => !!c.unread?.operator);
945
- return () => _c$4() ? ` (${c.unread.operator})` : "";
946
- })(), null);
947
- _$setProp(_el$35, "truncate", true);
948
- _$setProp(_el$35, "wrapMode", "none");
949
- _$insert(_el$35, () => clean(c.last_message || c.meta?.email || c.session_id));
950
- _$effect((_p$) => {
951
- var _v$25 = state().active?.session_id === c.session_id ? "#234465" : pane() === "inbox" && cursor() === i() ? "#263448" : undefined, { fg: _v$26, dim: _v$27 } = color;
952
- _v$25 !== _p$.e && (_p$.e = _$setProp(_el$33, "backgroundColor", _v$25, _p$.e));
953
- _v$26 !== _p$.t && (_p$.t = _$setProp(_el$34, "fg", _v$26, _p$.t));
954
- _v$27 !== _p$.a && (_p$.a = _$setProp(_el$35, "fg", _v$27, _p$.a));
955
- return _p$;
956
- }, {
957
- e: undefined,
958
- t: undefined,
959
- a: undefined
960
- });
961
- return _el$33;
962
- })()
963
- }), null);
964
- _$insert(_el$15, _$createComponent(Show, {
965
- get when() {
966
- return _$memo(() => !!!state().loading)() && !state().conversations.length;
967
- },
968
- get children() {
969
- var _el$16 = _$createElement("text");
970
- _$insertNode(_el$16, _$createTextNode(`No conversations on this page.`));
971
- _$setProp(_el$16, "padding", 1);
972
- _$effect((_$p) => _$setProp(_el$16, "fg", color.dim, _$p));
973
- return _el$16;
974
- }
975
- }), null);
976
- _$insertNode(_el$18, _$createTextNode(`[ previous ] next`));
977
- _$setProp(_el$18, "paddingLeft", 1);
978
- _$setProp(_el$18, "height", 2);
979
- _$insertNode(_el$20, _el$21);
980
- _$insertNode(_el$20, _el$25);
981
- _$setProp(_el$20, "flexGrow", 1);
982
- _$setProp(_el$20, "minWidth", 0);
983
- _$setProp(_el$20, "flexDirection", "column");
984
- _$insertNode(_el$21, _el$22);
985
- _$insertNode(_el$21, _el$24);
986
- _$setProp(_el$21, "height", 4);
987
- _$setProp(_el$21, "flexShrink", 0);
988
- _$setProp(_el$21, "paddingLeft", 2);
989
- _$setProp(_el$21, "paddingRight", 1);
990
- _$setProp(_el$21, "paddingTop", 1);
991
- _$setProp(_el$21, "flexDirection", "column");
992
- _$setProp(_el$21, "border", ["bottom"]);
993
- _$insertNode(_el$22, _el$23);
994
- _$setProp(_el$22, "truncate", true);
995
- _$setProp(_el$22, "wrapMode", "none");
996
- _$insert(_el$23, (() => {
997
- var _c$ = _$memo(() => !!state().active);
998
- return () => _c$() ? clean(label(state().active)) : "Choose a conversation";
999
- })());
1000
- _$insert(_el$22, (() => {
1001
- var _c$2 = _$memo(() => !!state().active);
1002
- return () => _c$2() ? ` \xB7 ${state().active.state || "unknown"}` : "";
1003
- })(), null);
1004
- _$setProp(_el$24, "truncate", true);
1005
- _$setProp(_el$24, "wrapMode", "none");
1006
- _$insert(_el$24, () => clean([state().active?.meta?.email, ...state().active?.meta?.segments || []].filter(Boolean).join(" \xB7 ")));
1007
- var _ref$2 = history;
1008
- typeof _ref$2 === "function" ? _$use(_ref$2, _el$25) : history = _el$25;
1009
- _$setProp(_el$25, "flexGrow", 1);
1010
- _$setProp(_el$25, "minHeight", 0);
1011
- _$setProp(_el$25, "stickyScroll", true);
1012
- _$setProp(_el$25, "stickyStart", "bottom");
1013
- _$setProp(_el$25, "paddingLeft", 2);
1014
- _$setProp(_el$25, "paddingRight", 2);
1015
- _$insert(_el$25, _$createComponent(For, {
1016
- get each() {
1017
- return state().messages;
1018
- },
1019
- children: (m) => (() => {
1020
- var _el$36 = _$createElement("box"), _el$37 = _$createElement("text"), _el$38 = _$createElement("b"), _el$39 = _$createElement("span"), _el$40 = _$createElement("text");
1021
- _$insertNode(_el$36, _el$37);
1022
- _$insertNode(_el$36, _el$40);
1023
- _$setProp(_el$36, "flexDirection", "column");
1024
- _$setProp(_el$36, "flexShrink", 0);
1025
- _$setProp(_el$36, "marginTop", 1);
1026
- _$setProp(_el$36, "marginBottom", 1);
1027
- _$insertNode(_el$37, _el$38);
1028
- _$insertNode(_el$37, _el$39);
1029
- _$insert(_el$38, () => clean(m.user?.nickname || (m.from === "operator" ? "Operator" : "Visitor")));
1030
- _$insert(_el$37, () => m.type === "note" ? " \xB7 INTERNAL NOTE" : "", _el$39);
1031
- _$insert(_el$39, (() => {
1032
- var _c$5 = _$memo(() => !!m.timestamp);
1033
- return () => _c$5() ? ` ${new Date(m.timestamp).toLocaleTimeString([], {
1034
- hour: "2-digit",
1035
- minute: "2-digit"
1036
- })}` : "";
1037
- })());
1038
- _$setProp(_el$40, "wrapMode", "word");
1039
- _$insert(_el$40, () => messageText(m));
1040
- _$effect((_p$) => {
1041
- var _v$28 = m.type === "note" ? color.note : m.from === "operator" ? color.blue : color.green, _v$29 = {
1042
- fg: color.dim
1043
- }, _v$30 = color.fg;
1044
- _v$28 !== _p$.e && (_p$.e = _$setProp(_el$37, "fg", _v$28, _p$.e));
1045
- _v$29 !== _p$.t && (_p$.t = _$setProp(_el$39, "style", _v$29, _p$.t));
1046
- _v$30 !== _p$.a && (_p$.a = _$setProp(_el$40, "fg", _v$30, _p$.a));
1047
- return _p$;
1048
- }, {
1049
- e: undefined,
1050
- t: undefined,
1051
- a: undefined
1052
- });
1053
- return _el$36;
1054
- })()
1055
- }), null);
1056
- _$insert(_el$25, _$createComponent(Show, {
1057
- get when() {
1058
- return _$memo(() => !!state().active)() && !state().messages.length;
1059
- },
1060
- get children() {
1061
- var _el$26 = _$createElement("text");
1062
- _$insertNode(_el$26, _$createTextNode(`No messages.`));
1063
- _$setProp(_el$26, "marginTop", 1);
1064
- _$effect((_$p) => _$setProp(_el$26, "fg", color.dim, _$p));
1065
- return _el$26;
1066
- }
1067
- }), null);
1068
- _$insert(_el$20, _$createComponent(Show, {
1069
- get when() {
1070
- return !state().readOnly;
1071
- },
1072
- get fallback() {
1073
- return (() => {
1074
- var _el$41 = _$createElement("text");
1075
- _$insertNode(_el$41, _$createTextNode(`Read only \xB7 sending and conversation changes disabled`));
1076
- _$setProp(_el$41, "height", 2);
1077
- _$setProp(_el$41, "paddingLeft", 2);
1078
- _$effect((_$p) => _$setProp(_el$41, "fg", color.note, _$p));
1079
- return _el$41;
1080
- })();
1081
- },
1082
- get children() {
1083
- var _el$28 = _$createElement("box"), _el$29 = _$createElement("textarea");
1084
- _$insertNode(_el$28, _el$29);
1085
- _$setProp(_el$28, "flexShrink", 0);
1086
- _$setProp(_el$28, "minHeight", 5);
1087
- _$setProp(_el$28, "maxHeight", 10);
1088
- _$setProp(_el$28, "marginLeft", 1);
1089
- _$setProp(_el$28, "marginRight", 1);
1090
- _$setProp(_el$28, "border", true);
1091
- _$setProp(_el$28, "borderStyle", "rounded");
1092
- _$setProp(_el$28, "onMouseUp", () => setPane("composer"));
1093
- _$use((el) => {
1094
- textarea = el;
1095
- }, _el$29);
1096
- _$setProp(_el$29, "minHeight", 3);
1097
- _$setProp(_el$29, "maxHeight", 8);
1098
- _$setProp(_el$29, "flexGrow", 1);
1099
- _$setProp(_el$29, "keyBindings", [{
1100
- name: "return",
1101
- action: "submit"
1102
- }, {
1103
- name: "return",
1104
- shift: true,
1105
- action: "newline"
1106
- }, {
1107
- name: "return",
1108
- meta: true,
1109
- action: "newline"
1110
- }, {
1111
- name: "j",
1112
- ctrl: true,
1113
- action: "newline"
1114
- }, {
1115
- name: "linefeed",
1116
- action: "newline"
1117
- }]);
1118
- _$setProp(_el$29, "onContentChange", () => {
1119
- if (textarea && state().active && !state().sending && textarea.plainText !== draft().text)
1120
- store.setDraft(textarea.plainText);
1121
- });
1122
- _$setProp(_el$29, "onSubmit", () => run(() => store.send()));
1123
- _$effect((_p$) => {
1124
- var _v$3 = draft().note ? color.note : pane() === "composer" ? color.blue : color.line, _v$4 = draft().note ? " Internal note \xB7 Ctrl+N for reply " : " Reply \xB7 Ctrl+N for internal note ", _v$5 = pane() === "composer" && !!state().active && !state().sending, { fg: _v$6, dim: _v$7 } = color, _v$8 = state().sending ? "Sending\u2026" : "Enter to send \xB7 Shift+Enter for newline";
1125
- _v$3 !== _p$.e && (_p$.e = _$setProp(_el$28, "borderColor", _v$3, _p$.e));
1126
- _v$4 !== _p$.t && (_p$.t = _$setProp(_el$28, "title", _v$4, _p$.t));
1127
- _v$5 !== _p$.a && (_p$.a = _$setProp(_el$29, "focused", _v$5, _p$.a));
1128
- _v$6 !== _p$.o && (_p$.o = _$setProp(_el$29, "textColor", _v$6, _p$.o));
1129
- _v$7 !== _p$.i && (_p$.i = _$setProp(_el$29, "placeholderColor", _v$7, _p$.i));
1130
- _v$8 !== _p$.n && (_p$.n = _$setProp(_el$29, "placeholder", _v$8, _p$.n));
1131
- return _p$;
1132
- }, {
1133
- e: undefined,
1134
- t: undefined,
1135
- a: undefined,
1136
- o: undefined,
1137
- i: undefined,
1138
- n: undefined
1139
- });
1140
- return _el$28;
1141
- }
1142
- }), null);
1143
- _$setProp(_el$30, "height", 1);
1144
- _$setProp(_el$30, "flexShrink", 0);
1145
- _$setProp(_el$30, "paddingLeft", 1);
1146
- _$setProp(_el$30, "truncate", true);
1147
- _$setProp(_el$30, "wrapMode", "none");
1148
- _$insert(_el$30, () => state().error || state().status);
1149
- _$setProp(_el$31, "height", 1);
1150
- _$setProp(_el$31, "flexShrink", 0);
1151
- _$setProp(_el$31, "paddingLeft", 1);
1152
- _$setProp(_el$31, "truncate", true);
1153
- _$setProp(_el$31, "wrapMode", "none");
1154
- _$insert(_el$31, () => state().readOnly ? "Tab panes \xB7 / search \xB7 ^R refresh \xB7 ^C quit \xB7 READ ONLY" : "Tab panes \xB7 / search \xB7 ^R refresh \xB7 ^E resolve/reopen \xB7 ^U read \xB7 ^C quit");
1155
- _$effect((_p$) => {
1156
- var { bg: _v$9, panel: _v$0, blue: _v$1, fg: _v$10 } = color, _v$11 = state().source.startsWith("DEMO") ? color.note : color.green, _v$12 = dims().width < 90 ? 25 : 34, _v$13 = pane() === "inbox" ? color.blue : color.line, { panel: _v$14, dim: _v$15, dim: _v$16, line: _v$17, fg: _v$18, dim: _v$19 } = color, _v$20 = pane() === "messages" ? ["left"] : undefined, _v$21 = color.blue, _v$22 = state().error ? color.error : color.dim, { dim: _v$23, panel: _v$24 } = color;
1157
- _v$9 !== _p$.e && (_p$.e = _$setProp(_el$, "backgroundColor", _v$9, _p$.e));
1158
- _v$0 !== _p$.t && (_p$.t = _$setProp(_el$2, "backgroundColor", _v$0, _p$.t));
1159
- _v$1 !== _p$.a && (_p$.a = _$setProp(_el$3, "fg", _v$1, _p$.a));
1160
- _v$10 !== _p$.o && (_p$.o = _$setProp(_el$6, "fg", _v$10, _p$.o));
1161
- _v$11 !== _p$.i && (_p$.i = _$setProp(_el$10, "fg", _v$11, _p$.i));
1162
- _v$12 !== _p$.n && (_p$.n = _$setProp(_el$12, "width", _v$12, _p$.n));
1163
- _v$13 !== _p$.s && (_p$.s = _$setProp(_el$12, "borderColor", _v$13, _p$.s));
1164
- _v$14 !== _p$.h && (_p$.h = _$setProp(_el$12, "backgroundColor", _v$14, _p$.h));
1165
- _v$15 !== _p$.r && (_p$.r = _$setProp(_el$13, "fg", _v$15, _p$.r));
1166
- _v$16 !== _p$.d && (_p$.d = _$setProp(_el$18, "fg", _v$16, _p$.d));
1167
- _v$17 !== _p$.l && (_p$.l = _$setProp(_el$21, "borderColor", _v$17, _p$.l));
1168
- _v$18 !== _p$.u && (_p$.u = _$setProp(_el$22, "fg", _v$18, _p$.u));
1169
- _v$19 !== _p$.c && (_p$.c = _$setProp(_el$24, "fg", _v$19, _p$.c));
1170
- _v$20 !== _p$.w && (_p$.w = _$setProp(_el$25, "border", _v$20, _p$.w));
1171
- _v$21 !== _p$.m && (_p$.m = _$setProp(_el$25, "borderColor", _v$21, _p$.m));
1172
- _v$22 !== _p$.f && (_p$.f = _$setProp(_el$30, "fg", _v$22, _p$.f));
1173
- _v$23 !== _p$.y && (_p$.y = _$setProp(_el$31, "fg", _v$23, _p$.y));
1174
- _v$24 !== _p$.g && (_p$.g = _$setProp(_el$31, "bg", _v$24, _p$.g));
1175
- return _p$;
1176
- }, {
1177
- e: undefined,
1178
- t: undefined,
1179
- a: undefined,
1180
- o: undefined,
1181
- i: undefined,
1182
- n: undefined,
1183
- s: undefined,
1184
- h: undefined,
1185
- r: undefined,
1186
- d: undefined,
1187
- l: undefined,
1188
- u: undefined,
1189
- c: undefined,
1190
- w: undefined,
1191
- m: undefined,
1192
- f: undefined,
1193
- y: undefined,
1194
- g: undefined
1195
- });
1196
- return _el$;
1197
- })();
1198
- }
1199
- var color;
1200
- var init_App = __esm(() => {
1201
- color = {
1202
- bg: "#111823",
1203
- panel: "#182333",
1204
- fg: "#dbe7f7",
1205
- dim: "#8799b2",
1206
- blue: "#54a5ff",
1207
- line: "#2b3d54",
1208
- green: "#6ad6b1",
1209
- note: "#f0c36a",
1210
- error: "#ff929b"
1211
- };
1212
- });
1213
-
1214
- // src/tui.tsx
1215
- var exports_tui = {};
1216
- __export(exports_tui, {
1217
- startTui: () => startTui
1218
- });
1219
- import { createComponent as _$createComponent2 } from "@opentui/solid";
1220
- import { createCliRenderer } from "@opentui/core";
1221
- import { render } from "@opentui/solid";
1222
- async function startTui(store, path, pollMs) {
1223
- let focus = () => {};
1224
- const control = await serve(path, controller(store, () => focus()));
1225
- let timer;
1226
- let stopped = false;
1227
- let failures = 0;
1228
- let stopRealtime = () => {};
1229
- const stop = () => {
1230
- if (stopped)
1231
- return;
1232
- stopped = true;
1233
- clearTimeout(timer);
1234
- stopRealtime();
1235
- control.stop();
1236
- };
1237
- try {
1238
- const renderer = await createCliRenderer({
1239
- onDestroy: stop,
1240
- exitOnCtrlC: true
1241
- });
1242
- await render(() => _$createComponent2(App, {
1243
- store,
1244
- bindFocus: (fn) => {
1245
- focus = fn;
1246
- }
1247
- }), renderer);
1248
- const refresh = async () => {
1249
- try {
1250
- await store.refresh();
1251
- failures = 0;
1252
- } catch (error) {
1253
- failures++;
1254
- store.update({
1255
- error: error instanceof Error ? error.message : "Refresh failed"
1256
- });
1257
- }
1258
- if (!stopped && pollMs)
1259
- timer = setTimeout(refresh, Math.min(pollMs * 2 ** failures, 900000));
1260
- };
1261
- refresh();
1262
- if (!stopped)
1263
- stopRealtime = attachRealtime(store);
1264
- } catch (error) {
1265
- stop();
1266
- throw error;
1267
- }
1268
- }
1269
- var init_tui = __esm(() => {
1270
- init_control();
1271
- init_App();
1272
- init_rtm();
1273
- });
1274
-
1275
- // src/live-readonly.ts
1276
- import { parseArgs } from "util";
1277
- async function liveReadonlyMain(args) {
1278
- const { values } = parseArgs({ args, options: {
1279
- website: { type: "string" },
1280
- item: { type: "string" },
1281
- check: { type: "boolean" },
1282
- help: { type: "boolean" },
1283
- "rtm-timeout": { type: "string" }
1284
- } });
1285
- if (values.help) {
1286
- console.log(`crisp-tui live --item ITEM [--website UUID]
1287
- crisp-tui check --item ITEM [--website UUID] [--rtm-timeout SECONDS]
1288
- Website ID defaults to the item's website_id field. API access uses crispctl --read-only. Credentials and customer content are never saved.`);
1289
- } else {
1290
- try {
1291
- if (!values.item?.trim())
1292
- throw new Error("Choose a 1Password item explicitly with --item ITEM");
1293
- const timeout = values["rtm-timeout"] === undefined ? 0 : Number(values["rtm-timeout"]);
1294
- if (values["rtm-timeout"] !== undefined && (!values.check || !Number.isInteger(timeout) || timeout < 1 || timeout > 120))
1295
- throw new Error("--rtm-timeout requires check mode and 1\u2013120 seconds");
1296
- const credentials = await loadWebsiteCredentials(values.item, values.website);
1297
- const websiteId = credentials.websiteId;
1298
- const session = credentialClient(credentials);
1299
- const store = new Store(session.client);
1300
- if (values.check) {
1301
- await store.list();
1302
- const [{ testRender }, { createComponent }, { App }] = await Promise.all([
1303
- import("@opentui/solid"),
1304
- import("solid-js"),
1305
- Promise.resolve().then(() => (init_App(), exports_App))
1306
- ]);
1307
- const ui = await testRender(() => createComponent(App, { store }), { width: 120, height: 35 });
1308
- let rendered = false;
1309
- try {
1310
- await ui.renderOnce();
1311
- rendered = ui.captureCharFrame().includes("READ ONLY");
1312
- if (!rendered)
1313
- throw new Error("Read-only indicator was not rendered");
1314
- } finally {
1315
- ui.renderer.destroy();
1316
- }
1317
- const rtm = timeout ? await observeRealtime(session.client.subscribe, timeout * 1000) : undefined;
1318
- console.log(JSON.stringify({
1319
- ok: true,
1320
- mode: "read-only",
1321
- transport: "crispctl",
1322
- website: { id: websiteId },
1323
- conversationsOnPage: store.state.conversations.length,
1324
- statesOnPage: store.state.conversations.reduce((counts, c) => {
1325
- const state = c.state || "unknown";
1326
- counts[state] = (counts[state] || 0) + 1;
1327
- return counts;
1328
- }, {}),
1329
- messagesInOpenedConversation: store.state.messages.length,
1330
- messageTypes: [...new Set(store.state.messages.map((m) => m.type))],
1331
- tuiRendered: rendered,
1332
- commands: session.commands,
1333
- rtm,
1334
- writes: 0
1335
- }, null, 2));
1336
- } else {
1337
- if (!process.stdin.isTTY || !process.stdout.isTTY)
1338
- throw new Error("Run in an interactive terminal, or use live:check for a read-only connection test");
1339
- await Promise.resolve().then(() => init_tui());
1340
- await startTui(store, socketPath("onepassword-readonly", websiteId), 0);
1341
- }
1342
- } catch (error) {
1343
- console.error(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : "Read-only check failed" }));
1344
- return 1;
1345
- }
1346
- }
1347
- return 0;
1348
- }
1349
- var init_live_readonly = __esm(() => {
1350
- init_session();
1351
- init_store();
1352
- init_control();
1353
- });
1354
-
1355
- // src/cli.ts
1356
- init_crispctl();
1357
- import { parseArgs as parseArgs2 } from "util";
1358
-
1359
- // src/demo.ts
1360
- function demoClient() {
1361
- const conversations = [
1362
- { session_id: "session_demo_1", state: "unresolved", meta: { nickname: "Demo Customer A", email: "customer-a@example.com", segments: ["billing"] }, last_message: "Can you help me find my invoice?", unread: { operator: 1 } },
1363
- { session_id: "session_demo_2", state: "unresolved", meta: { nickname: "Demo Customer B", email: "customer-b@example.com", segments: ["onboarding"] }, last_message: "\u5982\u4F55\u9080\u8ACB\u5718\u968A\u6210\u54E1\uFF1F", unread: { operator: 2 } },
1364
- { session_id: "session_demo_3", state: "resolved", meta: { nickname: "Demo Customer C", segments: ["feedback"] }, last_message: "That worked. Thank you!" }
1365
- ];
1366
- const histories = Object.fromEntries(conversations.map((c, i) => [c.session_id, [
1367
- { fingerprint: i + 1, timestamp: Date.now() - 60000, from: "user", type: "text", content: c.last_message, user: { nickname: c.meta?.nickname } }
1368
- ]]));
1369
- const get = (id) => {
1370
- const c = conversations.find((c) => c.session_id === id);
1371
- if (!c)
1372
- throw new Error(`Unknown demo conversation: ${id}`);
1373
- return c;
1374
- };
1375
- return {
1376
- label: "DEMO \xB7 local only",
1377
- async list(page, query) {
1378
- return structuredClone(page === 1 ? conversations.filter((c) => JSON.stringify(c).toLowerCase().includes(query.toLowerCase())) : []);
1379
- },
1380
- async get(id) {
1381
- return structuredClone(get(id));
1382
- },
1383
- async messages(id) {
1384
- get(id);
1385
- return structuredClone(histories[id]);
1386
- },
1387
- async reply(id, text, note) {
1388
- get(id).last_message = text;
1389
- histories[id].push({ fingerprint: Date.now(), timestamp: Date.now(), from: "operator", type: note ? "note" : "text", content: text, user: { nickname: "You" } });
1390
- },
1391
- async state(id, resolved) {
1392
- get(id).state = resolved ? "resolved" : "unresolved";
1393
- },
1394
- async read(id) {
1395
- get(id).unread = { operator: 0 };
1396
- }
1397
- };
1398
- }
1399
-
1400
- // src/cli.ts
1401
- init_control();
1402
- init_store();
1403
- init_rtm();
1404
- var help = `crisp-tui \u2014 Crisp inbox for people and agents
1405
-
1406
- crisp-tui [tui] [--demo] [--read-only] [--profile sandbox] [--website ID]
1407
- crisp-tui ctl state|screen|conversations|messages|refresh
1408
- crisp-tui ctl goto SESSION
1409
- crisp-tui ctl draft SESSION TEXT [--note] [--replace]
1410
- crisp-tui cli <crispctl arguments...>
1411
- crisp-tui live --item ITEM [--website ID] # read-only TUI using 1Password
1412
- crisp-tui check --item ITEM [--website ID] # bounded read-only connection check
1413
-
1414
- TUI: Tab cycles panes; / searches; Ctrl+N toggles reply/note;
1415
- Ctrl+R refreshes; Ctrl+E resolves/reopens; Ctrl+U marks read;
1416
- [ / ] pages inbox; Enter sends in composer; Ctrl+C quits.
1417
- Global control/TUI options: --profile, --website. --poll SECONDS (default 60;
1418
- 0 disables polling; minimum 30). CRISPCTL_BIN selects the crispctl executable.
1419
- Agent drafts are never sent automatically. Direct writes use 'cli reply ...'.
1420
- --read-only disables TUI replies, notes, state changes, mark-read and agent drafts.
1421
- Exit codes: 0 success, 1 operation error, 2 usage, 3 no running TUI.
1422
- `;
1423
- function options(args) {
1424
- const { values, positionals } = parseArgs2({ args, allowPositionals: true, strict: true, options: {
1425
- demo: { type: "boolean" },
1426
- "read-only": { type: "boolean" },
1427
- profile: { type: "string", default: process.env.CRISPCTL_PROFILE || "sandbox" },
1428
- website: { type: "string" },
1429
- poll: { type: "string", default: "60" },
1430
- note: { type: "boolean" },
1431
- replace: { type: "boolean" },
1432
- help: { type: "boolean", short: "h" }
1433
- } });
1434
- const poll = Number(values.poll);
1435
- if (!Number.isInteger(poll) || poll !== 0 && poll < 30)
1436
- throw new CliError("--poll must be 0 or an integer of at least 30 seconds", 2);
1437
- return { ...values, profile: values.profile, poll, positionals };
1438
- }
1439
- async function main(args) {
1440
- if (args[0] === "live" || args[0] === "check") {
1441
- await Promise.resolve().then(() => init_live_readonly());
1442
- return liveReadonlyMain([...args.slice(1), ...args[0] === "check" ? ["--check"] : []]);
1443
- }
1444
- if (args[0] === "cli") {
1445
- return await Bun.spawn([...command(), ...args.slice(1)], { stdin: "inherit", stdout: "inherit", stderr: "inherit" }).exited;
1446
- }
1447
- let opts;
1448
- try {
1449
- opts = options(args);
1450
- } catch (e) {
1451
- throw new CliError(e instanceof Error ? e.message : "Invalid arguments", 2);
1452
- }
1453
- if (opts.help) {
1454
- console.log(help);
1455
- return 0;
1456
- }
1457
- const [mode, verb, ...rest] = opts.positionals;
1458
- const path = socketPath(opts.profile, opts.website);
1459
- if (mode === "ctl") {
1460
- if (opts.demo || opts["read-only"] || opts.poll !== 60)
1461
- throw new CliError("--demo, --read-only and --poll are TUI options", 2);
1462
- const simple = ["state", "screen", "conversations", "messages", "refresh"];
1463
- let params = {};
1464
- if (simple.includes(verb || "") && rest.length === 0 && !opts.note && !opts.replace) {} else if (verb === "goto" && rest.length === 1 && !opts.note && !opts.replace)
1465
- params = { session: rest[0] };
1466
- else if (verb === "draft" && rest.length === 2)
1467
- params = { session: rest[0], text: rest[1], note: !!opts.note, replace: !!opts.replace };
1468
- else
1469
- throw new CliError("Usage: ctl state|screen|conversations|messages|refresh; ctl goto SESSION; ctl draft SESSION TEXT [--note] [--replace]", 2);
1470
- const result = await request(path, verb, params);
1471
- console.log(verb === "screen" ? result : JSON.stringify(result));
1472
- return 0;
1473
- }
1474
- if (mode && mode !== "tui" || verb || rest.length || opts.note || opts.replace)
1475
- throw new CliError("Unknown command or option. Use --help; API commands go after 'cli'.", 2);
1476
- if (!process.stdin.isTTY || !process.stdout.isTTY)
1477
- throw new CliError("The TUI needs an interactive terminal. Use ctl or cli for agent workflows.", 2);
1478
- const readOnly = opts["read-only"] || process.env.CRISPCTL_READ_ONLY === "1";
1479
- let client;
1480
- if (opts.demo)
1481
- client = demoClient();
1482
- else {
1483
- const flags = ["--profile", opts.profile, ...opts.website ? ["--website", opts.website] : [], ...readOnly ? ["--read-only"] : []];
1484
- const prefix = command();
1485
- const run = runner(prefix, flags);
1486
- const auth = await run(["auth", "show"]);
1487
- if (auth.tier !== "website")
1488
- throw new Error("Configure crispctl with --tier website. This TUI uses website tokens.");
1489
- if (auth.key !== "set" || !auth.identifier || !auth.website_id)
1490
- throw new Error("Incomplete crispctl website credentials. See README.md.");
1491
- client = createClient(run, `${opts.profile} \xB7 website ${auth.website_id}`, listen(prefix, flags));
1492
- }
1493
- await (await Promise.resolve().then(() => (init_tui(), exports_tui))).startTui(new Store(readOnly ? readOnlyClient(client) : client), path, opts.poll * 1000);
1494
- return 0;
1495
- }
1496
- function exitCode(error) {
1497
- return error instanceof NotRunning ? 3 : error instanceof CliError ? error.code : 1;
1498
- }
1499
-
1500
- // src/index.ts
1501
- try {
1502
- process.exitCode = await main(process.argv.slice(2));
1503
- } catch (error) {
1504
- console.error(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : "Unexpected error" }));
1505
- process.exitCode = exitCode(error);
1506
- }