@hraness/ghostget 0.18.12 → 0.18.14
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 +29 -0
- package/README.md +17 -8
- 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-9ayntqrw.js → index-g6na3p6k.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 +12 -7
- package/skills/ghostget/SKILL.md +1 -1
- 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 +570 -0
- 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/imessage-direct-runtime.ts +28 -2
- package/src/providers/messaging-native-install.ts +2 -1
- package/src/support-profile.ts +7 -0
- package/src/support.ts +1 -7
- package/src/version.ts +1 -1
|
@@ -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",
|
|
@@ -978,7 +978,7 @@ export function materializeBeeperMessagingRead(
|
|
|
978
978
|
"tombstones",
|
|
979
979
|
"continuation",
|
|
980
980
|
"completeness",
|
|
981
|
-
], [], "messaging.read output");
|
|
981
|
+
], ["canonicalSelfUserId"], "messaging.read output");
|
|
982
982
|
if (source.accountId !== parsed.accountId || source.conversationId !== parsed.conversationId) {
|
|
983
983
|
drift("messaging.read output", "must bind input account and conversation");
|
|
984
984
|
}
|
|
@@ -987,6 +987,13 @@ export function materializeBeeperMessagingRead(
|
|
|
987
987
|
"messaging.read output.selfUserId",
|
|
988
988
|
2_048,
|
|
989
989
|
);
|
|
990
|
+
const canonicalSelfUserId = source.canonicalSelfUserId === undefined || source.canonicalSelfUserId === null
|
|
991
|
+
? null
|
|
992
|
+
: beeperRawIdentifier(
|
|
993
|
+
source.canonicalSelfUserId,
|
|
994
|
+
"messaging.read output.canonicalSelfUserId",
|
|
995
|
+
2_048,
|
|
996
|
+
);
|
|
990
997
|
const requestCursor = parsed.beforeCursor ?? parsed.afterCursor;
|
|
991
998
|
if (source.requestCursor !== requestCursor) {
|
|
992
999
|
drift("messaging.read output.requestCursor", "must bind the requested cursor");
|
|
@@ -1025,7 +1032,7 @@ export function materializeBeeperMessagingRead(
|
|
|
1025
1032
|
);
|
|
1026
1033
|
if (
|
|
1027
1034
|
isSender !== null
|
|
1028
|
-
&& isSender !== (senderId === selfUserId)
|
|
1035
|
+
&& isSender !== (senderId === selfUserId || (canonicalSelfUserId !== null && senderId === canonicalSelfUserId))
|
|
1029
1036
|
) drift(
|
|
1030
1037
|
"messaging.read output.messages",
|
|
1031
1038
|
"must bind direction to the exact self user ID",
|
|
@@ -448,18 +448,38 @@ export async function runImsgRpc(
|
|
|
448
448
|
let timedOut = false;
|
|
449
449
|
let cancelled = false;
|
|
450
450
|
let forceKill: ReturnType<typeof setTimeout> | null = null;
|
|
451
|
+
let groupTerminated: Promise<boolean> | null = null;
|
|
451
452
|
let terminationStarted = false;
|
|
452
453
|
const signalGroup = (signal: "SIGTERM" | "SIGKILL"): void => {
|
|
453
454
|
try {
|
|
454
455
|
process.kill(-child.pid, signal);
|
|
455
456
|
} catch {
|
|
456
|
-
//
|
|
457
|
+
// The bounded group probe below establishes descendant cleanup.
|
|
457
458
|
}
|
|
458
459
|
};
|
|
459
460
|
const terminate = (): void => {
|
|
460
461
|
if (!terminationStarted) {
|
|
461
462
|
terminationStarted = true;
|
|
462
463
|
signalGroup("SIGTERM");
|
|
464
|
+
groupTerminated = (async (): Promise<boolean> => {
|
|
465
|
+
// A joined leader and closed pipes do not prove its descendants exited.
|
|
466
|
+
// Retain the one-second escalation, then bound final group reaping.
|
|
467
|
+
const deadline = performance.now() + 1_500;
|
|
468
|
+
for (;;) {
|
|
469
|
+
try {
|
|
470
|
+
process.kill(-child.pid, 0);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
const code = typeof error === "object" && error !== null
|
|
473
|
+
&& "code" in error ? error.code : undefined;
|
|
474
|
+
if (code === "ESRCH") return true;
|
|
475
|
+
// Allow a transient permission denial to settle within the same
|
|
476
|
+
// bound; it is never proof that the group exited.
|
|
477
|
+
if (code !== "EPERM") return false;
|
|
478
|
+
}
|
|
479
|
+
if (performance.now() >= deadline) return false;
|
|
480
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 20));
|
|
481
|
+
}
|
|
482
|
+
})();
|
|
463
483
|
}
|
|
464
484
|
forceKill ??= setTimeout(() => signalGroup("SIGKILL"), 1_000);
|
|
465
485
|
};
|
|
@@ -509,8 +529,14 @@ export async function runImsgRpc(
|
|
|
509
529
|
});
|
|
510
530
|
} finally {
|
|
511
531
|
clearTimeout(timeout);
|
|
512
|
-
if (forceKill !== null) clearTimeout(forceKill);
|
|
513
532
|
invocation.signal?.removeEventListener("abort", onAbort);
|
|
533
|
+
try {
|
|
534
|
+
if (groupTerminated !== null && !await groupTerminated) {
|
|
535
|
+
throw new ImsgCleanupUnverifiedError();
|
|
536
|
+
}
|
|
537
|
+
} finally {
|
|
538
|
+
if (forceKill !== null) clearTimeout(forceKill);
|
|
539
|
+
}
|
|
514
540
|
}
|
|
515
541
|
}
|
|
516
542
|
|
|
@@ -62,8 +62,9 @@ export async function ensureImsgNativeResources(installDirectory: string, enviro
|
|
|
62
62
|
|
|
63
63
|
/** Owner setup only. Installing resources never pairs, opens Messages, changes
|
|
64
64
|
* OS permissions, connects a provider, or grants messaging authority. */
|
|
65
|
-
export async function installBundledMessagingRuntime(provider: "imessage" | "whatsapp", environment: Environment = process.env): Promise<{ version: string; sha256: string; alreadyPresent?: boolean }> {
|
|
65
|
+
export async function installBundledMessagingRuntime(provider: "imessage" | "whatsapp" | "beeper", environment: Environment = process.env): Promise<{ version: string; sha256: string; alreadyPresent?: boolean }> {
|
|
66
66
|
if (process.platform !== "darwin" || process.arch !== "arm64") throw new Error("Bundled messaging runtimes require Apple silicon macOS");
|
|
67
|
+
if (provider === "beeper") throw new Error("Beeper has no bundled messaging runtime; the pinned local CLI and running Beeper Desktop are installed separately");
|
|
67
68
|
if (provider !== "imessage" && provider !== "whatsapp") throw new Error("Unknown messaging runtime");
|
|
68
69
|
const bytes = await readBundledMessagingAsset(provider), directory = await mkdtemp(join(await realpath(tmpdir()), "wrench-messaging-install-")); await chmod(directory, 0o700);
|
|
69
70
|
const path = join(directory, "runtime"), file = await open(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o500);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Public product identity shared by the CLI, menu and static website. */
|
|
2
|
+
export const ghostgetSupportProfile = Object.freeze({
|
|
3
|
+
id: "wrench",
|
|
4
|
+
name: "Ghostget",
|
|
5
|
+
valueProposition: "Support ongoing development of precise web tools for agents.",
|
|
6
|
+
updates: true,
|
|
7
|
+
} as const);
|
package/src/support.ts
CHANGED
|
@@ -2,13 +2,7 @@ import {
|
|
|
2
2
|
maybeShowSupportInvitation,
|
|
3
3
|
runSupportCommand,
|
|
4
4
|
} from "@hraness/support-foundation/node";
|
|
5
|
-
|
|
6
|
-
const profile = {
|
|
7
|
-
id: "wrench",
|
|
8
|
-
name: "Ghostget",
|
|
9
|
-
valueProposition: "Support ongoing development of precise web tools for agents.",
|
|
10
|
-
updates: true,
|
|
11
|
-
} as const;
|
|
5
|
+
import { ghostgetSupportProfile as profile } from "./support-profile";
|
|
12
6
|
|
|
13
7
|
type SupportOutput = {
|
|
14
8
|
readonly stdout: (text: string) => unknown;
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Canonical immutable Ghostget package release identity. */
|
|
2
|
-
export const GHOSTGET_VERSION = "0.18.
|
|
2
|
+
export const GHOSTGET_VERSION = "0.18.14" as const;
|