@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +17 -8
  3. package/dist/apple-photos-client.js +1 -1
  4. package/dist/beeper-client.js +42 -6
  5. package/dist/{imessage-direct-install-2gm3kge7.js → imessage-direct-install-5nftwc6p.js} +1 -1
  6. package/dist/{index-9ayntqrw.js → index-g6na3p6k.js} +1 -1
  7. package/dist/{index-en5hycxp.js → index-kaav16fd.js} +4 -2
  8. package/dist/{index-2ymnp8xv.js → index-qcf2f6wm.js} +5 -1
  9. package/dist/{messaging-automation-j4274hvc.js → messaging-automation-58s1366z.js} +1 -1
  10. package/dist/messaging-automation-api.js +2 -2
  11. package/dist/{messaging-native-install-8w1j36ah.js → messaging-native-install-c90nv501.js} +1 -1
  12. package/dist/{whatsapp-automation-runtime-hgh1e9rm.js → whatsapp-automation-runtime-ke1dx91p.js} +1 -1
  13. package/docs/messaging-automation.md +14 -7
  14. package/package.json +12 -7
  15. package/skills/ghostget/SKILL.md +1 -1
  16. package/skills/ghostget/references/install.md +5 -5
  17. package/src/assets/adapters/beeper/wrench-web-adapter.json +37 -1
  18. package/src/assets/adapters/beeper/wrench-web-adapter.v2.4.0.json +1500 -0
  19. package/src/beeper-client-types.ts +1 -1
  20. package/src/control/menubar-cli.ts +570 -0
  21. package/src/control/menubar-icon.ts +4 -0
  22. package/src/messaging-automation-descriptors.ts +9 -1
  23. package/src/messaging-automation-factory.ts +7 -5
  24. package/src/messaging-automation-server.ts +2 -2
  25. package/src/messaging-automation-types.ts +3 -2
  26. package/src/messaging-automation-validation.ts +5 -1
  27. package/src/plugins/beeper-linked-device/plugin.ts +3 -1
  28. package/src/provider-plugin-contract-identity.ts +11 -2
  29. package/src/providers/beeper-automation.ts +447 -0
  30. package/src/providers/beeper-local-runtime.ts +61 -21
  31. package/src/providers/beeper-local.ts +4 -4
  32. package/src/providers/beeper-omni.ts +9 -2
  33. package/src/providers/imessage-direct-runtime.ts +28 -2
  34. package/src/providers/messaging-native-install.ts +2 -1
  35. package/src/support-profile.ts +7 -0
  36. package/src/support.ts +1 -7
  37. package/src/version.ts +1 -1
@@ -2544,11 +2544,11 @@ async function directMessagesV3(
2544
2544
  cursor: string;
2545
2545
  }> | null;
2546
2546
  }>> {
2547
- const selfUserId = requireBoundSelfAccount(
2547
+ const selfUserIds = boundSelfUserIds(
2548
+ requireBoundSelfAccount(realm.accounts, input.accountId, "messaging.read"),
2548
2549
  realm.accounts,
2549
- input.accountId,
2550
2550
  "messaging.read",
2551
- ).user.id;
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 !== selfUserId
2586
- || message.isSender === false && message.senderId === selfUserId)) {
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
- reaction.participantId === requireBoundAccount(accounts, accountId, action).user.id
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 conversations = strictArray(raw, "Beeper conversations", MAX_CHATS)
4036
- .map((item, index) => parseConversation(
4037
- item,
4038
- `Beeper conversations[${index}]`,
4039
- accountIds,
4040
- accountId,
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 = conversations.length >= input.limit;
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 selfUserId = requireBoundSelfAccount(
4306
- accounts,
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 === selfUserId))) {
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
- Object.keys(adapterOperations).sort().map((operation) => [
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.4.0",
2582
+ providerPluginVersion: "2.5.0",
2583
2583
  adapterId: "beeper-local",
2584
- adapterVersion: "2.4.0",
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
- "72201ac5eb3532f7c159583f19009f547d7d313e86388466b57c135bd2dc4944" as const;
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
- // child.exited remains the reaping proof.
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.12" as const;
2
+ export const GHOSTGET_VERSION = "0.18.14" as const;