@prismer/sdk 1.3.4 → 1.7.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/README.md +395 -7
- package/dist/chunk-Y6FXYEAI.mjs +10 -0
- package/dist/cli.js +1365 -12
- package/dist/index.d.mts +739 -8
- package/dist/index.d.ts +739 -8
- package/dist/index.js +2116 -12
- package/dist/index.mjs +2102 -12
- package/dist/webhook.d.mts +114 -0
- package/dist/webhook.d.ts +114 -0
- package/dist/webhook.js +200 -0
- package/dist/webhook.mjs +175 -0
- package/package.json +6 -1
package/dist/cli.js
CHANGED
|
@@ -457,6 +457,657 @@ var RealtimeSSEClient = class extends TypedEmitter {
|
|
|
457
457
|
}
|
|
458
458
|
};
|
|
459
459
|
|
|
460
|
+
// src/offline.ts
|
|
461
|
+
var OfflineEmitter = class {
|
|
462
|
+
constructor() {
|
|
463
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
464
|
+
}
|
|
465
|
+
on(event, cb) {
|
|
466
|
+
if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
467
|
+
this.listeners.get(event).add(cb);
|
|
468
|
+
return this;
|
|
469
|
+
}
|
|
470
|
+
off(event, cb) {
|
|
471
|
+
this.listeners.get(event)?.delete(cb);
|
|
472
|
+
return this;
|
|
473
|
+
}
|
|
474
|
+
emit(event, payload) {
|
|
475
|
+
const set = this.listeners.get(event);
|
|
476
|
+
if (set) for (const cb of set) {
|
|
477
|
+
try {
|
|
478
|
+
cb(payload);
|
|
479
|
+
} catch {
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
removeAllListeners() {
|
|
484
|
+
this.listeners.clear();
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
function generateId() {
|
|
488
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
489
|
+
return crypto.randomUUID();
|
|
490
|
+
}
|
|
491
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
492
|
+
const r = Math.random() * 16 | 0;
|
|
493
|
+
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
var WRITE_PATTERNS = [
|
|
497
|
+
{ method: "POST", pattern: /\/api\/im\/(messages|direct|groups)\//, opType: "message.send" },
|
|
498
|
+
{ method: "PATCH", pattern: /\/api\/im\/messages\//, opType: "message.edit" },
|
|
499
|
+
{ method: "DELETE", pattern: /\/api\/im\/messages\//, opType: "message.delete" },
|
|
500
|
+
{ method: "POST", pattern: /\/api\/im\/conversations\/[^/]+\/read/, opType: "conversation.read" }
|
|
501
|
+
];
|
|
502
|
+
function matchWriteOp(method, path2) {
|
|
503
|
+
for (const { method: m, pattern, opType } of WRITE_PATTERNS) {
|
|
504
|
+
if (method === m && pattern.test(path2)) return opType;
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
var OfflineManager = class extends OfflineEmitter {
|
|
509
|
+
constructor(storage, networkRequest, options = {}) {
|
|
510
|
+
super();
|
|
511
|
+
this.flushTimer = null;
|
|
512
|
+
this.flushing = false;
|
|
513
|
+
this._isOnline = true;
|
|
514
|
+
this._syncState = "idle";
|
|
515
|
+
this.sseSource = null;
|
|
516
|
+
this.sseReconnectTimer = null;
|
|
517
|
+
this.sseReconnectAttempts = 0;
|
|
518
|
+
/** Presence cache for realtime presence events */
|
|
519
|
+
this.presenceCache = /* @__PURE__ */ new Map();
|
|
520
|
+
this.storage = storage;
|
|
521
|
+
this.networkRequest = networkRequest;
|
|
522
|
+
this.options = {
|
|
523
|
+
syncOnConnect: options.syncOnConnect ?? true,
|
|
524
|
+
outboxRetryLimit: options.outboxRetryLimit ?? 5,
|
|
525
|
+
outboxFlushInterval: options.outboxFlushInterval ?? 1e3,
|
|
526
|
+
conflictStrategy: options.conflictStrategy ?? "server",
|
|
527
|
+
onConflict: options.onConflict,
|
|
528
|
+
syncMode: options.syncMode ?? "push",
|
|
529
|
+
quota: options.quota ? {
|
|
530
|
+
maxStorageBytes: options.quota.maxStorageBytes ?? 500 * 1024 * 1024,
|
|
531
|
+
warningThreshold: options.quota.warningThreshold ?? 0.9
|
|
532
|
+
} : void 0
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
get isOnline() {
|
|
536
|
+
return this._isOnline;
|
|
537
|
+
}
|
|
538
|
+
get syncState() {
|
|
539
|
+
return this._syncState;
|
|
540
|
+
}
|
|
541
|
+
async init() {
|
|
542
|
+
await this.storage.init();
|
|
543
|
+
this.startFlushTimer();
|
|
544
|
+
}
|
|
545
|
+
async destroy() {
|
|
546
|
+
this.stopFlushTimer();
|
|
547
|
+
this.stopContinuousSync();
|
|
548
|
+
this.removeAllListeners();
|
|
549
|
+
}
|
|
550
|
+
// ── Network state ─────────────────────────────────────────
|
|
551
|
+
setOnline(online) {
|
|
552
|
+
if (this._isOnline === online) return;
|
|
553
|
+
this._isOnline = online;
|
|
554
|
+
this.emit(online ? "network.online" : "network.offline", void 0);
|
|
555
|
+
if (online) {
|
|
556
|
+
this.flush();
|
|
557
|
+
if (this.options.syncOnConnect) {
|
|
558
|
+
if (this.options.syncMode === "push") {
|
|
559
|
+
this.startContinuousSync();
|
|
560
|
+
} else {
|
|
561
|
+
this.sync();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
} else {
|
|
565
|
+
this.stopContinuousSync();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// ── Request dispatch ──────────────────────────────────────
|
|
569
|
+
/**
|
|
570
|
+
* Dispatch an IM request. Write ops go through outbox; reads check local cache.
|
|
571
|
+
*/
|
|
572
|
+
async dispatch(method, path2, body, query) {
|
|
573
|
+
const opType = matchWriteOp(method, path2);
|
|
574
|
+
if (opType) {
|
|
575
|
+
return this.dispatchWrite(opType, method, path2, body, query);
|
|
576
|
+
}
|
|
577
|
+
if (method === "GET") {
|
|
578
|
+
const cached = await this.readFromCache(path2, query);
|
|
579
|
+
if (cached !== null) return cached;
|
|
580
|
+
}
|
|
581
|
+
try {
|
|
582
|
+
const result = await this.networkRequest(method, path2, body, query);
|
|
583
|
+
if (method === "GET") this.cacheReadResult(path2, query, result);
|
|
584
|
+
return result;
|
|
585
|
+
} catch {
|
|
586
|
+
if (!this._isOnline) {
|
|
587
|
+
return { ok: true, data: [] };
|
|
588
|
+
}
|
|
589
|
+
throw new Error("Network request failed");
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// ── Outbox: write operations ──────────────────────────────
|
|
593
|
+
async dispatchWrite(opType, method, path2, body, query) {
|
|
594
|
+
const clientId = generateId();
|
|
595
|
+
const idempotencyKey = `sdk-${clientId}`;
|
|
596
|
+
let enrichedBody = body;
|
|
597
|
+
if (body && typeof body === "object" && (opType === "message.send" || opType === "message.edit")) {
|
|
598
|
+
enrichedBody = { ...body };
|
|
599
|
+
enrichedBody.metadata = {
|
|
600
|
+
...body.metadata,
|
|
601
|
+
_idempotencyKey: idempotencyKey
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
let localMessage;
|
|
605
|
+
if (opType === "message.send" && body && typeof body === "object") {
|
|
606
|
+
const b = body;
|
|
607
|
+
const convIdMatch = path2.match(/\/(?:messages|direct|groups)\/([^/]+)/);
|
|
608
|
+
const conversationId = convIdMatch?.[1] ?? "";
|
|
609
|
+
localMessage = {
|
|
610
|
+
id: `local-${clientId}`,
|
|
611
|
+
clientId,
|
|
612
|
+
conversationId,
|
|
613
|
+
content: b.content ?? "",
|
|
614
|
+
type: b.type ?? "text",
|
|
615
|
+
senderId: "__self__",
|
|
616
|
+
parentId: b.parentId ?? null,
|
|
617
|
+
status: "pending",
|
|
618
|
+
metadata: b.metadata,
|
|
619
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
620
|
+
};
|
|
621
|
+
await this.storage.putMessages([localMessage]);
|
|
622
|
+
this.emit("message.local", localMessage);
|
|
623
|
+
}
|
|
624
|
+
const op = {
|
|
625
|
+
id: clientId,
|
|
626
|
+
type: opType,
|
|
627
|
+
method,
|
|
628
|
+
path: path2,
|
|
629
|
+
body: enrichedBody,
|
|
630
|
+
query,
|
|
631
|
+
status: "pending",
|
|
632
|
+
createdAt: Date.now(),
|
|
633
|
+
retries: 0,
|
|
634
|
+
maxRetries: this.options.outboxRetryLimit,
|
|
635
|
+
idempotencyKey,
|
|
636
|
+
localData: localMessage
|
|
637
|
+
};
|
|
638
|
+
await this.storage.enqueue(op);
|
|
639
|
+
if (this._isOnline) this.flush();
|
|
640
|
+
const optimisticResult = {
|
|
641
|
+
ok: true,
|
|
642
|
+
data: localMessage ? { conversationId: localMessage.conversationId, message: localMessage } : void 0,
|
|
643
|
+
_pending: true,
|
|
644
|
+
_clientId: clientId
|
|
645
|
+
};
|
|
646
|
+
return optimisticResult;
|
|
647
|
+
}
|
|
648
|
+
// ── Outbox flush ──────────────────────────────────────────
|
|
649
|
+
startFlushTimer() {
|
|
650
|
+
this.stopFlushTimer();
|
|
651
|
+
this.flushTimer = setInterval(() => this.flush(), this.options.outboxFlushInterval);
|
|
652
|
+
}
|
|
653
|
+
stopFlushTimer() {
|
|
654
|
+
if (this.flushTimer) {
|
|
655
|
+
clearInterval(this.flushTimer);
|
|
656
|
+
this.flushTimer = null;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
async flush() {
|
|
660
|
+
if (this.flushing || !this._isOnline) return;
|
|
661
|
+
this.flushing = true;
|
|
662
|
+
try {
|
|
663
|
+
const ops = await this.storage.dequeueReady(10);
|
|
664
|
+
for (const op of ops) {
|
|
665
|
+
this.emit("outbox.sending", { opId: op.id, type: op.type });
|
|
666
|
+
try {
|
|
667
|
+
const result = await this.networkRequest(
|
|
668
|
+
op.method,
|
|
669
|
+
op.path,
|
|
670
|
+
op.body,
|
|
671
|
+
op.query
|
|
672
|
+
);
|
|
673
|
+
if (result.ok) {
|
|
674
|
+
await this.storage.ack(op.id);
|
|
675
|
+
this.emit("outbox.confirmed", { opId: op.id, serverData: result.data });
|
|
676
|
+
if (op.type === "message.send" && op.localData) {
|
|
677
|
+
const local = op.localData;
|
|
678
|
+
const serverMsg = result.data?.message;
|
|
679
|
+
if (serverMsg) {
|
|
680
|
+
await this.storage.deleteMessage(local.id);
|
|
681
|
+
await this.storage.putMessages([{
|
|
682
|
+
id: serverMsg.id,
|
|
683
|
+
clientId: op.id,
|
|
684
|
+
conversationId: serverMsg.conversationId ?? local.conversationId,
|
|
685
|
+
content: serverMsg.content ?? local.content,
|
|
686
|
+
type: serverMsg.type ?? local.type,
|
|
687
|
+
senderId: serverMsg.senderId ?? local.senderId,
|
|
688
|
+
parentId: serverMsg.parentId,
|
|
689
|
+
status: "confirmed",
|
|
690
|
+
metadata: serverMsg.metadata ? typeof serverMsg.metadata === "string" ? JSON.parse(serverMsg.metadata) : serverMsg.metadata : void 0,
|
|
691
|
+
createdAt: serverMsg.createdAt ?? local.createdAt
|
|
692
|
+
}]);
|
|
693
|
+
this.emit("message.confirmed", { clientId: op.id, serverMessage: serverMsg });
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
const errCode = result.error?.code;
|
|
698
|
+
if (errCode && !errCode.includes("TIMEOUT") && !errCode.includes("NETWORK")) {
|
|
699
|
+
await this.storage.nack(op.id, result.error?.message ?? "Request failed", op.maxRetries);
|
|
700
|
+
this.emit("outbox.failed", { opId: op.id, error: result.error?.message ?? "Request failed", retriesLeft: 0 });
|
|
701
|
+
if (op.type === "message.send") {
|
|
702
|
+
this.emit("message.failed", { clientId: op.id, error: result.error?.message ?? "Request failed" });
|
|
703
|
+
}
|
|
704
|
+
} else {
|
|
705
|
+
await this.storage.nack(op.id, result.error?.message ?? "Transient error", op.retries + 1);
|
|
706
|
+
this.emit("outbox.failed", {
|
|
707
|
+
opId: op.id,
|
|
708
|
+
error: result.error?.message ?? "Transient error",
|
|
709
|
+
retriesLeft: op.maxRetries - op.retries - 1
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
} catch (err) {
|
|
714
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
715
|
+
await this.storage.nack(op.id, msg, op.retries + 1);
|
|
716
|
+
if (op.retries + 1 >= op.maxRetries) {
|
|
717
|
+
this.emit("outbox.failed", { opId: op.id, error: msg, retriesLeft: 0 });
|
|
718
|
+
if (op.type === "message.send") {
|
|
719
|
+
this.emit("message.failed", { clientId: op.id, error: msg });
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
} finally {
|
|
725
|
+
this.flushing = false;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
get outboxSize() {
|
|
729
|
+
return this.storage.getPendingCount();
|
|
730
|
+
}
|
|
731
|
+
// ── Sync engine ───────────────────────────────────────────
|
|
732
|
+
async sync() {
|
|
733
|
+
if (this._syncState === "syncing" || !this._isOnline) return;
|
|
734
|
+
this._syncState = "syncing";
|
|
735
|
+
this.emit("sync.start", void 0);
|
|
736
|
+
let totalNew = 0;
|
|
737
|
+
let totalUpdated = 0;
|
|
738
|
+
try {
|
|
739
|
+
let cursor = await this.storage.getCursor("global_sync") ?? "0";
|
|
740
|
+
let hasMore = true;
|
|
741
|
+
while (hasMore) {
|
|
742
|
+
const result = await this.networkRequest(
|
|
743
|
+
"GET",
|
|
744
|
+
"/api/im/sync",
|
|
745
|
+
void 0,
|
|
746
|
+
{ since: cursor, limit: "100" }
|
|
747
|
+
);
|
|
748
|
+
if (!result.ok || !result.data) {
|
|
749
|
+
throw new Error(result.error?.message ?? "Sync failed");
|
|
750
|
+
}
|
|
751
|
+
const { events, cursor: newCursor, hasMore: more } = result.data;
|
|
752
|
+
for (const event of events) {
|
|
753
|
+
await this.applySyncEvent(event);
|
|
754
|
+
if (event.type === "message.new") totalNew++;
|
|
755
|
+
if (event.type.startsWith("conversation.")) totalUpdated++;
|
|
756
|
+
}
|
|
757
|
+
cursor = String(newCursor);
|
|
758
|
+
await this.storage.setCursor("global_sync", cursor);
|
|
759
|
+
hasMore = more;
|
|
760
|
+
this.emit("sync.progress", { synced: events.length, total: events.length });
|
|
761
|
+
}
|
|
762
|
+
this._syncState = "idle";
|
|
763
|
+
this.emit("sync.complete", { newMessages: totalNew, updatedConversations: totalUpdated });
|
|
764
|
+
} catch (err) {
|
|
765
|
+
this._syncState = "error";
|
|
766
|
+
this.emit("sync.error", {
|
|
767
|
+
error: err instanceof Error ? err.message : "Sync failed",
|
|
768
|
+
willRetry: false
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
async applySyncEvent(event) {
|
|
773
|
+
switch (event.type) {
|
|
774
|
+
case "message.new": {
|
|
775
|
+
const msg = event.data;
|
|
776
|
+
await this.storage.putMessages([{
|
|
777
|
+
id: msg.id,
|
|
778
|
+
conversationId: msg.conversationId ?? event.conversationId ?? "",
|
|
779
|
+
content: msg.content ?? "",
|
|
780
|
+
type: msg.type ?? "text",
|
|
781
|
+
senderId: msg.senderId ?? "",
|
|
782
|
+
parentId: msg.parentId ?? null,
|
|
783
|
+
status: "confirmed",
|
|
784
|
+
metadata: msg.metadata,
|
|
785
|
+
createdAt: msg.createdAt ?? event.at,
|
|
786
|
+
syncSeq: event.seq
|
|
787
|
+
}]);
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
case "message.edit": {
|
|
791
|
+
const existing = await this.storage.getMessage(event.data.id);
|
|
792
|
+
if (existing) {
|
|
793
|
+
const hasLocalEdits = existing.status !== "confirmed";
|
|
794
|
+
if (hasLocalEdits && this.options.onConflict) {
|
|
795
|
+
const resolution = this.options.onConflict(existing, event);
|
|
796
|
+
if (resolution === "keep_local") break;
|
|
797
|
+
if (resolution !== "accept_remote" && typeof resolution === "object") {
|
|
798
|
+
resolution.syncSeq = event.seq;
|
|
799
|
+
await this.storage.putMessages([resolution]);
|
|
800
|
+
break;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
existing.content = event.data.content ?? existing.content;
|
|
804
|
+
existing.updatedAt = event.at;
|
|
805
|
+
existing.syncSeq = event.seq;
|
|
806
|
+
await this.storage.putMessages([existing]);
|
|
807
|
+
}
|
|
808
|
+
break;
|
|
809
|
+
}
|
|
810
|
+
case "message.delete": {
|
|
811
|
+
if (event.data?.id) await this.storage.deleteMessage(event.data.id);
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
case "conversation.create":
|
|
815
|
+
case "conversation.update": {
|
|
816
|
+
const conv = event.data;
|
|
817
|
+
await this.storage.putConversations([{
|
|
818
|
+
id: conv.id ?? event.conversationId ?? "",
|
|
819
|
+
type: conv.type ?? "direct",
|
|
820
|
+
title: conv.title,
|
|
821
|
+
unreadCount: conv.unreadCount ?? 0,
|
|
822
|
+
members: conv.members,
|
|
823
|
+
metadata: conv.metadata,
|
|
824
|
+
syncSeq: event.seq,
|
|
825
|
+
updatedAt: event.at,
|
|
826
|
+
lastMessageAt: conv.lastMessageAt
|
|
827
|
+
}]);
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
case "conversation.archive": {
|
|
831
|
+
const convId = event.data?.id ?? event.conversationId;
|
|
832
|
+
if (convId) {
|
|
833
|
+
const existing = await this.storage.getConversation(convId);
|
|
834
|
+
if (existing) {
|
|
835
|
+
existing.metadata = { ...existing.metadata, _archived: true };
|
|
836
|
+
existing.syncSeq = event.seq;
|
|
837
|
+
existing.updatedAt = event.at;
|
|
838
|
+
await this.storage.putConversations([existing]);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
case "participant.add": {
|
|
844
|
+
const convId = event.data?.conversationId ?? event.conversationId;
|
|
845
|
+
if (convId) {
|
|
846
|
+
const existing = await this.storage.getConversation(convId);
|
|
847
|
+
if (existing && existing.members) {
|
|
848
|
+
const already = existing.members.find((m) => m.userId === event.data.userId);
|
|
849
|
+
if (!already) {
|
|
850
|
+
existing.members.push({
|
|
851
|
+
userId: event.data.userId,
|
|
852
|
+
username: event.data.username ?? "",
|
|
853
|
+
displayName: event.data.displayName,
|
|
854
|
+
role: event.data.role ?? "member"
|
|
855
|
+
});
|
|
856
|
+
existing.syncSeq = event.seq;
|
|
857
|
+
existing.updatedAt = event.at;
|
|
858
|
+
await this.storage.putConversations([existing]);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
case "participant.remove": {
|
|
865
|
+
const convId = event.data?.conversationId ?? event.conversationId;
|
|
866
|
+
if (convId) {
|
|
867
|
+
const existing = await this.storage.getConversation(convId);
|
|
868
|
+
if (existing && existing.members) {
|
|
869
|
+
existing.members = existing.members.filter((m) => m.userId !== event.data.userId);
|
|
870
|
+
existing.syncSeq = event.seq;
|
|
871
|
+
existing.updatedAt = event.at;
|
|
872
|
+
await this.storage.putConversations([existing]);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
break;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Handle a realtime event (from WS/SSE) and store locally.
|
|
881
|
+
*/
|
|
882
|
+
async handleRealtimeEvent(type, payload) {
|
|
883
|
+
if (type === "message.new" && payload) {
|
|
884
|
+
await this.storage.putMessages([{
|
|
885
|
+
id: payload.id,
|
|
886
|
+
conversationId: payload.conversationId ?? "",
|
|
887
|
+
content: payload.content ?? "",
|
|
888
|
+
type: payload.type ?? "text",
|
|
889
|
+
senderId: payload.senderId ?? "",
|
|
890
|
+
parentId: payload.parentId ?? null,
|
|
891
|
+
status: "confirmed",
|
|
892
|
+
metadata: payload.metadata,
|
|
893
|
+
createdAt: payload.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
894
|
+
}]);
|
|
895
|
+
}
|
|
896
|
+
if (type === "presence.changed" && payload?.userId) {
|
|
897
|
+
this.presenceCache.set(payload.userId, {
|
|
898
|
+
status: payload.status ?? "offline",
|
|
899
|
+
lastSeen: payload.lastSeen ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
900
|
+
});
|
|
901
|
+
this.emit("presence.changed", payload);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Get cached presence status for a user.
|
|
906
|
+
*/
|
|
907
|
+
getPresence(userId) {
|
|
908
|
+
return this.presenceCache.get(userId) ?? null;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Search messages in local storage.
|
|
912
|
+
*/
|
|
913
|
+
async searchMessages(query, opts) {
|
|
914
|
+
if (this.storage.searchMessages) {
|
|
915
|
+
return this.storage.searchMessages(query, opts);
|
|
916
|
+
}
|
|
917
|
+
return [];
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Get storage size and quota info.
|
|
921
|
+
*/
|
|
922
|
+
async getQuotaStatus() {
|
|
923
|
+
const limit = this.options.quota?.maxStorageBytes ?? 500 * 1024 * 1024;
|
|
924
|
+
const threshold = this.options.quota?.warningThreshold ?? 0.9;
|
|
925
|
+
if (this.storage.getStorageSize) {
|
|
926
|
+
const size = await this.storage.getStorageSize();
|
|
927
|
+
const percentage = size.total / limit;
|
|
928
|
+
return {
|
|
929
|
+
used: size.total,
|
|
930
|
+
limit,
|
|
931
|
+
percentage,
|
|
932
|
+
warning: percentage >= threshold,
|
|
933
|
+
exceeded: percentage >= 1
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
return { used: 0, limit, percentage: 0, warning: false, exceeded: false };
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Clear old messages for a conversation (user-initiated quota management).
|
|
940
|
+
*/
|
|
941
|
+
async clearOldMessages(conversationId, keepCount) {
|
|
942
|
+
if (this.storage.clearOldMessages) {
|
|
943
|
+
return this.storage.clearOldMessages(conversationId, keepCount);
|
|
944
|
+
}
|
|
945
|
+
return 0;
|
|
946
|
+
}
|
|
947
|
+
// ── Read cache ────────────────────────────────────────────
|
|
948
|
+
async readFromCache(path2, query) {
|
|
949
|
+
if (/\/api\/im\/conversations$/.test(path2)) {
|
|
950
|
+
const convos2 = await this.storage.getConversations({ limit: 50 });
|
|
951
|
+
if (convos2.length > 0) return { ok: true, data: convos2 };
|
|
952
|
+
}
|
|
953
|
+
const msgMatch = path2.match(/\/api\/im\/messages\/([^/]+)$/);
|
|
954
|
+
if (msgMatch) {
|
|
955
|
+
const convId = msgMatch[1];
|
|
956
|
+
const limit = query?.limit ? parseInt(query.limit) : 50;
|
|
957
|
+
const messages = await this.storage.getMessages(convId, { limit, before: query?.before });
|
|
958
|
+
if (messages.length > 0) return { ok: true, data: messages };
|
|
959
|
+
}
|
|
960
|
+
if (/\/api\/im\/contacts$/.test(path2)) {
|
|
961
|
+
const contacts = await this.storage.getContacts();
|
|
962
|
+
if (contacts.length > 0) return { ok: true, data: contacts };
|
|
963
|
+
}
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
async cacheReadResult(path2, _query, result) {
|
|
967
|
+
if (!result?.ok || !result?.data) return;
|
|
968
|
+
try {
|
|
969
|
+
if (/\/api\/im\/conversations$/.test(path2) && Array.isArray(result.data)) {
|
|
970
|
+
const convos2 = result.data.map((c) => ({
|
|
971
|
+
id: c.id,
|
|
972
|
+
type: c.type ?? "direct",
|
|
973
|
+
title: c.title,
|
|
974
|
+
lastMessage: c.lastMessage,
|
|
975
|
+
lastMessageAt: c.lastMessageAt ?? c.updatedAt,
|
|
976
|
+
unreadCount: c.unreadCount ?? 0,
|
|
977
|
+
members: c.members,
|
|
978
|
+
metadata: c.metadata,
|
|
979
|
+
updatedAt: c.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
980
|
+
}));
|
|
981
|
+
await this.storage.putConversations(convos2);
|
|
982
|
+
}
|
|
983
|
+
const msgMatch = path2.match(/\/api\/im\/messages\/([^/]+)$/);
|
|
984
|
+
if (msgMatch && Array.isArray(result.data)) {
|
|
985
|
+
const messages = result.data.map((m) => ({
|
|
986
|
+
id: m.id,
|
|
987
|
+
conversationId: m.conversationId ?? msgMatch[1],
|
|
988
|
+
content: m.content ?? "",
|
|
989
|
+
type: m.type ?? "text",
|
|
990
|
+
senderId: m.senderId ?? "",
|
|
991
|
+
parentId: m.parentId ?? null,
|
|
992
|
+
status: "confirmed",
|
|
993
|
+
metadata: m.metadata,
|
|
994
|
+
createdAt: m.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
995
|
+
}));
|
|
996
|
+
await this.storage.putMessages(messages);
|
|
997
|
+
}
|
|
998
|
+
if (/\/api\/im\/contacts$/.test(path2) && Array.isArray(result.data)) {
|
|
999
|
+
await this.storage.putContacts(result.data);
|
|
1000
|
+
}
|
|
1001
|
+
} catch {
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
// ── SSE continuous sync ────────────────────────────────────
|
|
1005
|
+
/**
|
|
1006
|
+
* Start continuous sync via SSE (Server-Sent Events).
|
|
1007
|
+
* Replaces polling with real-time push when syncMode is 'push'.
|
|
1008
|
+
*/
|
|
1009
|
+
async startContinuousSync() {
|
|
1010
|
+
if (this.sseSource) return;
|
|
1011
|
+
if (typeof EventSource === "undefined") {
|
|
1012
|
+
return this.sync();
|
|
1013
|
+
}
|
|
1014
|
+
const token = this.tokenProvider?.();
|
|
1015
|
+
if (!token) {
|
|
1016
|
+
return this.sync();
|
|
1017
|
+
}
|
|
1018
|
+
const cursor = await this.storage.getCursor("global_sync") ?? "0";
|
|
1019
|
+
const baseUrl = this.getBaseUrl();
|
|
1020
|
+
const url = `${baseUrl}/api/im/sync/stream?token=${encodeURIComponent(token)}&since=${cursor}`;
|
|
1021
|
+
this._syncState = "syncing";
|
|
1022
|
+
this.emit("sync.start", void 0);
|
|
1023
|
+
this.sseReconnectAttempts = 0;
|
|
1024
|
+
try {
|
|
1025
|
+
this.sseSource = new EventSource(url);
|
|
1026
|
+
let totalNew = 0;
|
|
1027
|
+
let totalUpdated = 0;
|
|
1028
|
+
this.sseSource.addEventListener("sync", async (e) => {
|
|
1029
|
+
try {
|
|
1030
|
+
const event = JSON.parse(e.data);
|
|
1031
|
+
await this.applySyncEvent(event);
|
|
1032
|
+
await this.storage.setCursor("global_sync", String(event.seq));
|
|
1033
|
+
if (event.type === "message.new") totalNew++;
|
|
1034
|
+
if (event.type.startsWith("conversation.")) totalUpdated++;
|
|
1035
|
+
this.emit("sync.progress", { synced: 1, total: 1 });
|
|
1036
|
+
if (this.options.quota) {
|
|
1037
|
+
await this.checkQuota();
|
|
1038
|
+
}
|
|
1039
|
+
} catch {
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
this.sseSource.addEventListener("caught_up", () => {
|
|
1043
|
+
this._syncState = "idle";
|
|
1044
|
+
this.sseReconnectAttempts = 0;
|
|
1045
|
+
this.emit("sync.complete", { newMessages: totalNew, updatedConversations: totalUpdated });
|
|
1046
|
+
totalNew = 0;
|
|
1047
|
+
totalUpdated = 0;
|
|
1048
|
+
});
|
|
1049
|
+
this.sseSource.addEventListener("error", () => {
|
|
1050
|
+
this._syncState = "error";
|
|
1051
|
+
this.emit("sync.error", { error: "SSE connection error", willRetry: true });
|
|
1052
|
+
});
|
|
1053
|
+
this.sseSource.onerror = () => {
|
|
1054
|
+
if (this.sseSource?.readyState === EventSource.CLOSED) {
|
|
1055
|
+
this.sseSource = null;
|
|
1056
|
+
this._syncState = "error";
|
|
1057
|
+
this.scheduleSseReconnect();
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
} catch (err) {
|
|
1061
|
+
this._syncState = "error";
|
|
1062
|
+
this.emit("sync.error", {
|
|
1063
|
+
error: err instanceof Error ? err.message : "SSE init failed",
|
|
1064
|
+
willRetry: true
|
|
1065
|
+
});
|
|
1066
|
+
this.scheduleSseReconnect();
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Stop the SSE continuous sync connection.
|
|
1071
|
+
*/
|
|
1072
|
+
stopContinuousSync() {
|
|
1073
|
+
if (this.sseSource) {
|
|
1074
|
+
this.sseSource.close();
|
|
1075
|
+
this.sseSource = null;
|
|
1076
|
+
}
|
|
1077
|
+
if (this.sseReconnectTimer) {
|
|
1078
|
+
clearTimeout(this.sseReconnectTimer);
|
|
1079
|
+
this.sseReconnectTimer = null;
|
|
1080
|
+
}
|
|
1081
|
+
this._syncState = "idle";
|
|
1082
|
+
}
|
|
1083
|
+
scheduleSseReconnect() {
|
|
1084
|
+
if (!this._isOnline) return;
|
|
1085
|
+
this.sseReconnectAttempts++;
|
|
1086
|
+
const delay = Math.min(1e3 * Math.pow(2, this.sseReconnectAttempts - 1), 3e4);
|
|
1087
|
+
this.sseReconnectTimer = setTimeout(() => {
|
|
1088
|
+
this.sseReconnectTimer = null;
|
|
1089
|
+
if (this._isOnline) this.startContinuousSync();
|
|
1090
|
+
}, delay);
|
|
1091
|
+
}
|
|
1092
|
+
/** Get the base URL for SSE connections (strip /api/im prefix). */
|
|
1093
|
+
getBaseUrl() {
|
|
1094
|
+
return typeof window !== "undefined" ? window.location.origin : "http://localhost:3000";
|
|
1095
|
+
}
|
|
1096
|
+
// ── Quota check ─────────────────────────────────────────────
|
|
1097
|
+
async checkQuota() {
|
|
1098
|
+
if (!this.options.quota || !this.storage.getStorageSize) return;
|
|
1099
|
+
const size = await this.storage.getStorageSize();
|
|
1100
|
+
const limit = this.options.quota.maxStorageBytes;
|
|
1101
|
+
const threshold = this.options.quota.warningThreshold;
|
|
1102
|
+
const pct = size.total / limit;
|
|
1103
|
+
if (pct >= 1) {
|
|
1104
|
+
this.emit("quota.exceeded", { used: size.total, limit });
|
|
1105
|
+
} else if (pct >= threshold) {
|
|
1106
|
+
this.emit("quota.warning", { used: size.total, limit, percentage: pct });
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
|
|
460
1111
|
// src/types.ts
|
|
461
1112
|
var ENVIRONMENTS = {
|
|
462
1113
|
production: "https://prismer.cloud"
|
|
@@ -496,8 +1147,8 @@ var DirectClient = class {
|
|
|
496
1147
|
/** Get direct message history with a user */
|
|
497
1148
|
async getMessages(userId, options) {
|
|
498
1149
|
const query = {};
|
|
499
|
-
if (options?.limit) query.limit = String(options.limit);
|
|
500
|
-
if (options?.offset) query.offset = String(options.offset);
|
|
1150
|
+
if (options?.limit != null) query.limit = String(options.limit);
|
|
1151
|
+
if (options?.offset != null) query.offset = String(options.offset);
|
|
501
1152
|
return this._r("GET", `/api/im/direct/${userId}/messages`, void 0, query);
|
|
502
1153
|
}
|
|
503
1154
|
};
|
|
@@ -529,8 +1180,8 @@ var GroupsClient = class {
|
|
|
529
1180
|
/** Get group message history */
|
|
530
1181
|
async getMessages(groupId, options) {
|
|
531
1182
|
const query = {};
|
|
532
|
-
if (options?.limit) query.limit = String(options.limit);
|
|
533
|
-
if (options?.offset) query.offset = String(options.offset);
|
|
1183
|
+
if (options?.limit != null) query.limit = String(options.limit);
|
|
1184
|
+
if (options?.offset != null) query.offset = String(options.offset);
|
|
534
1185
|
return this._r("GET", `/api/im/groups/${groupId}/messages`, void 0, query);
|
|
535
1186
|
}
|
|
536
1187
|
/** Add a member to a group (owner/admin only) */
|
|
@@ -582,8 +1233,8 @@ var MessagesClient = class {
|
|
|
582
1233
|
/** Get message history for a conversation */
|
|
583
1234
|
async getHistory(conversationId, options) {
|
|
584
1235
|
const query = {};
|
|
585
|
-
if (options?.limit) query.limit = String(options.limit);
|
|
586
|
-
if (options?.offset) query.offset = String(options.offset);
|
|
1236
|
+
if (options?.limit != null) query.limit = String(options.limit);
|
|
1237
|
+
if (options?.offset != null) query.offset = String(options.offset);
|
|
587
1238
|
return this._r("GET", `/api/im/messages/${conversationId}`, void 0, query);
|
|
588
1239
|
}
|
|
589
1240
|
/** Edit a message */
|
|
@@ -643,8 +1294,8 @@ var CreditsClient = class {
|
|
|
643
1294
|
/** Get credit transaction history */
|
|
644
1295
|
async transactions(options) {
|
|
645
1296
|
const query = {};
|
|
646
|
-
if (options?.limit) query.limit = String(options.limit);
|
|
647
|
-
if (options?.offset) query.offset = String(options.offset);
|
|
1297
|
+
if (options?.limit != null) query.limit = String(options.limit);
|
|
1298
|
+
if (options?.offset != null) query.offset = String(options.offset);
|
|
648
1299
|
return this._r("GET", "/api/im/credits/transactions", void 0, query);
|
|
649
1300
|
}
|
|
650
1301
|
};
|
|
@@ -675,6 +1326,211 @@ var WorkspaceClient = class {
|
|
|
675
1326
|
return this._r("GET", "/api/im/workspace/mentions/autocomplete", void 0, q);
|
|
676
1327
|
}
|
|
677
1328
|
};
|
|
1329
|
+
function guessMimeType(fileName) {
|
|
1330
|
+
const ext = fileName.split(".").pop()?.toLowerCase() || "";
|
|
1331
|
+
const map = {
|
|
1332
|
+
png: "image/png",
|
|
1333
|
+
jpg: "image/jpeg",
|
|
1334
|
+
jpeg: "image/jpeg",
|
|
1335
|
+
gif: "image/gif",
|
|
1336
|
+
webp: "image/webp",
|
|
1337
|
+
svg: "image/svg+xml",
|
|
1338
|
+
ico: "image/x-icon",
|
|
1339
|
+
bmp: "image/bmp",
|
|
1340
|
+
pdf: "application/pdf",
|
|
1341
|
+
doc: "application/msword",
|
|
1342
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1343
|
+
xls: "application/vnd.ms-excel",
|
|
1344
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1345
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
1346
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
1347
|
+
txt: "text/plain",
|
|
1348
|
+
csv: "text/csv",
|
|
1349
|
+
html: "text/html",
|
|
1350
|
+
css: "text/css",
|
|
1351
|
+
js: "text/javascript",
|
|
1352
|
+
json: "application/json",
|
|
1353
|
+
xml: "application/xml",
|
|
1354
|
+
md: "text/markdown",
|
|
1355
|
+
yaml: "text/yaml",
|
|
1356
|
+
yml: "text/yaml",
|
|
1357
|
+
zip: "application/zip",
|
|
1358
|
+
gz: "application/gzip",
|
|
1359
|
+
tar: "application/x-tar",
|
|
1360
|
+
mp3: "audio/mpeg",
|
|
1361
|
+
wav: "audio/wav",
|
|
1362
|
+
mp4: "video/mp4",
|
|
1363
|
+
webm: "video/webm"
|
|
1364
|
+
};
|
|
1365
|
+
return map[ext] || "application/octet-stream";
|
|
1366
|
+
}
|
|
1367
|
+
var FilesClient = class {
|
|
1368
|
+
constructor(_r, _baseUrl, _fetchFn, _getAuthHeaders) {
|
|
1369
|
+
this._r = _r;
|
|
1370
|
+
this._baseUrl = _baseUrl;
|
|
1371
|
+
this._fetchFn = _fetchFn;
|
|
1372
|
+
this._getAuthHeaders = _getAuthHeaders;
|
|
1373
|
+
}
|
|
1374
|
+
/** Get a presigned upload URL */
|
|
1375
|
+
async presign(options) {
|
|
1376
|
+
return this._r("POST", "/api/im/files/presign", options);
|
|
1377
|
+
}
|
|
1378
|
+
/** Confirm an uploaded file (triggers validation + CDN activation) */
|
|
1379
|
+
async confirm(uploadId) {
|
|
1380
|
+
return this._r("POST", "/api/im/files/confirm", { uploadId });
|
|
1381
|
+
}
|
|
1382
|
+
/** Get storage quota */
|
|
1383
|
+
async quota() {
|
|
1384
|
+
return this._r("GET", "/api/im/files/quota");
|
|
1385
|
+
}
|
|
1386
|
+
/** Delete a file */
|
|
1387
|
+
async delete(uploadId) {
|
|
1388
|
+
return this._r("DELETE", `/api/im/files/${uploadId}`);
|
|
1389
|
+
}
|
|
1390
|
+
/** List allowed MIME types */
|
|
1391
|
+
async types() {
|
|
1392
|
+
return this._r("GET", "/api/im/files/types");
|
|
1393
|
+
}
|
|
1394
|
+
/** Initialize a multipart upload (for files > 10 MB) */
|
|
1395
|
+
async initMultipart(opts) {
|
|
1396
|
+
return this._r("POST", "/api/im/files/upload/init", opts);
|
|
1397
|
+
}
|
|
1398
|
+
/** Complete a multipart upload */
|
|
1399
|
+
async completeMultipart(uploadId, parts) {
|
|
1400
|
+
return this._r("POST", "/api/im/files/upload/complete", { uploadId, parts });
|
|
1401
|
+
}
|
|
1402
|
+
// --------------------------------------------------------------------------
|
|
1403
|
+
// High-level convenience methods
|
|
1404
|
+
// --------------------------------------------------------------------------
|
|
1405
|
+
/**
|
|
1406
|
+
* Upload a file (full lifecycle: presign → upload → confirm).
|
|
1407
|
+
*
|
|
1408
|
+
* @param input - File, Blob, Buffer, Uint8Array, or file path (Node.js string)
|
|
1409
|
+
* @param opts - Optional fileName, mimeType, onProgress
|
|
1410
|
+
* @returns Confirmed upload result with CDN URL
|
|
1411
|
+
*/
|
|
1412
|
+
async upload(input, opts) {
|
|
1413
|
+
let bytes;
|
|
1414
|
+
let fileName;
|
|
1415
|
+
if (typeof input === "string") {
|
|
1416
|
+
const fs2 = await import("fs");
|
|
1417
|
+
const path2 = await import("path");
|
|
1418
|
+
const buf = await fs2.promises.readFile(input);
|
|
1419
|
+
bytes = new Uint8Array(buf);
|
|
1420
|
+
fileName = opts?.fileName || path2.basename(input);
|
|
1421
|
+
} else if (typeof Blob !== "undefined" && input instanceof Blob) {
|
|
1422
|
+
const ab = await input.arrayBuffer();
|
|
1423
|
+
bytes = new Uint8Array(ab);
|
|
1424
|
+
fileName = opts?.fileName || (input instanceof File ? input.name : "");
|
|
1425
|
+
if (!fileName) throw new Error("fileName is required when uploading Blob without name");
|
|
1426
|
+
} else if (input instanceof Uint8Array) {
|
|
1427
|
+
bytes = input;
|
|
1428
|
+
fileName = opts?.fileName || "";
|
|
1429
|
+
if (!fileName) throw new Error("fileName is required when uploading Buffer or Uint8Array");
|
|
1430
|
+
} else {
|
|
1431
|
+
throw new Error("Unsupported input type");
|
|
1432
|
+
}
|
|
1433
|
+
const fileSize = bytes.byteLength;
|
|
1434
|
+
const mimeType = opts?.mimeType || guessMimeType(fileName);
|
|
1435
|
+
if (fileSize > 50 * 1024 * 1024) {
|
|
1436
|
+
throw new Error("File exceeds maximum size of 50 MB");
|
|
1437
|
+
}
|
|
1438
|
+
if (fileSize <= 10 * 1024 * 1024) {
|
|
1439
|
+
return this._uploadSimple(bytes, fileName, fileSize, mimeType, opts?.onProgress);
|
|
1440
|
+
}
|
|
1441
|
+
return this._uploadMultipart(bytes, fileName, fileSize, mimeType, opts?.onProgress);
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Upload a file and send it as a message in one call.
|
|
1445
|
+
*
|
|
1446
|
+
* @param conversationId - Target conversation
|
|
1447
|
+
* @param input - File input (same as upload())
|
|
1448
|
+
* @param opts - Upload options + optional message content/parentId
|
|
1449
|
+
*/
|
|
1450
|
+
async sendFile(conversationId, input, opts) {
|
|
1451
|
+
const uploaded = await this.upload(input, opts);
|
|
1452
|
+
const msgRes = await this._r("POST", `/api/im/messages/${conversationId}`, {
|
|
1453
|
+
content: opts?.content || uploaded.fileName,
|
|
1454
|
+
type: "file",
|
|
1455
|
+
metadata: {
|
|
1456
|
+
uploadId: uploaded.uploadId,
|
|
1457
|
+
fileUrl: uploaded.cdnUrl,
|
|
1458
|
+
fileName: uploaded.fileName,
|
|
1459
|
+
fileSize: uploaded.fileSize,
|
|
1460
|
+
mimeType: uploaded.mimeType
|
|
1461
|
+
},
|
|
1462
|
+
parentId: opts?.parentId
|
|
1463
|
+
});
|
|
1464
|
+
if (!msgRes.ok) {
|
|
1465
|
+
throw new Error(msgRes.error?.message || "Failed to send file message");
|
|
1466
|
+
}
|
|
1467
|
+
return { upload: uploaded, message: msgRes.data };
|
|
1468
|
+
}
|
|
1469
|
+
// --------------------------------------------------------------------------
|
|
1470
|
+
// Private upload helpers
|
|
1471
|
+
// --------------------------------------------------------------------------
|
|
1472
|
+
async _uploadSimple(bytes, fileName, fileSize, mimeType, onProgress) {
|
|
1473
|
+
const presignRes = await this.presign({ fileName, fileSize, mimeType });
|
|
1474
|
+
if (!presignRes.ok || !presignRes.data) {
|
|
1475
|
+
throw new Error(presignRes.error?.message || "Presign failed");
|
|
1476
|
+
}
|
|
1477
|
+
const { uploadId, url, fields } = presignRes.data;
|
|
1478
|
+
const formData = new FormData();
|
|
1479
|
+
const isS3 = url.startsWith("http");
|
|
1480
|
+
const uploadUrl = isS3 ? url : `${this._baseUrl}${url}`;
|
|
1481
|
+
if (isS3) {
|
|
1482
|
+
for (const [k, v] of Object.entries(fields)) formData.append(k, v);
|
|
1483
|
+
}
|
|
1484
|
+
const ab = new ArrayBuffer(bytes.byteLength);
|
|
1485
|
+
new Uint8Array(ab).set(bytes);
|
|
1486
|
+
formData.append("file", new Blob([ab], { type: mimeType }), fileName);
|
|
1487
|
+
const headers = {};
|
|
1488
|
+
if (!isS3) Object.assign(headers, this._getAuthHeaders());
|
|
1489
|
+
const resp = await this._fetchFn(uploadUrl, { method: "POST", body: formData, headers });
|
|
1490
|
+
if (!resp.ok) {
|
|
1491
|
+
const text = await resp.text();
|
|
1492
|
+
throw new Error(`Upload failed (${resp.status}): ${text}`);
|
|
1493
|
+
}
|
|
1494
|
+
onProgress?.(fileSize, fileSize);
|
|
1495
|
+
const confirmRes = await this.confirm(uploadId);
|
|
1496
|
+
if (!confirmRes.ok || !confirmRes.data) {
|
|
1497
|
+
throw new Error(confirmRes.error?.message || "Confirm failed");
|
|
1498
|
+
}
|
|
1499
|
+
return confirmRes.data;
|
|
1500
|
+
}
|
|
1501
|
+
async _uploadMultipart(bytes, fileName, fileSize, mimeType, onProgress) {
|
|
1502
|
+
const initRes = await this.initMultipart({ fileName, fileSize, mimeType });
|
|
1503
|
+
if (!initRes.ok || !initRes.data) {
|
|
1504
|
+
throw new Error(initRes.error?.message || "Multipart init failed");
|
|
1505
|
+
}
|
|
1506
|
+
const { uploadId, parts: partUrls } = initRes.data;
|
|
1507
|
+
const CHUNK_SIZE = 5 * 1024 * 1024;
|
|
1508
|
+
const completedParts = [];
|
|
1509
|
+
let uploaded = 0;
|
|
1510
|
+
for (const part of partUrls) {
|
|
1511
|
+
const start = (part.partNumber - 1) * CHUNK_SIZE;
|
|
1512
|
+
const end = Math.min(start + CHUNK_SIZE, fileSize);
|
|
1513
|
+
const chunk = bytes.slice(start, end);
|
|
1514
|
+
const isS3 = part.url.startsWith("http");
|
|
1515
|
+
const partUrl = isS3 ? part.url : `${this._baseUrl}${part.url}`;
|
|
1516
|
+
const headers = { "Content-Type": mimeType };
|
|
1517
|
+
if (!isS3) Object.assign(headers, this._getAuthHeaders());
|
|
1518
|
+
const resp = await this._fetchFn(partUrl, { method: "PUT", body: chunk, headers });
|
|
1519
|
+
if (!resp.ok) {
|
|
1520
|
+
throw new Error(`Part ${part.partNumber} upload failed (${resp.status})`);
|
|
1521
|
+
}
|
|
1522
|
+
const etag = resp.headers.get("ETag") || `"part-${part.partNumber}"`;
|
|
1523
|
+
completedParts.push({ partNumber: part.partNumber, etag });
|
|
1524
|
+
uploaded += chunk.byteLength;
|
|
1525
|
+
onProgress?.(uploaded, fileSize);
|
|
1526
|
+
}
|
|
1527
|
+
const completeRes = await this.completeMultipart(uploadId, completedParts);
|
|
1528
|
+
if (!completeRes.ok || !completeRes.data) {
|
|
1529
|
+
throw new Error(completeRes.error?.message || "Multipart complete failed");
|
|
1530
|
+
}
|
|
1531
|
+
return completeRes.data;
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
678
1534
|
var IMRealtimeClient = class {
|
|
679
1535
|
constructor(_wsBase) {
|
|
680
1536
|
this._wsBase = _wsBase;
|
|
@@ -698,7 +1554,7 @@ var IMRealtimeClient = class {
|
|
|
698
1554
|
}
|
|
699
1555
|
};
|
|
700
1556
|
var IMClient = class {
|
|
701
|
-
constructor(request, wsBase) {
|
|
1557
|
+
constructor(request, wsBase, fetchFn, getAuthHeaders, offlineManager) {
|
|
702
1558
|
this.account = new AccountClient(request);
|
|
703
1559
|
this.direct = new DirectClient(request);
|
|
704
1560
|
this.groups = new GroupsClient(request);
|
|
@@ -708,7 +1564,9 @@ var IMClient = class {
|
|
|
708
1564
|
this.bindings = new BindingsClient(request);
|
|
709
1565
|
this.credits = new CreditsClient(request);
|
|
710
1566
|
this.workspace = new WorkspaceClient(request);
|
|
1567
|
+
this.files = new FilesClient(request, wsBase, fetchFn, getAuthHeaders);
|
|
711
1568
|
this.realtime = new IMRealtimeClient(wsBase);
|
|
1569
|
+
this.offline = offlineManager ?? null;
|
|
712
1570
|
}
|
|
713
1571
|
/** IM health check */
|
|
714
1572
|
async health() {
|
|
@@ -717,6 +1575,7 @@ var IMClient = class {
|
|
|
717
1575
|
};
|
|
718
1576
|
var PrismerClient = class {
|
|
719
1577
|
constructor(config = {}) {
|
|
1578
|
+
this._offlineManager = null;
|
|
720
1579
|
if (config.apiKey && !config.apiKey.startsWith("sk-prismer-") && !config.apiKey.startsWith("eyJ")) {
|
|
721
1580
|
console.warn('Warning: API key should start with "sk-prismer-" (or "eyJ" for IM JWT)');
|
|
722
1581
|
}
|
|
@@ -726,11 +1585,32 @@ var PrismerClient = class {
|
|
|
726
1585
|
this.timeout = config.timeout || 3e4;
|
|
727
1586
|
this.fetchFn = config.fetch || fetch;
|
|
728
1587
|
this.imAgent = config.imAgent;
|
|
1588
|
+
if (config.offline) {
|
|
1589
|
+
this._offlineManager = new OfflineManager(
|
|
1590
|
+
config.offline.storage,
|
|
1591
|
+
(m, p, b, q) => this._request(m, p, b, q),
|
|
1592
|
+
config.offline
|
|
1593
|
+
);
|
|
1594
|
+
this._offlineManager.init().catch(
|
|
1595
|
+
(err) => console.warn("[PrismerSDK] Offline storage init failed:", err)
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
const imRequest = this._offlineManager ? (m, p, b, q) => this._offlineManager.dispatch(m, p, b, q) : (m, p, b, q) => this._request(m, p, b, q);
|
|
729
1599
|
this.im = new IMClient(
|
|
730
|
-
|
|
731
|
-
this.baseUrl
|
|
1600
|
+
imRequest,
|
|
1601
|
+
this.baseUrl,
|
|
1602
|
+
this.fetchFn,
|
|
1603
|
+
() => this._getAuthHeaders(),
|
|
1604
|
+
this._offlineManager
|
|
732
1605
|
);
|
|
733
1606
|
}
|
|
1607
|
+
/** Build auth headers for raw HTTP requests (used by file upload) */
|
|
1608
|
+
_getAuthHeaders() {
|
|
1609
|
+
const headers = {};
|
|
1610
|
+
if (this.apiKey) headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
1611
|
+
if (this.imAgent) headers["X-IM-Agent"] = this.imAgent;
|
|
1612
|
+
return headers;
|
|
1613
|
+
}
|
|
734
1614
|
/**
|
|
735
1615
|
* Set or update the auth token (API key or IM JWT).
|
|
736
1616
|
* Useful after anonymous registration to set the returned JWT.
|
|
@@ -738,10 +1618,16 @@ var PrismerClient = class {
|
|
|
738
1618
|
setToken(token) {
|
|
739
1619
|
this.apiKey = token;
|
|
740
1620
|
}
|
|
1621
|
+
/** Cleanup resources (offline manager, timers). Call when disposing the client. */
|
|
1622
|
+
async destroy() {
|
|
1623
|
+
if (this._offlineManager) {
|
|
1624
|
+
await this._offlineManager.destroy();
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
741
1627
|
// --------------------------------------------------------------------------
|
|
742
1628
|
// Internal request helper
|
|
743
1629
|
// --------------------------------------------------------------------------
|
|
744
|
-
async _request(method, path2, body, query) {
|
|
1630
|
+
async _request(method, path2, body, query, _isRetry) {
|
|
745
1631
|
const controller = new AbortController();
|
|
746
1632
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
747
1633
|
try {
|
|
@@ -763,6 +1649,16 @@ var PrismerClient = class {
|
|
|
763
1649
|
}
|
|
764
1650
|
const response = await this.fetchFn(url, init);
|
|
765
1651
|
const data = await response.json();
|
|
1652
|
+
if (response.status === 401 && this.apiKey.startsWith("eyJ") && !_isRetry && !path2.includes("/token/refresh")) {
|
|
1653
|
+
try {
|
|
1654
|
+
const refreshRes = await this._request("POST", "/api/im/token/refresh", void 0, void 0, true);
|
|
1655
|
+
if (refreshRes?.ok && refreshRes?.data?.token) {
|
|
1656
|
+
this.apiKey = refreshRes.data.token;
|
|
1657
|
+
return this._request(method, path2, body, query, true);
|
|
1658
|
+
}
|
|
1659
|
+
} catch {
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
766
1662
|
if (!response.ok) {
|
|
767
1663
|
const err = data.error || { code: "HTTP_ERROR", message: `Request failed with status ${response.status}` };
|
|
768
1664
|
return { ...data, success: false, ok: false, error: err };
|
|
@@ -876,6 +1772,28 @@ function setNestedValue(obj, dotPath, value) {
|
|
|
876
1772
|
}
|
|
877
1773
|
current[parts[parts.length - 1]] = value;
|
|
878
1774
|
}
|
|
1775
|
+
function getIMClient() {
|
|
1776
|
+
const cfg = readConfig();
|
|
1777
|
+
const token = cfg?.auth?.im_token;
|
|
1778
|
+
if (!token) {
|
|
1779
|
+
console.error('No IM token. Run "prismer register" first.');
|
|
1780
|
+
process.exit(1);
|
|
1781
|
+
}
|
|
1782
|
+
const env = cfg?.default?.environment || "production";
|
|
1783
|
+
const baseUrl = cfg?.default?.base_url || "";
|
|
1784
|
+
return new PrismerClient({ apiKey: token, environment: env, ...baseUrl ? { baseUrl } : {} });
|
|
1785
|
+
}
|
|
1786
|
+
function getAPIClient() {
|
|
1787
|
+
const cfg = readConfig();
|
|
1788
|
+
const apiKey = cfg?.default?.api_key;
|
|
1789
|
+
if (!apiKey) {
|
|
1790
|
+
console.error('No API key. Run "prismer init <api-key>" first.');
|
|
1791
|
+
process.exit(1);
|
|
1792
|
+
}
|
|
1793
|
+
const env = cfg?.default?.environment || "production";
|
|
1794
|
+
const baseUrl = cfg?.default?.base_url || "";
|
|
1795
|
+
return new PrismerClient({ apiKey, environment: env, ...baseUrl ? { baseUrl } : {} });
|
|
1796
|
+
}
|
|
879
1797
|
var program = new import_commander.Command();
|
|
880
1798
|
program.name("prismer").description("Prismer Cloud SDK CLI").version(cliVersion);
|
|
881
1799
|
program.command("init <api-key>").description("Store API key in ~/.prismer/config.toml").action((apiKey) => {
|
|
@@ -1020,4 +1938,439 @@ configCmd.command("set <key> <value>").description("Set a config value (e.g., pr
|
|
|
1020
1938
|
writeConfig(config);
|
|
1021
1939
|
console.log(`Set ${key} = ${value}`);
|
|
1022
1940
|
});
|
|
1941
|
+
var im = program.command("im").description("IM messaging commands");
|
|
1942
|
+
im.command("me").description("Show current identity and stats").option("--json", "JSON output").action(async (opts) => {
|
|
1943
|
+
const client = getIMClient();
|
|
1944
|
+
const res = await client.im.account.me();
|
|
1945
|
+
if (!res.ok) {
|
|
1946
|
+
console.error("Error:", res.error);
|
|
1947
|
+
process.exit(1);
|
|
1948
|
+
}
|
|
1949
|
+
const d = res.data;
|
|
1950
|
+
if (opts.json) {
|
|
1951
|
+
console.log(JSON.stringify(d, null, 2));
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
console.log(`Display Name: ${d?.user?.displayName || "-"}`);
|
|
1955
|
+
console.log(`Username: ${d?.user?.username || "-"}`);
|
|
1956
|
+
console.log(`Role: ${d?.user?.role || "-"}`);
|
|
1957
|
+
console.log(`Agent Type: ${d?.agentCard?.agentType || "-"}`);
|
|
1958
|
+
console.log(`Credits: ${d?.credits?.balance ?? "-"}`);
|
|
1959
|
+
console.log(`Messages: ${d?.stats?.messagesSent ?? "-"}`);
|
|
1960
|
+
console.log(`Unread: ${d?.stats?.unreadCount ?? "-"}`);
|
|
1961
|
+
});
|
|
1962
|
+
im.command("health").description("Check IM service health").action(async () => {
|
|
1963
|
+
const client = getIMClient();
|
|
1964
|
+
const res = await client.im.health();
|
|
1965
|
+
console.log(`IM Service: ${res.ok ? "OK" : "ERROR"}`);
|
|
1966
|
+
if (!res.ok) {
|
|
1967
|
+
console.error(res.error);
|
|
1968
|
+
process.exit(1);
|
|
1969
|
+
}
|
|
1970
|
+
});
|
|
1971
|
+
im.command("send").description("Send a direct message").argument("<user-id>", "Target user ID").argument("<message>", "Message content").option("--json", "JSON output").action(async (userId, message, opts) => {
|
|
1972
|
+
const client = getIMClient();
|
|
1973
|
+
const res = await client.im.direct.send(userId, message);
|
|
1974
|
+
if (!res.ok) {
|
|
1975
|
+
console.error("Error:", res.error);
|
|
1976
|
+
process.exit(1);
|
|
1977
|
+
}
|
|
1978
|
+
if (opts.json) {
|
|
1979
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
console.log(`Message sent (conversationId: ${res.data?.conversationId})`);
|
|
1983
|
+
});
|
|
1984
|
+
im.command("messages").description("View direct message history").argument("<user-id>", "Target user ID").option("-n, --limit <n>", "Max messages", "20").option("--json", "JSON output").action(async (userId, opts) => {
|
|
1985
|
+
const client = getIMClient();
|
|
1986
|
+
const res = await client.im.direct.getMessages(userId, { limit: parseInt(opts.limit) });
|
|
1987
|
+
if (!res.ok) {
|
|
1988
|
+
console.error("Error:", res.error);
|
|
1989
|
+
process.exit(1);
|
|
1990
|
+
}
|
|
1991
|
+
const msgs = res.data || [];
|
|
1992
|
+
if (opts.json) {
|
|
1993
|
+
console.log(JSON.stringify(msgs, null, 2));
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
if (msgs.length === 0) {
|
|
1997
|
+
console.log("No messages.");
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
for (const m of msgs) {
|
|
2001
|
+
const ts = m.createdAt ? new Date(m.createdAt).toLocaleString() : "";
|
|
2002
|
+
console.log(`[${ts}] ${m.senderId || "?"}: ${m.content}`);
|
|
2003
|
+
}
|
|
2004
|
+
});
|
|
2005
|
+
im.command("discover").description("Discover available agents").option("--type <type>", "Filter by type").option("--capability <cap>", "Filter by capability").option("--json", "JSON output").action(async (opts) => {
|
|
2006
|
+
const client = getIMClient();
|
|
2007
|
+
const discoverOpts = {};
|
|
2008
|
+
if (opts.type) discoverOpts.type = opts.type;
|
|
2009
|
+
if (opts.capability) discoverOpts.capability = opts.capability;
|
|
2010
|
+
const res = await client.im.contacts.discover(discoverOpts);
|
|
2011
|
+
if (!res.ok) {
|
|
2012
|
+
console.error("Error:", res.error);
|
|
2013
|
+
process.exit(1);
|
|
2014
|
+
}
|
|
2015
|
+
const agents = res.data || [];
|
|
2016
|
+
if (opts.json) {
|
|
2017
|
+
console.log(JSON.stringify(agents, null, 2));
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
if (agents.length === 0) {
|
|
2021
|
+
console.log("No agents found.");
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
console.log("Username".padEnd(20) + "Type".padEnd(14) + "Status".padEnd(10) + "Display Name");
|
|
2025
|
+
for (const a of agents) {
|
|
2026
|
+
console.log(`${(a.username || "").padEnd(20)}${(a.agentType || "").padEnd(14)}${(a.status || "").padEnd(10)}${a.displayName || ""}`);
|
|
2027
|
+
}
|
|
2028
|
+
});
|
|
2029
|
+
im.command("contacts").description("List contacts").option("--json", "JSON output").action(async (opts) => {
|
|
2030
|
+
const client = getIMClient();
|
|
2031
|
+
const res = await client.im.contacts.list();
|
|
2032
|
+
if (!res.ok) {
|
|
2033
|
+
console.error("Error:", res.error);
|
|
2034
|
+
process.exit(1);
|
|
2035
|
+
}
|
|
2036
|
+
const contacts = res.data || [];
|
|
2037
|
+
if (opts.json) {
|
|
2038
|
+
console.log(JSON.stringify(contacts, null, 2));
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
if (contacts.length === 0) {
|
|
2042
|
+
console.log("No contacts.");
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
console.log("Username".padEnd(20) + "Role".padEnd(10) + "Unread".padEnd(8) + "Display Name");
|
|
2046
|
+
for (const c of contacts) {
|
|
2047
|
+
console.log(`${(c.username || "").padEnd(20)}${(c.role || "").padEnd(10)}${String(c.unreadCount ?? 0).padEnd(8)}${c.displayName || ""}`);
|
|
2048
|
+
}
|
|
2049
|
+
});
|
|
2050
|
+
var groups = im.command("groups").description("Group management");
|
|
2051
|
+
groups.command("list").description("List groups").option("--json", "JSON output").action(async (opts) => {
|
|
2052
|
+
const client = getIMClient();
|
|
2053
|
+
const res = await client.im.groups.list();
|
|
2054
|
+
if (!res.ok) {
|
|
2055
|
+
console.error("Error:", res.error);
|
|
2056
|
+
process.exit(1);
|
|
2057
|
+
}
|
|
2058
|
+
const list = res.data || [];
|
|
2059
|
+
if (opts.json) {
|
|
2060
|
+
console.log(JSON.stringify(list, null, 2));
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2063
|
+
if (list.length === 0) {
|
|
2064
|
+
console.log("No groups.");
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
for (const g of list) {
|
|
2068
|
+
console.log(`${g.groupId || ""} ${g.title || ""} (${g.members?.length || "?"} members)`);
|
|
2069
|
+
}
|
|
2070
|
+
});
|
|
2071
|
+
groups.command("create").description("Create a group").argument("<title>", "Group title").option("-m, --members <ids>", "Comma-separated member IDs").option("--json", "JSON output").action(async (title, opts) => {
|
|
2072
|
+
const client = getIMClient();
|
|
2073
|
+
const members = opts.members ? opts.members.split(",").map((s) => s.trim()) : [];
|
|
2074
|
+
const res = await client.im.groups.create({ title, members });
|
|
2075
|
+
if (!res.ok) {
|
|
2076
|
+
console.error("Error:", res.error);
|
|
2077
|
+
process.exit(1);
|
|
2078
|
+
}
|
|
2079
|
+
if (opts.json) {
|
|
2080
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
console.log(`Group created (groupId: ${res.data?.groupId})`);
|
|
2084
|
+
});
|
|
2085
|
+
groups.command("send").description("Send message to group").argument("<group-id>", "Group ID").argument("<message>", "Message content").option("--json", "JSON output").action(async (groupId, message, opts) => {
|
|
2086
|
+
const client = getIMClient();
|
|
2087
|
+
const res = await client.im.groups.send(groupId, message);
|
|
2088
|
+
if (!res.ok) {
|
|
2089
|
+
console.error("Error:", res.error);
|
|
2090
|
+
process.exit(1);
|
|
2091
|
+
}
|
|
2092
|
+
if (opts.json) {
|
|
2093
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
console.log("Message sent to group.");
|
|
2097
|
+
});
|
|
2098
|
+
groups.command("messages").description("View group message history").argument("<group-id>", "Group ID").option("-n, --limit <n>", "Max messages", "20").option("--json", "JSON output").action(async (groupId, opts) => {
|
|
2099
|
+
const client = getIMClient();
|
|
2100
|
+
const res = await client.im.groups.getMessages(groupId, { limit: parseInt(opts.limit) });
|
|
2101
|
+
if (!res.ok) {
|
|
2102
|
+
console.error("Error:", res.error);
|
|
2103
|
+
process.exit(1);
|
|
2104
|
+
}
|
|
2105
|
+
const msgs = res.data || [];
|
|
2106
|
+
if (opts.json) {
|
|
2107
|
+
console.log(JSON.stringify(msgs, null, 2));
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
if (msgs.length === 0) {
|
|
2111
|
+
console.log("No messages.");
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
for (const m of msgs) {
|
|
2115
|
+
const ts = m.createdAt ? new Date(m.createdAt).toLocaleString() : "";
|
|
2116
|
+
console.log(`[${ts}] ${m.senderId || "?"}: ${m.content}`);
|
|
2117
|
+
}
|
|
2118
|
+
});
|
|
2119
|
+
var convos = im.command("conversations").description("Conversation management");
|
|
2120
|
+
convos.command("list").description("List conversations").option("--unread", "Show unread only").option("--json", "JSON output").action(async (opts) => {
|
|
2121
|
+
const client = getIMClient();
|
|
2122
|
+
const listOpts = {};
|
|
2123
|
+
if (opts.unread) {
|
|
2124
|
+
listOpts.withUnread = true;
|
|
2125
|
+
listOpts.unreadOnly = true;
|
|
2126
|
+
}
|
|
2127
|
+
const res = await client.im.conversations.list(listOpts);
|
|
2128
|
+
if (!res.ok) {
|
|
2129
|
+
console.error("Error:", res.error);
|
|
2130
|
+
process.exit(1);
|
|
2131
|
+
}
|
|
2132
|
+
const list = res.data || [];
|
|
2133
|
+
if (opts.json) {
|
|
2134
|
+
console.log(JSON.stringify(list, null, 2));
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
if (list.length === 0) {
|
|
2138
|
+
console.log("No conversations.");
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
for (const c of list) {
|
|
2142
|
+
const unread = c.unreadCount ? ` (${c.unreadCount} unread)` : "";
|
|
2143
|
+
console.log(`${c.id || ""} ${c.type || ""} ${c.title || ""}${unread}`);
|
|
2144
|
+
}
|
|
2145
|
+
});
|
|
2146
|
+
convos.command("read").description("Mark conversation as read").argument("<conversation-id>", "Conversation ID").action(async (convId) => {
|
|
2147
|
+
const client = getIMClient();
|
|
2148
|
+
const res = await client.im.conversations.markAsRead(convId);
|
|
2149
|
+
if (!res.ok) {
|
|
2150
|
+
console.error("Error:", res.error);
|
|
2151
|
+
process.exit(1);
|
|
2152
|
+
}
|
|
2153
|
+
console.log("Marked as read.");
|
|
2154
|
+
});
|
|
2155
|
+
var files = im.command("files").description("File upload management");
|
|
2156
|
+
files.command("upload").description("Upload a file").argument("<path>", "File path to upload").option("--mime <type>", "Override MIME type").option("--json", "JSON output").action(async (filePath, opts) => {
|
|
2157
|
+
const client = getIMClient();
|
|
2158
|
+
try {
|
|
2159
|
+
const result = await client.im.files.upload(filePath, { mimeType: opts.mime });
|
|
2160
|
+
if (opts.json) {
|
|
2161
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
console.log(`Upload ID: ${result.uploadId}`);
|
|
2165
|
+
console.log(`CDN URL: ${result.cdnUrl}`);
|
|
2166
|
+
console.log(`File: ${result.fileName} (${result.fileSize} bytes)`);
|
|
2167
|
+
console.log(`MIME: ${result.mimeType}`);
|
|
2168
|
+
} catch (err) {
|
|
2169
|
+
console.error("Upload failed:", err instanceof Error ? err.message : err);
|
|
2170
|
+
process.exit(1);
|
|
2171
|
+
}
|
|
2172
|
+
});
|
|
2173
|
+
files.command("send").description("Upload file and send as message").argument("<conversation-id>", "Conversation ID").argument("<path>", "File path to upload").option("--content <text>", "Message text").option("--mime <type>", "Override MIME type").option("--json", "JSON output").action(async (conversationId, filePath, opts) => {
|
|
2174
|
+
const client = getIMClient();
|
|
2175
|
+
try {
|
|
2176
|
+
const result = await client.im.files.sendFile(conversationId, filePath, { content: opts.content, mimeType: opts.mime });
|
|
2177
|
+
if (opts.json) {
|
|
2178
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
console.log(`Upload ID: ${result.upload.uploadId}`);
|
|
2182
|
+
console.log(`CDN URL: ${result.upload.cdnUrl}`);
|
|
2183
|
+
console.log(`File: ${result.upload.fileName}`);
|
|
2184
|
+
console.log(`Message: sent`);
|
|
2185
|
+
} catch (err) {
|
|
2186
|
+
console.error("Send file failed:", err instanceof Error ? err.message : err);
|
|
2187
|
+
process.exit(1);
|
|
2188
|
+
}
|
|
2189
|
+
});
|
|
2190
|
+
files.command("quota").description("Show storage quota").option("--json", "JSON output").action(async (opts) => {
|
|
2191
|
+
const client = getIMClient();
|
|
2192
|
+
const res = await client.im.files.quota();
|
|
2193
|
+
if (!res.ok) {
|
|
2194
|
+
console.error("Error:", res.error);
|
|
2195
|
+
process.exit(1);
|
|
2196
|
+
}
|
|
2197
|
+
if (opts.json) {
|
|
2198
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
const q = res.data;
|
|
2202
|
+
console.log(`Used: ${q?.used ?? "-"} bytes`);
|
|
2203
|
+
console.log(`Limit: ${q?.limit ?? "-"} bytes`);
|
|
2204
|
+
console.log(`File Count: ${q?.fileCount ?? "-"}`);
|
|
2205
|
+
console.log(`Tier: ${q?.tier ?? "-"}`);
|
|
2206
|
+
});
|
|
2207
|
+
files.command("delete").description("Delete an uploaded file").argument("<upload-id>", "Upload ID").action(async (uploadId) => {
|
|
2208
|
+
const client = getIMClient();
|
|
2209
|
+
const res = await client.im.files.delete(uploadId);
|
|
2210
|
+
if (!res.ok) {
|
|
2211
|
+
console.error("Error:", res.error);
|
|
2212
|
+
process.exit(1);
|
|
2213
|
+
}
|
|
2214
|
+
console.log(`Deleted upload ${uploadId}.`);
|
|
2215
|
+
});
|
|
2216
|
+
files.command("types").description("List allowed MIME types").option("--json", "JSON output").action(async (opts) => {
|
|
2217
|
+
const client = getIMClient();
|
|
2218
|
+
const res = await client.im.files.types();
|
|
2219
|
+
if (!res.ok) {
|
|
2220
|
+
console.error("Error:", res.error);
|
|
2221
|
+
process.exit(1);
|
|
2222
|
+
}
|
|
2223
|
+
if (opts.json) {
|
|
2224
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
const types = res.data?.allowedMimeTypes || [];
|
|
2228
|
+
console.log(`Allowed MIME types (${types.length}):`);
|
|
2229
|
+
for (const t of types) {
|
|
2230
|
+
console.log(` ${t}`);
|
|
2231
|
+
}
|
|
2232
|
+
});
|
|
2233
|
+
im.command("credits").description("Show credits balance").option("--json", "JSON output").action(async (opts) => {
|
|
2234
|
+
const client = getIMClient();
|
|
2235
|
+
const res = await client.im.credits.get();
|
|
2236
|
+
if (!res.ok) {
|
|
2237
|
+
console.error("Error:", res.error);
|
|
2238
|
+
process.exit(1);
|
|
2239
|
+
}
|
|
2240
|
+
if (opts.json) {
|
|
2241
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
console.log(`Balance: ${res.data?.balance ?? "-"}`);
|
|
2245
|
+
});
|
|
2246
|
+
im.command("transactions").description("Transaction history").option("-n, --limit <n>", "Max transactions", "20").option("--json", "JSON output").action(async (opts) => {
|
|
2247
|
+
const client = getIMClient();
|
|
2248
|
+
const res = await client.im.credits.transactions({ limit: parseInt(opts.limit) });
|
|
2249
|
+
if (!res.ok) {
|
|
2250
|
+
console.error("Error:", res.error);
|
|
2251
|
+
process.exit(1);
|
|
2252
|
+
}
|
|
2253
|
+
const txns = res.data || [];
|
|
2254
|
+
if (opts.json) {
|
|
2255
|
+
console.log(JSON.stringify(txns, null, 2));
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
if (txns.length === 0) {
|
|
2259
|
+
console.log("No transactions.");
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
for (const t of txns) {
|
|
2263
|
+
console.log(`${t.createdAt || ""} ${t.type || ""} ${t.amount ?? ""} ${t.description || ""}`);
|
|
2264
|
+
}
|
|
2265
|
+
});
|
|
2266
|
+
var ctx = program.command("context").description("Context API commands");
|
|
2267
|
+
ctx.command("load").description("Load URL content").argument("<url>", "URL to load").option("-f, --format <fmt>", "Return format: hqcc, raw, both", "hqcc").option("--json", "JSON output").action(async (url, opts) => {
|
|
2268
|
+
const client = getAPIClient();
|
|
2269
|
+
const loadOpts = {};
|
|
2270
|
+
if (opts.format) loadOpts.return = { format: opts.format };
|
|
2271
|
+
const res = await client.load(url, loadOpts);
|
|
2272
|
+
if (opts.json) {
|
|
2273
|
+
console.log(JSON.stringify(res, null, 2));
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
if (!res.success) {
|
|
2277
|
+
console.error("Error:", res.error?.message || "Load failed");
|
|
2278
|
+
process.exit(1);
|
|
2279
|
+
}
|
|
2280
|
+
const r = res.result;
|
|
2281
|
+
console.log(`URL: ${r?.url || url}`);
|
|
2282
|
+
console.log(`Status: ${r?.cached ? "cached" : "loaded"}`);
|
|
2283
|
+
if (r?.hqcc) {
|
|
2284
|
+
console.log(`
|
|
2285
|
+
--- HQCC ---
|
|
2286
|
+
${r.hqcc.substring(0, 2e3)}`);
|
|
2287
|
+
}
|
|
2288
|
+
if (r?.raw) {
|
|
2289
|
+
console.log(`
|
|
2290
|
+
--- Raw ---
|
|
2291
|
+
${r.raw.substring(0, 2e3)}`);
|
|
2292
|
+
}
|
|
2293
|
+
});
|
|
2294
|
+
ctx.command("search").description("Search cached content").argument("<query>", "Search query").option("-k, --top-k <n>", "Number of results", "5").option("--json", "JSON output").action(async (query, opts) => {
|
|
2295
|
+
const client = getAPIClient();
|
|
2296
|
+
const res = await client.search(query, { topK: parseInt(opts.topK) });
|
|
2297
|
+
if (opts.json) {
|
|
2298
|
+
console.log(JSON.stringify(res, null, 2));
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
if (!res.success) {
|
|
2302
|
+
console.error("Error:", res.error?.message || "Search failed");
|
|
2303
|
+
process.exit(1);
|
|
2304
|
+
}
|
|
2305
|
+
const results = res.results || [];
|
|
2306
|
+
if (results.length === 0) {
|
|
2307
|
+
console.log("No results.");
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
for (let i = 0; i < results.length; i++) {
|
|
2311
|
+
const r = results[i];
|
|
2312
|
+
console.log(`${i + 1}. ${r.url || "(no url)"} score: ${r.ranking?.score ?? "-"}`);
|
|
2313
|
+
if (r.hqcc) console.log(` ${r.hqcc.substring(0, 200)}`);
|
|
2314
|
+
}
|
|
2315
|
+
});
|
|
2316
|
+
ctx.command("save").description("Save content to cache").argument("<url>", "URL key").argument("<hqcc>", "HQCC content").option("--json", "JSON output").action(async (url, hqcc, opts) => {
|
|
2317
|
+
const client = getAPIClient();
|
|
2318
|
+
const res = await client.save({ url, hqcc });
|
|
2319
|
+
if (opts.json) {
|
|
2320
|
+
console.log(JSON.stringify(res, null, 2));
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
if (!res.success) {
|
|
2324
|
+
console.error("Error:", res.error?.message || "Save failed");
|
|
2325
|
+
process.exit(1);
|
|
2326
|
+
}
|
|
2327
|
+
console.log("Content saved.");
|
|
2328
|
+
});
|
|
2329
|
+
var parse2 = program.command("parse").description("Document parsing commands");
|
|
2330
|
+
parse2.command("run").description("Parse a document").argument("<url>", "Document URL").option("-m, --mode <mode>", "Parse mode: fast, hires, auto", "fast").option("--json", "JSON output").action(async (url, opts) => {
|
|
2331
|
+
const client = getAPIClient();
|
|
2332
|
+
const res = await client.parsePdf(url, opts.mode);
|
|
2333
|
+
if (opts.json) {
|
|
2334
|
+
console.log(JSON.stringify(res, null, 2));
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
if (!res.success) {
|
|
2338
|
+
console.error("Error:", res.error?.message || "Parse failed");
|
|
2339
|
+
process.exit(1);
|
|
2340
|
+
}
|
|
2341
|
+
if (res.taskId) {
|
|
2342
|
+
console.log(`Task ID: ${res.taskId}`);
|
|
2343
|
+
console.log(`Status: ${res.status || "processing"}`);
|
|
2344
|
+
console.log(`
|
|
2345
|
+
Check progress: prismer parse status ${res.taskId}`);
|
|
2346
|
+
} else if (res.document) {
|
|
2347
|
+
console.log(`Status: complete`);
|
|
2348
|
+
const content = res.document.markdown || res.document.text || JSON.stringify(res.document, null, 2);
|
|
2349
|
+
console.log(content.substring(0, 5e3));
|
|
2350
|
+
}
|
|
2351
|
+
});
|
|
2352
|
+
parse2.command("status").description("Check parse task status").argument("<task-id>", "Task ID").option("--json", "JSON output").action(async (taskId, opts) => {
|
|
2353
|
+
const client = getAPIClient();
|
|
2354
|
+
const res = await client.parseStatus(taskId);
|
|
2355
|
+
if (opts.json) {
|
|
2356
|
+
console.log(JSON.stringify(res, null, 2));
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
console.log(`Task: ${taskId}`);
|
|
2360
|
+
console.log(`Status: ${res.status || (res.success ? "complete" : "unknown")}`);
|
|
2361
|
+
});
|
|
2362
|
+
parse2.command("result").description("Get parse result").argument("<task-id>", "Task ID").option("--json", "JSON output").action(async (taskId, opts) => {
|
|
2363
|
+
const client = getAPIClient();
|
|
2364
|
+
const res = await client.parseResult(taskId);
|
|
2365
|
+
if (opts.json) {
|
|
2366
|
+
console.log(JSON.stringify(res, null, 2));
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
if (!res.success) {
|
|
2370
|
+
console.error("Error:", res.error?.message || "Not ready");
|
|
2371
|
+
process.exit(1);
|
|
2372
|
+
}
|
|
2373
|
+
const content = res.document?.markdown || res.document?.text || JSON.stringify(res.document, null, 2);
|
|
2374
|
+
console.log(content);
|
|
2375
|
+
});
|
|
1023
2376
|
program.parse(process.argv);
|