@hraness/ghostget 0.18.13 → 0.18.15
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/CHANGELOG.md +26 -0
- package/README.md +12 -7
- package/dist/apple-photos-client.js +1 -1
- package/dist/beeper-client.js +42 -6
- package/dist/{imessage-direct-install-2gm3kge7.js → imessage-direct-install-5nftwc6p.js} +1 -1
- package/dist/{index-maa9hb5z.js → index-4tsyncva.js} +1 -1
- package/dist/{index-en5hycxp.js → index-kaav16fd.js} +4 -2
- package/dist/{index-2ymnp8xv.js → index-qcf2f6wm.js} +5 -1
- package/dist/{messaging-automation-j4274hvc.js → messaging-automation-58s1366z.js} +1 -1
- package/dist/messaging-automation-api.js +2 -2
- package/dist/{messaging-native-install-8w1j36ah.js → messaging-native-install-c90nv501.js} +1 -1
- package/dist/{whatsapp-automation-runtime-hgh1e9rm.js → whatsapp-automation-runtime-ke1dx91p.js} +1 -1
- package/docs/messaging-automation.md +14 -7
- package/package.json +8 -4
- package/skills/ghostget/references/install.md +5 -5
- package/src/assets/adapters/beeper/wrench-web-adapter.json +37 -1
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.4.0.json +1500 -0
- package/src/beeper-client-types.ts +1 -1
- package/src/control/menubar-cli.ts +3 -1
- package/src/control/menubar-icon.ts +4 -0
- package/src/messaging-automation-descriptors.ts +9 -1
- package/src/messaging-automation-factory.ts +7 -5
- package/src/messaging-automation-server.ts +2 -2
- package/src/messaging-automation-types.ts +3 -2
- package/src/messaging-automation-validation.ts +5 -1
- package/src/plugins/beeper-linked-device/plugin.ts +3 -1
- package/src/provider-plugin-contract-identity.ts +11 -2
- package/src/providers/beeper-automation.ts +447 -0
- package/src/providers/beeper-local-runtime.ts +61 -21
- package/src/providers/beeper-local.ts +4 -4
- package/src/providers/beeper-omni.ts +9 -2
- package/src/providers/messaging-native-install.ts +2 -1
- package/src/providers/x-web-runtime.ts +11 -11
- package/src/providers/x-web.ts +36 -35
- package/src/version.ts +1 -1
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { GhostgetAuth } from "../auth";
|
|
3
|
+
import { canonicalJson } from "../canonical-json";
|
|
4
|
+
import type { LocalCliExecutionOptions } from "../local-cli-execution";
|
|
5
|
+
import type { LocalCliRecipe, OperationInput } from "../model";
|
|
6
|
+
import type { AutomationAction, AutomationConversation, AutomationIdentity, AutomationMessage, AutomationProviderPage, AutomationProviderStatus, AutomationProviderSendResult, MessagingAutomationProvider } from "../messaging-automation-types";
|
|
7
|
+
import { AUTOMATION_ACTION_KINDS, automationArray, automationInteger, automationRecord, automationText, parseAutomationAction, parseAutomationIdentity } from "../messaging-automation-validation";
|
|
8
|
+
import { OperationDeadline } from "../operation-deadline";
|
|
9
|
+
import { executeBeeperDirectMessagingPart, executeBeeperLocalOperation, type BeeperDirectDependencies, type BeeperDirectMessagingDependencies, type BeeperLocalRuntimeDependencies } from "./beeper-local-runtime";
|
|
10
|
+
import { materializeBeeperExactConversation, materializeBeeperMessagingList, materializeBeeperMessagingRead, rawBeeperConversationId, rawBeeperMessageId } from "./beeper-omni";
|
|
11
|
+
|
|
12
|
+
export type BeeperAutomationOperation = "inspect" | "conversations" | "resolve" | "history" | "events" | AutomationAction["kind"];
|
|
13
|
+
export type BeeperAutomationAdmission = Readonly<{ auth: GhostgetAuth; accountIdentity: string; implementationIdentity: string }>;
|
|
14
|
+
export type BeeperAutomationOptions = Readonly<{
|
|
15
|
+
authorize(operation: BeeperAutomationOperation, signal?: AbortSignal): Promise<BeeperAutomationAdmission>;
|
|
16
|
+
execution: Pick<LocalCliExecutionOptions, "registerCleanupBarrier" | "environment">;
|
|
17
|
+
/** Internal deterministic test seams, never populated from public input. */
|
|
18
|
+
dependencies?: BeeperLocalRuntimeDependencies;
|
|
19
|
+
directDependencies?: BeeperDirectDependencies;
|
|
20
|
+
messagingDependencies?: BeeperDirectMessagingDependencies;
|
|
21
|
+
}>;
|
|
22
|
+
|
|
23
|
+
const sha = (value: unknown): string => createHash("sha256").update(canonicalJson(value)).digest("hex");
|
|
24
|
+
|
|
25
|
+
/** Recipes mirror the pinned beeper adapter manifest contracts exactly. */
|
|
26
|
+
const RECIPES = Object.freeze({
|
|
27
|
+
accounts: Object.freeze({ surface: "beeper", action: "accounts.list", contractVersion: 2, timeoutMs: 120_000, maxOutputBytes: 10_485_760 }),
|
|
28
|
+
conversations: Object.freeze({ surface: "beeper", action: "messaging.list", contractVersion: 1, timeoutMs: 120_000, maxOutputBytes: 10_485_760 }),
|
|
29
|
+
conversation: Object.freeze({ surface: "beeper", action: "conversations.read", contractVersion: 2, timeoutMs: 120_000, maxOutputBytes: 10_485_760 }),
|
|
30
|
+
messages: Object.freeze({ surface: "beeper", action: "messaging.read", contractVersion: 3, timeoutMs: 120_000, maxOutputBytes: 10_485_760 }),
|
|
31
|
+
} as const satisfies Record<string, LocalCliRecipe>);
|
|
32
|
+
|
|
33
|
+
type Coordinate = Readonly<{ provider: "beeper"; accountId: string; conversationId: string }>;
|
|
34
|
+
function coordinate(value: unknown): Coordinate {
|
|
35
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("Beeper automation coordinate is invalid.");
|
|
36
|
+
const r = automationRecord(value, ["provider", "accountId", "conversationId"]);
|
|
37
|
+
if (r.provider !== "beeper") throw new Error("Beeper cannot operate another messaging network");
|
|
38
|
+
return Object.freeze({ provider: "beeper", accountId: automationText(r.accountId, 512), conversationId: automationText(r.conversationId, 2048) });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Automation message ids stay inside the strict identifier alphabet; raw
|
|
42
|
+
* Beeper ids can exceed it, so long or unusual ids are deterministically hashed. */
|
|
43
|
+
function automationMessageId(raw: string): string {
|
|
44
|
+
const id = automationText(raw, 2048);
|
|
45
|
+
if (/^[A-Za-z0-9._:-]{1,256}$/u.test(id)) return id;
|
|
46
|
+
return `beeper-${sha(id)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function rawAccountFromProviderId(providerId: string): string {
|
|
50
|
+
const match = /^beeper:([A-Za-z0-9_-]+):chat:[A-Za-z0-9_-]+$/u.exec(providerId);
|
|
51
|
+
if (match === null) throw new Error("Normalized Beeper conversation ID is malformed.");
|
|
52
|
+
const raw = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.from(match[1]!, "base64url"));
|
|
53
|
+
if (Buffer.from(raw, "utf8").toString("base64url") !== match[1]) throw new Error("Normalized Beeper conversation ID is not canonically encoded.");
|
|
54
|
+
return raw;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function rawUserFromProviderId(providerId: string): string | null {
|
|
58
|
+
const match = /^beeper:[A-Za-z0-9_-]+:user:([A-Za-z0-9_-]+)$/u.exec(providerId);
|
|
59
|
+
if (match === null) return null;
|
|
60
|
+
try { return new TextDecoder("utf-8", { fatal: true }).decode(Buffer.from(match[1]!, "base64url")); } catch { return null; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type AccountsSnapshot = Readonly<{ subject: string; accounts: readonly Readonly<{ accountId: string; userId: string | null; isSelf: boolean | null }>[] }>;
|
|
64
|
+
|
|
65
|
+
function snapshotIdentity(admission: BeeperAutomationAdmission, snapshot: AccountsSnapshot): AutomationIdentity {
|
|
66
|
+
return parseAutomationIdentity({
|
|
67
|
+
provider: "beeper", authId: admission.auth.id,
|
|
68
|
+
accountIdentity: admission.accountIdentity,
|
|
69
|
+
accountSubject: snapshot.subject,
|
|
70
|
+
implementationIdentity: admission.implementationIdentity,
|
|
71
|
+
// Stable realm membership only; live per-bridge statuses flap without
|
|
72
|
+
// changing the identity that enrollments and cursors must bind.
|
|
73
|
+
sourceGeneration: sha(snapshot.accounts.map(account => ({ accountId: account.accountId, userId: account.userId, isSelf: account.isSelf }))),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function conversation(entity: Readonly<Record<string, unknown>>): AutomationConversation {
|
|
78
|
+
const providerId = automationText(entity.providerId, 4096);
|
|
79
|
+
const accountId = rawAccountFromProviderId(providerId);
|
|
80
|
+
const rawConversationId = rawBeeperConversationId(accountId, providerId);
|
|
81
|
+
const kind = entity.conversationKind === "single" ? "single" : entity.conversationKind === "group" ? "group" : "unknown";
|
|
82
|
+
const participants = automationArray(entity.participants, 500)
|
|
83
|
+
.map(participant => rawUserFromProviderId(automationText((participant as Readonly<Record<string, unknown>>).providerId, 4096)))
|
|
84
|
+
.filter((participant): participant is string => participant !== null);
|
|
85
|
+
const unique = [...new Set(participants)].sort();
|
|
86
|
+
if (kind === "single" && unique.length > 2) return Object.freeze({
|
|
87
|
+
coordinate: Object.freeze({ provider: "beeper", accountId, conversationId: rawConversationId }),
|
|
88
|
+
title: null, kind: "unknown", participants: Object.freeze([]),
|
|
89
|
+
});
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
coordinate: Object.freeze({ provider: "beeper", accountId, conversationId: rawConversationId }),
|
|
92
|
+
title: entity.title === null || entity.title === undefined ? null : automationText(entity.title, 512),
|
|
93
|
+
kind, participants: Object.freeze(unique),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** One message observation. `sortKey` orders the feed; the materialized entity
|
|
98
|
+
* carries the reviewed projection fields. */
|
|
99
|
+
type ObservedMessage = Readonly<{ entity: Readonly<Record<string, unknown>>; id: string; sortKey: string }>;
|
|
100
|
+
|
|
101
|
+
function projectMessage(selected: Coordinate, observed: ObservedMessage): AutomationMessage {
|
|
102
|
+
const entity = observed.entity;
|
|
103
|
+
const body = entity.body === null || entity.body === undefined ? null : automationText(entity.body, 1_048_576, true);
|
|
104
|
+
let text = body;
|
|
105
|
+
if (text !== null && Buffer.byteLength(text) > 65_536) {
|
|
106
|
+
let cut = 65_500;
|
|
107
|
+
while (cut > 0 && (Buffer.from(text).subarray(cut, cut + 1)[0]! & 0xc0) === 0x80) cut -= 1;
|
|
108
|
+
text = `${Buffer.from(text).subarray(0, cut).toString("utf8")}…`;
|
|
109
|
+
}
|
|
110
|
+
const state = automationText(entity.state, 64);
|
|
111
|
+
const deleted = state !== "active";
|
|
112
|
+
const edited = entity.providerRevision !== null && entity.providerRevision !== undefined;
|
|
113
|
+
const attachments = deleted ? [] : automationArray(entity.attachments, 20).map(item => {
|
|
114
|
+
const a = item as Readonly<Record<string, unknown>>;
|
|
115
|
+
return Object.freeze({
|
|
116
|
+
name: a.name === null || a.name === undefined ? null : automationText(a.name, 512),
|
|
117
|
+
mimeType: a.mimeType === null || a.mimeType === undefined ? null : automationText(a.mimeType, 256),
|
|
118
|
+
sizeBytes: a.sizeBytes === null || a.sizeBytes === undefined ? null : automationInteger(a.sizeBytes, 0, 1024 * 1024 * 1024),
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
if (deleted) text = null;
|
|
122
|
+
const reply = entity.replyToProviderId === null || entity.replyToProviderId === undefined
|
|
123
|
+
? null
|
|
124
|
+
: automationMessageId(rawBeeperMessageId(selected.accountId, automationText(entity.replyToProviderId, 4096)));
|
|
125
|
+
return Object.freeze({
|
|
126
|
+
id: automationMessageId(observed.id),
|
|
127
|
+
coordinate: Object.freeze({ provider: "beeper", accountId: selected.accountId, conversationId: selected.conversationId }),
|
|
128
|
+
direction: entity.direction === "outgoing" || entity.direction === "incoming" ? entity.direction : "unknown",
|
|
129
|
+
occurredAt: automationText(entity.orderedAt, 32),
|
|
130
|
+
text, kind: state !== "active" ? "delete" : edited ? "edit" : "message",
|
|
131
|
+
relatedMessageId: reply, attachments: Object.freeze(attachments),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Feed cursor per coordinate. `mark` is the newest emitted sortKey and only
|
|
136
|
+
* advances to emitted positions. `floor` non-null means a drain is active: the
|
|
137
|
+
* interval (`floor`, `mark`] still has unemitted messages below the newest
|
|
138
|
+
* window. `window` is the before-cursor of the window being drained (`null` =
|
|
139
|
+
* the newest page) and `drained` is the last emitted sortKey inside it, so a
|
|
140
|
+
* partially emitted window resumes exactly. */
|
|
141
|
+
type CoordinateState = Readonly<{ mark: string | null; floor: string | null; window: string | null; drained: string | null }>;
|
|
142
|
+
type FeedCursor = Readonly<{ version: 1; identity: string; scope: string; states: Readonly<Record<string, CoordinateState>> }>;
|
|
143
|
+
const coordinateKey = (selected: Coordinate): string => sha([selected.accountId, selected.conversationId]);
|
|
144
|
+
|
|
145
|
+
function encodeCursor(current: AutomationIdentity, coordinates: readonly Coordinate[], states: Readonly<Record<string, CoordinateState>>): string {
|
|
146
|
+
return Buffer.from(canonicalJson({ version: 1, identity: sha(current), scope: sha(coordinates), states }), "utf8").toString("base64url");
|
|
147
|
+
}
|
|
148
|
+
function parseCursor(value: string | null, current: AutomationIdentity, coordinates: readonly Coordinate[]): FeedCursor {
|
|
149
|
+
const initial = { version: 1 as const, identity: sha(current), scope: sha(coordinates) };
|
|
150
|
+
if (value === null) return Object.freeze({ ...initial, states: Object.freeze({}) });
|
|
151
|
+
automationText(value, 16_384);
|
|
152
|
+
const decoded = Buffer.from(value, "base64url");
|
|
153
|
+
if (decoded.toString("base64url") !== value) throw new Error("Noncanonical Beeper cursor");
|
|
154
|
+
const row = automationRecord(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(decoded)), ["version", "identity", "scope", "states"]);
|
|
155
|
+
if (row.version !== 1 || row.identity !== initial.identity || row.scope !== initial.scope) throw new Error("Beeper cursor identity or scope changed");
|
|
156
|
+
const stateKeys = Object.keys(row.states as object);
|
|
157
|
+
if (stateKeys.length > 50) throw new Error("Beeper cursor carries too many scopes");
|
|
158
|
+
const rawStates = automationRecord(row.states, stateKeys);
|
|
159
|
+
const states: Record<string, CoordinateState> = {};
|
|
160
|
+
for (const [key, raw] of Object.entries(rawStates)) {
|
|
161
|
+
const state = automationRecord(raw, ["mark", "floor", "window", "drained"]);
|
|
162
|
+
states[key] = Object.freeze({
|
|
163
|
+
mark: state.mark === null ? null : automationText(state.mark, 2048),
|
|
164
|
+
floor: state.floor === null ? null : automationText(state.floor, 2048),
|
|
165
|
+
window: state.window === null ? null : automationText(state.window, 2048),
|
|
166
|
+
drained: state.drained === null ? null : automationText(state.drained, 2048),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return Object.freeze({ ...initial, states: Object.freeze(states) });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Concrete pinned Beeper linked-device adapter over the reviewed local CLI and
|
|
173
|
+
* Desktop loopback contracts. No push feed exists, so events are honest bounded
|
|
174
|
+
* polling: a newest-page watermark plus a replayable before-cursor drain for any
|
|
175
|
+
* window that outgrows one page. */
|
|
176
|
+
export function createBeeperAutomationProvider(options: BeeperAutomationOptions): MessagingAutomationProvider {
|
|
177
|
+
const lifetime = new AbortController();
|
|
178
|
+
let closed = false, inFlight: Promise<unknown> | undefined;
|
|
179
|
+
|
|
180
|
+
async function run<T>(operation: BeeperAutomationOperation, signal: AbortSignal | undefined, work: (admission: BeeperAutomationAdmission, signal: AbortSignal) => Promise<T>): Promise<T> {
|
|
181
|
+
if (closed || inFlight) throw new Error("Beeper automation provider is closed or busy");
|
|
182
|
+
const activeSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
|
|
183
|
+
const pending = (async () => { activeSignal.throwIfAborted(); const before = await options.authorize(operation, activeSignal); if (typeof before.auth.subject !== "string" || before.auth.subject.length === 0) throw new Error("Beeper automation requires a bound local account realm"); const result = await work(before, activeSignal); const after = await options.authorize(operation, activeSignal); if (sha(before) !== sha(after)) throw new Error("Beeper account or permission changed during the operation"); return result; })();
|
|
184
|
+
inFlight = pending; try { return await pending; } finally { if (inFlight === pending) inFlight = undefined; }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function execute(recipe: LocalCliRecipe, input: OperationInput, auth: GhostgetAuth, signal: AbortSignal): Promise<unknown> {
|
|
188
|
+
const execution = await executeBeeperLocalOperation(recipe, input, auth, {
|
|
189
|
+
signal,
|
|
190
|
+
...(options.execution.environment === undefined ? {} : { environment: options.execution.environment }),
|
|
191
|
+
...(options.execution.registerCleanupBarrier === undefined ? {} : { registerCleanupBarrier: options.execution.registerCleanupBarrier }),
|
|
192
|
+
...(options.dependencies === undefined ? {} : { dependencies: options.dependencies }),
|
|
193
|
+
...(options.directDependencies === undefined ? {} : { directDependencies: options.directDependencies }),
|
|
194
|
+
});
|
|
195
|
+
if (execution.status !== "succeeded") throw new Error(`Beeper ${recipe.action} did not succeed.`);
|
|
196
|
+
return execution.output;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function accountsSnapshot(admission: BeeperAutomationAdmission, signal: AbortSignal): Promise<AccountsSnapshot> {
|
|
200
|
+
const output = automationRecord(await execute(RECIPES.accounts, {}, admission.auth, signal), ["provider", "operation", "accountSubject", "accounts"]);
|
|
201
|
+
const subject = automationText(output.accountSubject, 512);
|
|
202
|
+
const accounts = automationArray(output.accounts, 128).map(value => {
|
|
203
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("Beeper account projection is invalid.");
|
|
204
|
+
const account = value as Record<string, unknown>;
|
|
205
|
+
const user = account.user;
|
|
206
|
+
if (typeof user !== "object" || user === null || Array.isArray(user)) throw new Error("Beeper account user projection is invalid.");
|
|
207
|
+
const record = user as Record<string, unknown>;
|
|
208
|
+
return Object.freeze({
|
|
209
|
+
accountId: automationText(account.accountId, 512),
|
|
210
|
+
userId: automationText(record.id, 512),
|
|
211
|
+
isSelf: record.isSelf === null || record.isSelf === undefined ? null : record.isSelf === true,
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
return Object.freeze({ subject, accounts: Object.freeze(accounts) });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function offlineIdentity(admission: BeeperAutomationAdmission): AutomationIdentity {
|
|
218
|
+
return parseAutomationIdentity({
|
|
219
|
+
provider: "beeper", authId: admission.auth.id, accountIdentity: admission.accountIdentity,
|
|
220
|
+
accountSubject: admission.auth.subject ?? "unreachable",
|
|
221
|
+
implementationIdentity: admission.implementationIdentity, sourceGeneration: "unreachable",
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function status(current: AutomationIdentity, connected: boolean): AutomationProviderStatus {
|
|
226
|
+
const actions = Object.fromEntries(AUTOMATION_ACTION_KINDS.map(kind => [kind, {
|
|
227
|
+
available: connected && kind === "text",
|
|
228
|
+
reason: kind !== "text"
|
|
229
|
+
? "The pinned Beeper local transport has no admitted operation for this experience."
|
|
230
|
+
: connected ? null : "Beeper Desktop is not reachable on the reviewed local endpoint.",
|
|
231
|
+
}])) as AutomationProviderStatus["actions"];
|
|
232
|
+
return Object.freeze({
|
|
233
|
+
identity: current, connected,
|
|
234
|
+
events: Object.freeze({ available: connected, reason: connected ? null : "Beeper Desktop is not reachable on the reviewed local endpoint." }),
|
|
235
|
+
actions,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
type ReadPage = Readonly<{ messages: readonly ObservedMessage[]; continuation: string | null }>;
|
|
240
|
+
|
|
241
|
+
/** Internal read bound. The runtime refuses a limit smaller than one provider
|
|
242
|
+
* page (it cannot prove a page-internal skip), so reads always collect a full
|
|
243
|
+
* contract-sized window and callers slice the visible share. */
|
|
244
|
+
const WINDOW_LIMIT = 200;
|
|
245
|
+
|
|
246
|
+
/** Newest-first window read; `beforeCursor === null` reads the newest page. */
|
|
247
|
+
async function readMessages(admission: BeeperAutomationAdmission, selected: Coordinate, beforeCursor: string | null, signal: AbortSignal): Promise<Readonly<{ page: ReadPage; output: Record<string, unknown> }>> {
|
|
248
|
+
const operationInput: OperationInput = {
|
|
249
|
+
account_id: selected.accountId, conversation_id: selected.conversationId,
|
|
250
|
+
...(beforeCursor === null ? {} : { before_cursor: beforeCursor }),
|
|
251
|
+
limit: WINDOW_LIMIT,
|
|
252
|
+
};
|
|
253
|
+
const output = automationRecord(await execute(RECIPES.messages, operationInput, admission.auth, signal), ["provider", "operation", "accountSubject", "projection", "accountId", "conversationId", "selfUserId", "canonicalSelfUserId", "requestCursor", "requestDirection", "requestedSender", "messages", "tombstones", "continuation", "completeness"]);
|
|
254
|
+
const materialized = materializeBeeperMessagingRead(operationInput, output);
|
|
255
|
+
const rawMessages = automationArray(output.messages, WINDOW_LIMIT);
|
|
256
|
+
if (rawMessages.length !== materialized.entities.length) throw new Error("Beeper message projection count drifted.");
|
|
257
|
+
const observed: ObservedMessage[] = materialized.entities.map((entity, index) => {
|
|
258
|
+
const raw = rawMessages[index];
|
|
259
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new Error("Beeper message projection is invalid.");
|
|
260
|
+
const record = raw as Record<string, unknown>;
|
|
261
|
+
const id = automationText(record.id, 2048);
|
|
262
|
+
if (rawBeeperMessageId(selected.accountId, (entity as Readonly<Record<string, unknown>>).providerId) !== id) throw new Error("Beeper message projection does not bind its raw record.");
|
|
263
|
+
return Object.freeze({ entity: entity as Readonly<Record<string, unknown>>, id, sortKey: automationText(record.sortKey, 1024) });
|
|
264
|
+
});
|
|
265
|
+
const continuation = output.continuation === null ? null : automationRecord(output.continuation, ["direction", "cursor"]);
|
|
266
|
+
return Object.freeze({
|
|
267
|
+
page: Object.freeze({
|
|
268
|
+
messages: Object.freeze(observed),
|
|
269
|
+
continuation: continuation === null ? null : automationText(continuation.cursor, 2048),
|
|
270
|
+
}),
|
|
271
|
+
output: output as Record<string, unknown>,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
provider: "beeper",
|
|
277
|
+
inspect(signal) {
|
|
278
|
+
return run("inspect", signal, async (admission, active) => {
|
|
279
|
+
try {
|
|
280
|
+
const snapshot = await accountsSnapshot(admission, active);
|
|
281
|
+
return status(snapshotIdentity(admission, snapshot), true);
|
|
282
|
+
} catch {
|
|
283
|
+
return status(offlineIdentity(admission), false);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
},
|
|
287
|
+
conversations(input, signal) {
|
|
288
|
+
const limit = automationInteger(input.limit, 1, 200);
|
|
289
|
+
return run("conversations", signal, async (admission, active) => {
|
|
290
|
+
const snapshot = await accountsSnapshot(admission, active);
|
|
291
|
+
const output = await execute(RECIPES.conversations, { limit }, admission.auth, active);
|
|
292
|
+
const materialized = materializeBeeperMessagingList({ limit }, output);
|
|
293
|
+
const completeness = typeof output === "object" && output !== null
|
|
294
|
+
? (output as Readonly<Record<string, unknown>>).completeness
|
|
295
|
+
: undefined;
|
|
296
|
+
if (typeof completeness !== "object" || completeness === null || typeof (completeness as Readonly<Record<string, unknown>>).requestedLimitReached !== "boolean") throw new Error("Beeper conversation list completeness is invalid.");
|
|
297
|
+
const requestedLimitReached = (completeness as Readonly<Record<string, unknown>>).requestedLimitReached === true;
|
|
298
|
+
return Object.freeze({
|
|
299
|
+
identity: snapshotIdentity(admission, snapshot),
|
|
300
|
+
conversations: Object.freeze(materialized.entities.map(entity => conversation(entity as Readonly<Record<string, unknown>>))),
|
|
301
|
+
// The pinned CLI list output has no continuation metadata; an exactly
|
|
302
|
+
// full remote window cannot prove completeness, even when out-of-realm
|
|
303
|
+
// rows were excluded from the projection.
|
|
304
|
+
complete: !requestedLimitReached,
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
},
|
|
308
|
+
resolve(value, signal) {
|
|
309
|
+
const selected = coordinate(value);
|
|
310
|
+
return run("resolve", signal, async (admission, active) => {
|
|
311
|
+
const snapshot = await accountsSnapshot(admission, active);
|
|
312
|
+
const operationInput = { account_id: selected.accountId, conversation_id: selected.conversationId };
|
|
313
|
+
const entity = materializeBeeperExactConversation(operationInput, await execute(RECIPES.conversation, operationInput, admission.auth, active));
|
|
314
|
+
const projected = conversation(entity as Readonly<Record<string, unknown>>);
|
|
315
|
+
if (canonicalJson(projected.coordinate) !== canonicalJson(selected)) throw new Error("Beeper route resolution changed target.");
|
|
316
|
+
return Object.freeze({ identity: snapshotIdentity(admission, snapshot), conversation: projected });
|
|
317
|
+
});
|
|
318
|
+
},
|
|
319
|
+
history(input, signal) {
|
|
320
|
+
const selected = coordinate(input.coordinate), limit = automationInteger(input.limit, 1, 200);
|
|
321
|
+
return run("history", signal, async (admission, active) => {
|
|
322
|
+
const snapshot = await accountsSnapshot(admission, active);
|
|
323
|
+
const current = snapshotIdentity(admission, snapshot);
|
|
324
|
+
const { page } = await readMessages(admission, selected, null, active);
|
|
325
|
+
// Descending window → take the newest `limit`, emit ascending; the
|
|
326
|
+
// newest observed sortKey seeds the enrollment feed watermark.
|
|
327
|
+
const ascending = page.messages.slice(0, limit).reverse();
|
|
328
|
+
const states: Record<string, CoordinateState> = { [coordinateKey(selected)]: Object.freeze({ mark: ascending.length ? ascending[ascending.length - 1]!.sortKey : null, floor: null, window: null, drained: null }) };
|
|
329
|
+
return Object.freeze({
|
|
330
|
+
identity: current,
|
|
331
|
+
messages: Object.freeze(ascending.map(observed => projectMessage(selected, observed))),
|
|
332
|
+
nextCursor: encodeCursor(current, [selected], states),
|
|
333
|
+
caughtUp: true, gap: false,
|
|
334
|
+
} satisfies AutomationProviderPage);
|
|
335
|
+
});
|
|
336
|
+
},
|
|
337
|
+
events(input, signal) {
|
|
338
|
+
const coordinates = automationArray(input.coordinates, 50).map(coordinate).sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b)));
|
|
339
|
+
const limit = automationInteger(input.limit, 1, 500);
|
|
340
|
+
if (coordinates.length === 0 || new Set(coordinates.map(coordinateKey)).size !== coordinates.length) throw new Error("Distinct Beeper event scopes are required");
|
|
341
|
+
return run("events", signal, async (admission, active) => {
|
|
342
|
+
const snapshot = await accountsSnapshot(admission, active);
|
|
343
|
+
const current = snapshotIdentity(admission, snapshot);
|
|
344
|
+
const prior = parseCursor(input.cursor, current, coordinates);
|
|
345
|
+
const emitted: AutomationMessage[] = [];
|
|
346
|
+
const states: Record<string, CoordinateState> = { ...prior.states };
|
|
347
|
+
let caughtUp = true;
|
|
348
|
+
for (const selected of coordinates) {
|
|
349
|
+
if (emitted.length >= limit) { caughtUp = false; break; }
|
|
350
|
+
const key = coordinateKey(selected);
|
|
351
|
+
const stored = states[key];
|
|
352
|
+
const state = { mark: stored?.mark ?? null, floor: stored?.floor ?? null, window: stored?.window ?? null, drained: stored?.drained ?? null };
|
|
353
|
+
// A bounded number of window reads per coordinate per poll; a drain
|
|
354
|
+
// that outlasts it keeps its cursor and resumes next poll.
|
|
355
|
+
for (let scans = 0; scans < 32 && emitted.length < limit; scans += 1) {
|
|
356
|
+
const draining = state.floor !== null;
|
|
357
|
+
const { page } = await readMessages(admission, selected, draining ? state.window : null, active);
|
|
358
|
+
const lowerBound = draining ? (state.drained ?? state.floor) : state.mark;
|
|
359
|
+
const ascending = page.messages.filter(observed => lowerBound === null || observed.sortKey > lowerBound).reverse();
|
|
360
|
+
const take = ascending.slice(0, limit - emitted.length);
|
|
361
|
+
for (const observed of take) emitted.push(projectMessage(selected, observed));
|
|
362
|
+
const lastEmitted = take[take.length - 1]?.sortKey;
|
|
363
|
+
if (lastEmitted !== undefined) {
|
|
364
|
+
if (draining) state.drained = lastEmitted;
|
|
365
|
+
if (state.mark === null || lastEmitted > state.mark) state.mark = lastEmitted;
|
|
366
|
+
}
|
|
367
|
+
const exhausted = ascending.length === take.length;
|
|
368
|
+
const oldest = page.messages[page.messages.length - 1];
|
|
369
|
+
if (!draining) {
|
|
370
|
+
const previous = lowerBound;
|
|
371
|
+
if (previous !== null && page.messages.length !== 0 && ascending.length === page.messages.length && page.continuation !== null) {
|
|
372
|
+
// The whole newest window postdates the watermark: arrivals may
|
|
373
|
+
// sit below it. Drain deeper windows until one reaches `floor`.
|
|
374
|
+
state.floor = previous;
|
|
375
|
+
state.window = exhausted ? page.continuation : null;
|
|
376
|
+
state.drained = exhausted ? null : (lastEmitted ?? null);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (!exhausted) caughtUp = false;
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
if (!exhausted) break;
|
|
383
|
+
const floor = state.floor;
|
|
384
|
+
if ((oldest !== undefined && floor !== null && oldest.sortKey <= floor) || page.continuation === null) {
|
|
385
|
+
// The walk covered every surviving message above the watermark.
|
|
386
|
+
// Provider pruning below it deletes unobserved history, which is
|
|
387
|
+
// not a coverage gap.
|
|
388
|
+
state.floor = null; state.window = null; state.drained = null;
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
state.window = page.continuation;
|
|
392
|
+
state.drained = null;
|
|
393
|
+
}
|
|
394
|
+
if (state.floor !== null) caughtUp = false;
|
|
395
|
+
states[key] = Object.freeze(state);
|
|
396
|
+
}
|
|
397
|
+
emitted.sort((a, b) => a.occurredAt === b.occurredAt ? a.id.localeCompare(b.id) : a.occurredAt.localeCompare(b.occurredAt));
|
|
398
|
+
return Object.freeze({
|
|
399
|
+
identity: current, messages: Object.freeze(emitted),
|
|
400
|
+
nextCursor: encodeCursor(current, coordinates, states), caughtUp, gap: false,
|
|
401
|
+
} satisfies AutomationProviderPage);
|
|
402
|
+
});
|
|
403
|
+
},
|
|
404
|
+
async send(input, signal): Promise<AutomationProviderSendResult> {
|
|
405
|
+
const selected = coordinate(input.coordinate), expected = parseAutomationIdentity(input.identity), action = parseAutomationAction(input.action);
|
|
406
|
+
automationText(input.intentId, 256);
|
|
407
|
+
let dispatched = false;
|
|
408
|
+
try {
|
|
409
|
+
return await run(action.kind, signal, async (admission, active) => {
|
|
410
|
+
const observe = async (holder: BeeperAutomationAdmission): Promise<AutomationIdentity> => snapshotIdentity(holder, await accountsSnapshot(holder, active));
|
|
411
|
+
try {
|
|
412
|
+
const current = await observe(admission);
|
|
413
|
+
if (sha(current) !== sha(expected) || action.kind !== "text" || !status(current, true).actions[action.kind].available) throw new Error("Beeper source identity or capability changed before dispatch");
|
|
414
|
+
const operationDeadline = new OperationDeadline(300_000, { signal: active });
|
|
415
|
+
try {
|
|
416
|
+
const acceptance = await executeBeeperDirectMessagingPart(
|
|
417
|
+
{ account_id: selected.accountId, conversation_id: selected.conversationId, kind: "text", text: action.text, mentions: [], no_preview: false },
|
|
418
|
+
admission.auth,
|
|
419
|
+
{
|
|
420
|
+
operationDeadline, signal: active,
|
|
421
|
+
...(options.execution.environment === undefined ? {} : { environment: options.execution.environment }),
|
|
422
|
+
...(options.messagingDependencies === undefined ? {} : { dependencies: options.messagingDependencies }),
|
|
423
|
+
beforeExternalBegin: async () => {
|
|
424
|
+
const again = await options.authorize("text", active);
|
|
425
|
+
if (sha(again) !== sha(admission)) throw new Error("Beeper permission changed before writing the action");
|
|
426
|
+
if (sha(await observe(again)) !== sha(expected)) throw new Error("Beeper source identity changed before writing the action");
|
|
427
|
+
dispatched = true;
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
);
|
|
431
|
+
return Object.freeze({ state: "accepted", messageId: automationMessageId(acceptance.pendingMessageId), providerReceiptId: null, delivery: "unknown" } satisfies AutomationProviderSendResult);
|
|
432
|
+
} finally { operationDeadline.dispose(); }
|
|
433
|
+
} catch {
|
|
434
|
+
return Object.freeze(dispatched
|
|
435
|
+
? { state: "indeterminate", reason: "The Beeper receipt or cleanup could not be verified; do not retry." }
|
|
436
|
+
: { state: "not-started", reason: "The selected Beeper account, permission, target, or action was unavailable." } satisfies AutomationProviderSendResult);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
} catch {
|
|
440
|
+
return Object.freeze(dispatched
|
|
441
|
+
? { state: "indeterminate", reason: "The Beeper outcome is uncertain and cannot be retried." }
|
|
442
|
+
: { state: "not-started", reason: "The selected Beeper account, permission, target, or action was unavailable." } satisfies AutomationProviderSendResult);
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
async close() { closed = true; lifetime.abort(); await inFlight?.catch(() => undefined); },
|
|
446
|
+
};
|
|
447
|
+
}
|
|
@@ -2544,11 +2544,11 @@ async function directMessagesV3(
|
|
|
2544
2544
|
cursor: string;
|
|
2545
2545
|
}> | null;
|
|
2546
2546
|
}>> {
|
|
2547
|
-
const
|
|
2547
|
+
const selfUserIds = boundSelfUserIds(
|
|
2548
|
+
requireBoundSelfAccount(realm.accounts, input.accountId, "messaging.read"),
|
|
2548
2549
|
realm.accounts,
|
|
2549
|
-
input.accountId,
|
|
2550
2550
|
"messaging.read",
|
|
2551
|
-
)
|
|
2551
|
+
);
|
|
2552
2552
|
const collected: unknown[] = [];
|
|
2553
2553
|
const seenIds = new Set<string>();
|
|
2554
2554
|
const seenCursors = new Set<string>();
|
|
@@ -2582,8 +2582,8 @@ async function directMessagesV3(
|
|
|
2582
2582
|
input,
|
|
2583
2583
|
));
|
|
2584
2584
|
if (parsed.some((message) =>
|
|
2585
|
-
message.isSender === true && message.senderId
|
|
2586
|
-
|| message.isSender === false && message.senderId
|
|
2585
|
+
message.isSender === true && !selfUserIds.has(message.senderId)
|
|
2586
|
+
|| message.isSender === false && selfUserIds.has(message.senderId))) {
|
|
2587
2587
|
throw new Error(
|
|
2588
2588
|
"Beeper Desktop direct message direction contradicted the bound account self identity",
|
|
2589
2589
|
);
|
|
@@ -3868,7 +3868,7 @@ export async function reconcileBeeperLocalOperation(
|
|
|
3868
3868
|
const actualState = action === "messaging.edit"
|
|
3869
3869
|
? message.isSender === true && message.text === value.text
|
|
3870
3870
|
: message.reactions.some((reaction) =>
|
|
3871
|
-
|
|
3871
|
+
boundSelfUserIds(requireBoundAccount(accounts, accountId, action), accounts, action).has(reaction.participantId)
|
|
3872
3872
|
&& reaction.reactionKey === value.reaction);
|
|
3873
3873
|
return Object.freeze({
|
|
3874
3874
|
actualState,
|
|
@@ -4032,13 +4032,21 @@ function conversationOutput(
|
|
|
4032
4032
|
if (accountId !== null && !accountIds.has(accountId)) {
|
|
4033
4033
|
throw new Error("messaging.list requested an account outside the bound Beeper realm");
|
|
4034
4034
|
}
|
|
4035
|
-
const
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4035
|
+
const rows = strictArray(raw, "Beeper conversations", MAX_CHATS);
|
|
4036
|
+
// Desktop keeps chats for stale bridge accounts that /v1/accounts no longer
|
|
4037
|
+
// enumerates. An unfiltered list excludes those rows instead of failing the
|
|
4038
|
+
// whole projection; every emitted row still binds to the realm and scoped
|
|
4039
|
+
// reads keep their exact-account check.
|
|
4040
|
+
let excludedOutOfRealm = 0;
|
|
4041
|
+
const conversations = rows.flatMap((item, index) => {
|
|
4042
|
+
if (
|
|
4043
|
+
accountId === null
|
|
4044
|
+
&& typeof item === "object" && item !== null
|
|
4045
|
+
&& typeof (item as Readonly<Record<string, unknown>>).accountID === "string"
|
|
4046
|
+
&& !accountIds.has((item as Readonly<Record<string, unknown>>).accountID as string)
|
|
4047
|
+
) { excludedOutOfRealm += 1; return []; }
|
|
4048
|
+
return [parseConversation(item, `Beeper conversations[${index}]`, accountIds, accountId)];
|
|
4049
|
+
});
|
|
4042
4050
|
for (const conversation of conversations) {
|
|
4043
4051
|
if (
|
|
4044
4052
|
(input.archived !== null && (conversation.isArchived === true) !== input.archived)
|
|
@@ -4055,7 +4063,7 @@ function conversationOutput(
|
|
|
4055
4063
|
}
|
|
4056
4064
|
const ids = conversations.map((conversation) => `${conversation.accountId}\0${conversation.id}`);
|
|
4057
4065
|
if (new Set(ids).size !== ids.length) throw new Error("Beeper conversations repeat an account-scoped ID");
|
|
4058
|
-
const requestedLimitReached =
|
|
4066
|
+
const requestedLimitReached = rows.length >= input.limit;
|
|
4059
4067
|
return Object.freeze({
|
|
4060
4068
|
provider: "beeper",
|
|
4061
4069
|
operation: "messaging.list",
|
|
@@ -4070,9 +4078,11 @@ function conversationOutput(
|
|
|
4070
4078
|
remoteConversationSetComplete: false,
|
|
4071
4079
|
continuationAvailable: false,
|
|
4072
4080
|
requestedLimitReached,
|
|
4081
|
+
excludedOutOfRealmConversationCount: excludedOutOfRealm,
|
|
4073
4082
|
warnings: Object.freeze([
|
|
4074
4083
|
"beeper-cli-v0.6.2-chat-result-window-has-no-continuation",
|
|
4075
4084
|
"newly-connected-accounts-may-have-incomplete-history",
|
|
4085
|
+
...(excludedOutOfRealm > 0 ? ["beeper-out-of-realm-conversations-excluded"] : []),
|
|
4076
4086
|
]),
|
|
4077
4087
|
}),
|
|
4078
4088
|
});
|
|
@@ -4302,11 +4312,8 @@ function directMessageOutputV3(
|
|
|
4302
4312
|
raw: unknown,
|
|
4303
4313
|
continuation: Readonly<{ direction: "before" | "after"; cursor: string }> | null,
|
|
4304
4314
|
) {
|
|
4305
|
-
const
|
|
4306
|
-
|
|
4307
|
-
input.accountId,
|
|
4308
|
-
"messaging.read",
|
|
4309
|
-
).user.id;
|
|
4315
|
+
const selfAccount = requireBoundSelfAccount(accounts, input.accountId, "messaging.read");
|
|
4316
|
+
const selfUserIds = boundSelfUserIds(selfAccount, accounts, "messaging.read");
|
|
4310
4317
|
const messages = strictArray(raw, "Beeper direct messages", input.limit)
|
|
4311
4318
|
.map((item, index) => parseMessage(
|
|
4312
4319
|
item,
|
|
@@ -4315,7 +4322,7 @@ function directMessageOutputV3(
|
|
|
4315
4322
|
));
|
|
4316
4323
|
if (messages.some((message) =>
|
|
4317
4324
|
message.isSender !== null
|
|
4318
|
-
&& message.isSender !== (message.senderId
|
|
4325
|
+
&& message.isSender !== selfUserIds.has(message.senderId))) {
|
|
4319
4326
|
throw new Error(
|
|
4320
4327
|
"Beeper direct messages contradicted the bound account self identity",
|
|
4321
4328
|
);
|
|
@@ -4367,7 +4374,8 @@ function directMessageOutputV3(
|
|
|
4367
4374
|
projection: "bounded-local-desktop-direct-iterator",
|
|
4368
4375
|
accountId: input.accountId,
|
|
4369
4376
|
conversationId: input.conversationId,
|
|
4370
|
-
selfUserId,
|
|
4377
|
+
selfUserId: selfAccount.user.id,
|
|
4378
|
+
canonicalSelfUserId: canonicalSelfUserId(accounts, "messaging.read"),
|
|
4371
4379
|
requestCursor,
|
|
4372
4380
|
requestDirection,
|
|
4373
4381
|
requestedSender: input.sender,
|
|
@@ -4415,6 +4423,38 @@ function requireBoundSelfAccount(
|
|
|
4415
4423
|
return account;
|
|
4416
4424
|
}
|
|
4417
4425
|
|
|
4426
|
+
/** The realm's canonical Matrix self identity when exactly one is present;
|
|
4427
|
+
* Desktop reports it as `senderID`/`participantId` for the owner on every
|
|
4428
|
+
* bridge rather than each account's bridge-scoped user ID. */
|
|
4429
|
+
function canonicalSelfUserId(
|
|
4430
|
+
accounts: readonly BeeperAccountProjection[],
|
|
4431
|
+
operation: string,
|
|
4432
|
+
): string | null {
|
|
4433
|
+
const canonical = accounts.filter((candidate) =>
|
|
4434
|
+
candidate.user.isSelf === true
|
|
4435
|
+
&& (
|
|
4436
|
+
candidate.bridge.type.toLowerCase() === "matrix"
|
|
4437
|
+
|| candidate.network?.toLowerCase() === "beeper"
|
|
4438
|
+
));
|
|
4439
|
+
if (canonical.length > 1) {
|
|
4440
|
+
throw new Error(`${operation} found ambiguous canonical self identities`);
|
|
4441
|
+
}
|
|
4442
|
+
return canonical.length === 1 ? canonical[0]!.user.id : null;
|
|
4443
|
+
}
|
|
4444
|
+
|
|
4445
|
+
/** Self sender identities proven by the bound realm: the given bound account's
|
|
4446
|
+
* bridge-scoped user ID plus the canonical Matrix self identity. */
|
|
4447
|
+
function boundSelfUserIds(
|
|
4448
|
+
account: BeeperAccountProjection,
|
|
4449
|
+
accounts: readonly BeeperAccountProjection[],
|
|
4450
|
+
operation: string,
|
|
4451
|
+
): ReadonlySet<string> {
|
|
4452
|
+
const self = new Set([account.user.id]);
|
|
4453
|
+
const canonical = canonicalSelfUserId(accounts, operation);
|
|
4454
|
+
if (canonical !== null) self.add(canonical);
|
|
4455
|
+
return self;
|
|
4456
|
+
}
|
|
4457
|
+
|
|
4418
4458
|
function exactConversation(
|
|
4419
4459
|
raw: unknown,
|
|
4420
4460
|
accounts: readonly BeeperAccountProjection[],
|
|
@@ -284,7 +284,7 @@ const adapterOperations = beeperAdapterManifest.operations as Readonly<Record<
|
|
|
284
284
|
>>;
|
|
285
285
|
|
|
286
286
|
export const BEEPER_LOCAL_OPERATION_INPUT_TYPES = Object.freeze(Object.fromEntries(
|
|
287
|
-
|
|
287
|
+
[...BEEPER_LOCAL_OPERATION_NAMES].sort().map((operation) => [
|
|
288
288
|
operation,
|
|
289
289
|
Object.freeze(Object.fromEntries(
|
|
290
290
|
Object.keys(adapterOperations[operation]!.input.properties).sort().map((field) => [
|
|
@@ -2579,9 +2579,9 @@ export const BEEPER_CLI_V062_SURFACE_CONTRACT = defineLocalCliSurfaceContractV1(
|
|
|
2579
2579
|
sdk: BEEPER_DESKTOP_API_PIN,
|
|
2580
2580
|
runtime: {
|
|
2581
2581
|
providerPluginId: "beeper-linked-device",
|
|
2582
|
-
providerPluginVersion: "2.
|
|
2582
|
+
providerPluginVersion: "2.5.0",
|
|
2583
2583
|
adapterId: "beeper-local",
|
|
2584
|
-
adapterVersion: "2.
|
|
2584
|
+
adapterVersion: "2.5.0",
|
|
2585
2585
|
operationContractVersions: BEEPER_LOCAL_OPERATION_CONTRACT_VERSIONS,
|
|
2586
2586
|
operationInputTypes: BEEPER_LOCAL_OPERATION_INPUT_TYPES,
|
|
2587
2587
|
target: BEEPER_DESKTOP_TARGET,
|
|
@@ -2600,7 +2600,7 @@ export const BEEPER_CLI_V062_CLASSIFICATION_SHA256 =
|
|
|
2600
2600
|
export const BEEPER_CLI_V062_SEMANTIC_PROFILES_SHA256 =
|
|
2601
2601
|
"fb7ea5f70f004dd8090c3e6e0996bfa00b0bab8ea5639203e2d1027602450ffe" as const;
|
|
2602
2602
|
export const BEEPER_CLI_V062_WHOLE_SURFACE_SHA256 =
|
|
2603
|
-
"
|
|
2603
|
+
"bedc3063a7a792686e351c78e7ba3ed2a5fc0cc6efd3d88a23d9b7ae777765e2" as const;
|
|
2604
2604
|
|
|
2605
2605
|
export const BEEPER_CLI_V062_PUBLIC_MANUAL_SEMANTIC_PROFILE_SHA256 = Object.freeze({
|
|
2606
2606
|
"setup": "cd432e2649e5724d70398e739a2d1c0c21557a23820aaa14562575a5fe689406",
|