@agentcomms/whatsapp 0.15.2 → 0.16.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 CHANGED
@@ -11,6 +11,10 @@ npm install -g @agentcomms/whatsapp # or run it with npx -y @agentcomms/whats
11
11
  It needs **macOS with WhatsApp for Mac** installed and signed in, and **Node 22.16 or newer** (it reads with Node's
12
12
  own SQLite, which is complete from 22.16; an older Node is refused with what to install).
13
13
 
14
+ The local event daemon, [`@agentcomms/events-daemon`](../events-daemon/README.md), may call this package's internal
15
+ read-only event operation over a checked local copy.
16
+ It adds no WhatsApp command or MCP tool, and it cannot send.
17
+
14
18
  ## What it does, and what it never does, in plain words
15
19
 
16
20
  - **It reads only local files.** Nothing it does to read, search, draft or keep its lists connects to WhatsApp, or
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as paint, A as PATH_OPTIONS, B as colorEnabled, C as checkStorePath, D as CHANNEL_SERVERS, F as approveAndWaitSentence, G as handoffSentence, H as escapeForDisplay, I as approveChangeAtTerminal, J as isCommsError, K as handoffText, L as canPrompt, M as absoluteSearchPath, N as agentMarker, O as CommsError, P as approvalsOf, Q as openCore, R as childEnvironment, S as storeIdentity, T as expandSource, U as exemptFromUpdateGate, V as commandPathOf, W as gatedChangeAtTerminal, X as nameAvailable, Y as lookupName, Z as nameShapeProblem, _ as chatRefOf, a as listChats, at as serverInstallChange, b as requireAccount, c as Presenter, ct as truncateDisplay, d as draftMessage, dt as windowsSystemProgram, et as pathOverridesFromCliOptions, f as Visibility, ft as withFileLock, g as requireSupportedNode, gt as VERSION, h as WhatsAppIndex, i as CHAT_KINDS, it as runCommand, k as EXIT_CODES, l as innerText, lt as updateGateAtTerminal, m as WhatsAppContext, mt as CALLER, n as whatsappStatus, nt as renderPrune, o as readChat, ot as serverPruneChange, p as listKey, pt as writeResult, q as installExitStatus, r as probeStore, rt as resolvePaths, s as searchMessages, t as syncAccount, tt as renderInstall, v as PLATFORM, w as defaultStorePath, x as requireNamedConfig, y as newWhatsAppAccount, z as cliHandoffs } from "./sync-Ce-u-zuv.mjs";
2
+ import { $ as exemptFromUpdateGate, A as requireAccount, B as PATH_OPTIONS, Ct as writeResult, Et as VERSION, F as expandSource, G as approveAndWaitSentence, H as absoluteSearchPath, J as childEnvironment, K as approveChangeAtTerminal, L as CHANNEL_SERVERS, M as storeIdentity, N as checkStorePath, O as PLATFORM, P as defaultStorePath, Q as escapeForDisplay, R as CommsError, S as chatRefOf, St as withFileLock, U as agentMarker, W as approvalsOf, X as colorEnabled, Y as cliHandoffs, Z as commandPathOf, _ as WhatsAppContext, a as CHAT_KINDS, at as lookupName, b as requireSupportedNode, c as searchMessages, ct as openCore, dt as renderInstall, et as gatedChangeAtTerminal, f as draftMessage, ft as renderPrune, gt as serverPruneChange, ht as serverInstallChange, i as probeStore, it as isCommsError, j as requireNamedConfig, k as newWhatsAppAccount, l as Presenter, lt as paint, m as listKey, mt as runCommand, nt as handoffText, o as listChats, ot as nameAvailable, p as Visibility, pt as resolvePaths, q as canPrompt, r as whatsappStatus, rt as installExitStatus, s as readChat, st as nameShapeProblem, t as syncAccount, tt as handoffSentence, u as innerText, ut as pathOverridesFromCliOptions, v as WhatsAppIndex, vt as truncateDisplay, wt as CALLER, xt as windowsSystemProgram, yt as updateGateAtTerminal, z as EXIT_CODES } from "./sync-Dx4QM1gQ.mjs";
3
3
  import { rm } from "node:fs/promises";
4
4
  import path, { delimiter, join } from "node:path";
5
5
  import { stripVTControlCharacters } from "node:util";
@@ -3582,7 +3582,7 @@ a macOS dialog may be waiting) · 77 permission needed (macOS privacy) · 78 con
3582
3582
  ran = true;
3583
3583
  exitCode = await runCommand(output(), async () => {
3584
3584
  requireSupportedNode();
3585
- await (deps.startMcp ?? (await import("./stdio-entry-Bjqf4Tz2.mjs")).startWhatsAppStdioServer)({
3585
+ await (deps.startMcp ?? (await import("./stdio-entry-Be_eFAtZ.mjs")).startWhatsAppStdioServer)({
3586
3586
  env,
3587
3587
  platform,
3588
3588
  ...deps,
package/dist/index.d.mts CHANGED
@@ -4277,7 +4277,7 @@ declare namespace external_d_exports {
4277
4277
  export { $brand, $input, $output, BRAND, CompileOptions, GlobalMeta, INVALID, Infer$1 as Infer, IssueData, JSONType, NEVER, $RefinementCtx as RefinementCtx, SafeExtendShape, Schema, TimePrecision, TypeOf, ZodAny, ZodArray, ZodBase64, ZodBase64URL, ZodBigInt, ZodBigIntFormat, ZodBoolean, ZodCIDRv4, ZodCIDRv6, ZodCUID, ZodCUID2, ZodCatch, ZodCodec, ZodCoercedBigInt, ZodCoercedBoolean, ZodCoercedDate, ZodCoercedNumber, ZodCoercedString, ZodCompileAsyncError, ZodCompileUnsupportedError, ZodCreditCard, ZodCustom, ZodCustomStringFormat, ZodDate, ZodDefault, ZodDiscriminatedUnion, ZodE164, ZodEmail, ZodEmoji, ZodEnum, ZodError, ZodErrorMap, ZodExactOptional, ZodFile, ZodFirstPartySchemaTypes, ZodFirstPartyTypeKind, ZodFlattenedError, ZodFloat32, ZodFloat64, ZodFormattedError, ZodFunction, ZodGUID, ZodIBAN, ZodIPv4, ZodIPv6, ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime, ZodInstanceOf, ZodInt, ZodInt32, ZodInt64, ZodIntersection, ZodIssue, ZodIssueCode, ZodJSONSchema, ZodJSONSchemaInternals, ZodJWT, ZodKSUID, ZodLazy, ZodLiteral, ZodMAC, ZodMap, ZodNaN, ZodNanoID, ZodNever, ZodNonOptional, ZodNull, ZodNullable, ZodNumber, ZodNumberFormat, ZodObject, ZodOptional, ZodPipe, ZodPrefault, ZodPreprocess, ZodPromise, ZodRawShape$1 as ZodRawShape, ZodReadonly, ZodRealError, ZodRecord, ZodSafeParseError, ZodSafeParseResult, ZodSafeParseSuccess, ZodSchema, ZodSet, ZodStandardSchemaWithJSON, ZodString, ZodStringFormat, ZodSuccess, ZodSymbol, ZodTemplateLiteral, ZodTransform, ZodTuple, ZodType, ZodTypeAny, ZodUInt32, ZodUInt64, ZodULID, ZodURL, ZodUUID, ZodUndefined, ZodUnion, ZodUnknown, ZodVoid, ZodXID, ZodXor, _ZodBigInt, _ZodBoolean, _ZodDate, _ZodNumber, _ZodString, _ZodType, _default, _function, any, array, base64, base64url, bigint$1 as bigint, boolean$1 as boolean, _catch as catch, check, cidrv4, cidrv6, clone, codec, coerce_d_exports as coerce, compile, config, index_d_exports$1 as core, creditCard, cuid, cuid2, currencyCode, custom, date$2 as date, decode, decodeAsync, deepPartial, describe, discriminatedUnion, e164, email, emoji, encode, encodeAsync, _endsWith as endsWith, _enum as enum, exactOptional, file, flattenError, float32, float64, formatError, fromJSONSchema, _function as function, getDiscriminatedOption, getErrorMap, globalRegistry, _gt as gt, _gte as gte, guid, hash, hex, hostname, httpUrl, iban, _includes as includes, output$1 as infer, inferFlattenedErrors, inferFormattedError, input, _instanceof as instanceof, int, int32, int64, intersection, invertCodec, ipv4, ipv6, iso_d_exports as iso, json, jwt, keyof, ksuid, lazy, _length as length, literal, index_d_exports$2 as locales, looseObject, looseRecord, _lowercase as lowercase, _lt as lt, _lte as lte, mac, map, _maxLength as maxLength, _maxSize as maxSize, memoizer, meta, _mime as mime, _minLength as minLength, _minSize as minSize, _multipleOf as multipleOf, nan, nanoid, nativeEnum, _negative as negative, never, _nonnegative as nonnegative, nonoptional, _nonpositive as nonpositive, _normalize as normalize, _null as null, nullable, nullish, number$1 as number, object, optional, output, _overwrite as overwrite, parse, parseAsync, partialRecord, pipe, _positive as positive, prefault, preprocess, prettifyError, promise, _properties as properties, _property as property, readonly, record, refine, _regex as regex, regexes_d_exports as regexes, registry, safeDecode, safeDecodeAsync, safeEncode, safeEncodeAsync, safeParse, safeParseAsync, set, setErrorMap, _size as size, _slugify as slugify, _startsWith as startsWith, strictObject, string$1 as string, stringFormat, stringbool, success, superRefine, symbol, templateLiteral, toJSONSchema, _toLowerCase as toLowerCase, _toUpperCase as toUpperCase, toZod, transform, treeifyError, _trim as trim, tuple, uint32, uint64, ulid, _undefined as undefined, union, unknown, _uppercase as uppercase, url, util_d_exports as util, uuid, uuidv4, uuidv6, uuidv7, validate, validateAsync, _void as void, withParser, xid, xor };
4278
4278
  }
4279
4279
  //#endregion
4280
- //#region ../core/dist/change-flow-mr6T-6nU.d.mts
4280
+ //#region ../core/dist/change-flow-B_bGbrkO.d.mts
4281
4281
  //#region src/config-hooks.d.ts
4282
4282
  /**
4283
4283
  * Test-only pauses inside `ConfigStore.convertToVersion3`, for the tests that prove a write racing the conversion is
@@ -4369,7 +4369,7 @@ interface ResolvedPaths {
4369
4369
  */
4370
4370
  downloadsDir: string;
4371
4371
  }
4372
- type ErrorCode = "UNEXPECTED" | "APPROVAL_REQUIRED" | "APPROVAL_PENDING" | "APPROVAL_EXPIRED" | "APPROVAL_VOID" | "SEND_OUTCOME_UNKNOWN" | "SEND_REFUSED" | "POLICY_NEVER" | "RATE_CAPPED" | "UNSENDABLE_HTML" | "LOOSENING_REFUSED" | "UPDATE_REQUIRED" | "USAGE" | "CURSOR_MISMATCH" | "BAD_DATA" | "REPLY_INVALID" | "NOT_FOUND" | "PROVIDER_UNAVAILABLE" | "SECRET_STORE_UNAVAILABLE" | "TRANSIENT" | "KEYCHAIN_APPROVAL_PENDING" | "LOCK_TIMEOUT" | "AUTH_REQUIRED" | "SCOPE_MISSING" | "CONFIG";
4372
+ type ErrorCode = "UNEXPECTED" | "APPROVAL_REQUIRED" | "APPROVAL_PENDING" | "APPROVAL_EXPIRED" | "APPROVAL_VOID" | "SEND_OUTCOME_UNKNOWN" | "SEND_REFUSED" | "POLICY_NEVER" | "RATE_CAPPED" | "UNSENDABLE_HTML" | "LOOSENING_REFUSED" | "UPDATE_REQUIRED" | "USAGE" | "CURSOR_MISMATCH" | "BAD_DATA" | "REPLY_INVALID" | "NOT_FOUND" | "PROVIDER_UNAVAILABLE" | "SOURCE_UNAVAILABLE" | "SECRET_STORE_UNAVAILABLE" | "TRANSIENT" | "KEYCHAIN_APPROVAL_PENDING" | "LOCK_TIMEOUT" | "AUTH_REQUIRED" | "SCOPE_MISSING" | "WHATSAPP_VISIBILITY_SEAM_REQUIRED" | "CONFIG";
4373
4373
  interface CommsErrorOptions {
4374
4374
  hint?: string;
4375
4375
  details?: Record<string, unknown>;
@@ -4999,6 +4999,28 @@ interface LooseningConsent {
4999
4999
  * change's is the live change policy. It never changes when the configuration does.
5000
5000
  */
5001
5001
  type ApprovalRoute = "chat" | "confirm";
5002
+ /** The four activation documents that may receive standing disclosure authority. */
5003
+ type DisclosureActivationKind = "rule" | "judge-budget" | "judge-kind" | "enable-all";
5004
+ /** An immutable version named by a standing disclosure authorisation. */
5005
+ type DisclosureVersionKind = "rule" | "target" | "subscriber" | "judge" | "judge-budget" | "judge-kind";
5006
+ interface DisclosureVersion {
5007
+ readonly kind: DisclosureVersionKind;
5008
+ readonly id: string;
5009
+ readonly version: number;
5010
+ }
5011
+ /**
5012
+ * The content-free, canonical authority for a future disclosure (local-event design D2).
5013
+ *
5014
+ * The event daemon derives this from one canonical activation document. Core does not construct
5015
+ * that document; it verifies the closed shape that makes an independently supplied version list
5016
+ * impossible to smuggle into a disclosure record.
5017
+ */
5018
+ interface DisclosureBinding {
5019
+ readonly digest: string;
5020
+ readonly activationIntentId: string;
5021
+ readonly activationKind: DisclosureActivationKind;
5022
+ readonly versions: readonly DisclosureVersion[];
5023
+ }
5002
5024
  /**
5003
5025
  * What a record's `inboxId` names. `owner` — a mailbox or account that existed when it was made (every send and
5004
5026
  * download, and a change to one). `prospective` — a change that creates its owner, which has no id yet. `global` — a
@@ -5130,7 +5152,10 @@ interface AuditRecord {
5130
5152
  recipients?: string[];
5131
5153
  approvalId?: string;
5132
5154
  reason?: string;
5133
- surface?: "cli" | "mcp";
5155
+ /** The process that performed the work; daemon work keeps its caller separately in `origin`. */
5156
+ surface?: "cli" | "mcp" | "app" | "daemon";
5157
+ /** The requesting client surface; autonomous work is `daemon`/`daemon`. */
5158
+ origin?: "cli" | "mcp" | "app" | "daemon";
5134
5159
  /**
5135
5160
  * The policy that decided how this was approved. Written by change approvals, where it is the whole question
5136
5161
  * afterwards: a loosening a person typed a code for and one an agent claimed after a yes in chat look the same in
@@ -5250,7 +5275,7 @@ interface CorruptStub {
5250
5275
  readonly reason: UnreadableReason | IntegrityReason;
5251
5276
  }
5252
5277
  /** What an attribution-verified corrupt record may still show its owner: the fields its binding was made over. */
5253
- interface SafeFields {
5278
+ interface OrdinarySafeFields {
5254
5279
  readonly kind: ApprovalKind;
5255
5280
  readonly channel: string;
5256
5281
  readonly ownerScope: OwnerScope;
@@ -5260,6 +5285,11 @@ interface SafeFields {
5260
5285
  readonly draftMessageId: string;
5261
5286
  readonly expect: Expectation;
5262
5287
  }
5288
+ /** A disclosure record has no channel owner or sender-controlled operational fields to expose. */
5289
+ interface DisclosureSafeFields {
5290
+ readonly kind: "disclosure";
5291
+ }
5292
+ type SafeFields = OrdinarySafeFields | DisclosureSafeFields;
5263
5293
  type StoredApproval = {
5264
5294
  readonly form: "v2";
5265
5295
  readonly record: ApprovalRecord;
@@ -5385,7 +5415,7 @@ type ApprovalChannel = "elicitation" | "terminal";
5385
5415
  * and a kind on every record, so that none can be spent as another. Without it, a person who approved a post at a
5386
5416
  * terminal would also have approved whatever change an agent claimed under the same id.
5387
5417
  */
5388
- type ApprovalKind = "send" | "change" | "download";
5418
+ type ApprovalKind = "send" | "change" | "download" | "disclosure";
5389
5419
  /** The inbox or account a change is about. `id` is absent when the change connects it, and it does not exist yet. */
5390
5420
  interface ChangeTarget {
5391
5421
  kind: "inbox" | "account";
@@ -5562,6 +5592,17 @@ interface ApprovalRecord {
5562
5592
  change?: ChangeBinding | undefined;
5563
5593
  /** For a download's question: what it asked about, and the folders it offered. `contentDigest` is `downloadDigest`. */
5564
5594
  download?: DownloadBinding | undefined;
5595
+ /** For a standing disclosure: the canonical, content-free event activation authority. */
5596
+ disclosure?: DisclosureBinding | undefined;
5597
+ }
5598
+ /**
5599
+ * The stored branch for standing disclosure. Its runtime decoder refuses every field belonging to send, change or
5600
+ * download records; this type is what the three disclosure-only methods return.
5601
+ */
5602
+ interface DisclosureApprovalRecord extends Omit<ApprovalRecord, "kind" | "disclosure" | "approvedVia" | "channel" | "ownerScope" | "route" | "inboxId" | "inboxSub" | "draftId" | "draftMessageId" | "contentDigest" | "sendEpoch" | "policy" | "requiredPolicy" | "riskFlags" | "expect" | "approvedBindingDigest" | "sendingAt" | "sendingHeartbeatAt" | "sentAt" | "sentMessageId" | "failedAt" | "change" | "download"> {
5603
+ kind: "disclosure";
5604
+ disclosure: DisclosureBinding;
5605
+ approvedVia?: "terminal" | "app" | undefined;
5565
5606
  }
5566
5607
  interface CreateApprovalInput {
5567
5608
  /** The manifest channel preparing the send. A channel this release does not know is refused. */
@@ -5706,6 +5747,8 @@ declare class ApprovalStore {
5706
5747
  */
5707
5748
  ensurePruned(options?: MaintenanceOptions): Promise<MaintenanceStatus>;
5708
5749
  create(input: CreateApprovalInput): Promise<ApprovalRecord>;
5750
+ /** Creates only a pending standing disclosure approval from a daemon-derived canonical binding. */
5751
+ createDisclosure(binding: DisclosureBinding): Promise<DisclosureApprovalRecord>;
5709
5752
  /**
5710
5753
  * One approval, in whichever of its four forms it is (`StoredApproval`), read against one configuration snapshot —
5711
5754
  * or null when there is none. Nothing is written.
@@ -5748,6 +5791,21 @@ declare class ApprovalStore {
5748
5791
  issueChallenge(approvalId: string, kind?: ApprovalKind, _platform?: NodeJS.Platform, options?: {
5749
5792
  usedSaid?: UsedSaid | undefined;
5750
5793
  }): Promise<string>;
5794
+ /** Issues the typed challenge that a terminal or the trusted desktop app must answer for disclosure. */
5795
+ issueDisclosureChallenge(approvalId: string): Promise<string>;
5796
+ /**
5797
+ * Approves one exact standing disclosure authority. MCP/chat forms are never an approval channel here: `app` means
5798
+ * the desktop application's trusted typed-challenge bridge, not a client form.
5799
+ */
5800
+ approveDisclosure(approvalId: string, liveBinding: DisclosureBinding, answer: string, via: "terminal" | "app"): Promise<DisclosureApprovalRecord>;
5801
+ /**
5802
+ * Claims an approved disclosure once, persisting the exact `usedAt` that recovery must subsequently reuse.
5803
+ * A specialised caller may impose a shorter deadline; it is evaluated while this record's transition lock is held,
5804
+ * so a check made before waiting on that lock cannot accidentally consume the approval after it has expired.
5805
+ */
5806
+ claimForDisclosure(approvalId: string, liveBinding: DisclosureBinding, options?: {
5807
+ readonly deadlineMs?: number | undefined;
5808
+ }): Promise<DisclosureApprovalRecord>;
5751
5809
  /**
5752
5810
  * A human approved through a confirm channel by typing the issued challenge. The draft must still be exactly what the
5753
5811
  * record was prepared for; otherwise the record is voided, because the human would be approving content the record
@@ -6069,9 +6127,13 @@ interface TaintHandle {
6069
6127
  id: string;
6070
6128
  }
6071
6129
  type TaintSource = "header" | "body";
6130
+ /** Why the content reached this process. This is separate from `TaintSource`, which remains the cap priority. */
6131
+ type TaintOrigin = "read" | "event";
6072
6132
  interface TaintObservation {
6073
6133
  address: string;
6074
6134
  source: TaintSource;
6135
+ /** Old readers did not record this, so an omitted origin remains an ordinary read. */
6136
+ origin?: TaintOrigin | undefined;
6075
6137
  inboxId: string;
6076
6138
  messageId?: string | undefined;
6077
6139
  }
@@ -6079,6 +6141,8 @@ interface TaintObservation {
6079
6141
  interface TaintHandleObservation {
6080
6142
  handle: TaintHandle;
6081
6143
  source: TaintSource;
6144
+ /** Old readers did not record this, so an omitted origin remains an ordinary read. */
6145
+ origin?: TaintOrigin | undefined;
6082
6146
  inboxId: string;
6083
6147
  messageId?: string | undefined;
6084
6148
  }
@@ -6102,6 +6166,8 @@ interface TaintAggregate {
6102
6166
  readonly at: string;
6103
6167
  readonly source: TaintSource;
6104
6168
  readonly inboxIds: readonly string[];
6169
+ /** Sorted, duplicate-free provenance. A base entry with no sidecar record is a legacy read. */
6170
+ readonly origins: readonly TaintOrigin[];
6105
6171
  }
6106
6172
  interface TaintCheck {
6107
6173
  address: boolean;
@@ -6111,10 +6177,15 @@ interface TaintCheck {
6111
6177
  /** Its domain's, when that counts — not a public mailbox provider — and is in the window. */
6112
6178
  domainSeen?: TaintAggregate | undefined;
6113
6179
  }
6180
+ /** Testable interruption point for the crash-safe, two-lock sidecar protocol. */
6181
+ interface TaintStoreHooks {
6182
+ afterOriginsCommit?: (() => Promise<void>) | undefined;
6183
+ onLockAcquired?: ((lock: "origins" | "taint" | "handles") => void) | undefined;
6184
+ }
6114
6185
  declare class TaintStore {
6115
6186
  #private;
6116
6187
  readonly directory: string;
6117
- constructor(stateDir: string, now?: () => Date);
6188
+ constructor(stateDir: string, now?: () => Date, hooks?: TaintStoreHooks);
6118
6189
  /**
6119
6190
  * Records observations. Throws if it cannot — callers must fail the read rather than return content whose taint
6120
6191
  * was not recorded.
@@ -6132,6 +6203,8 @@ declare class TaintStore {
6132
6203
  * carries a domain: two ids sharing a workspace says nothing about either of them.
6133
6204
  */
6134
6205
  checkHandle(handle: TaintHandle): Promise<boolean>;
6206
+ /** The merged provenance of a retained handle, or no origins when it is not in the taint window. */
6207
+ checkHandleOrigins(handle: TaintHandle): Promise<readonly TaintOrigin[]>;
6135
6208
  }
6136
6209
  //#endregion
6137
6210
  //#region src/core.d.ts
@@ -13349,9 +13422,121 @@ type RegisteredPrompt = {
13349
13422
  remove(): void;
13350
13423
  };
13351
13424
  //#endregion
13425
+ //#region src/config.d.ts
13426
+ interface WhatsAppAccount extends AccountConfig {
13427
+ /** The store, when it is not WhatsApp for Mac's default. Absent means the default. */
13428
+ source?: string | undefined;
13429
+ }
13430
+ //#endregion
13352
13431
  //#region src/source/types.d.ts
13432
+ /**
13433
+ * What a `ZWAMESSAGE.ZMESSAGETYPE` number means.
13434
+ *
13435
+ * From [F] and [W] in `schema.ts` — two independent public tables that agree where they overlap — with [I] for 5 and
13436
+ * [K] for 6, 14 and 15. iLEAPP says plainly that it has no source for the rest; neither has this. A number not listed
13437
+ * is reported as `unknown` with the number, never guessed at.
13438
+ */
13439
+ type MessageKind = "text" | "image" | "video" | "audio" | "contact" | "location" | "group-event" | "link" | "document" | "system" | "gif" | "waiting" | "deleted" | "sticker" | "poll" | "video-note" | "call" | "album" | "unknown";
13353
13440
  /** What a chat is, from its JID — the one part of a chat WhatsApp assigns and nobody in it can choose. */
13354
13441
  type ChatKind = "direct" | "hidden-number" | "group" | "status" | "broadcast" | "channel" | "unknown";
13442
+ /**
13443
+ * [M] imports exactly three kinds of conversation — `@s.whatsapp.net`, `@lid` and `@g.us` — and skips every other
13444
+ * session, comparing JIDs lower-cased; [I] names `@newsletter` chats as channels. The rest were seen rather than
13445
+ * sourced: a real store kept each contact's status posts in a session of its own, `<number>@status`, and WhatsApp's
13446
+ * own status feed and broadcast lists use `status@broadcast` and `<id>@broadcast`. A JID none of these match is
13447
+ * `unknown`, never guessed at.
13448
+ */
13449
+ export declare function chatKindOf(jid: string): ChatKind;
13450
+ //#endregion
13451
+ //#region src/visibility.d.ts
13452
+ /**
13453
+ * Which chats an agent may see in an account: the person's allow and deny lists.
13454
+ *
13455
+ * - **deny**: chats an agent must never see. A denied chat is not listed, searched, read, counted or drafted to, and
13456
+ * asking for it by id gets the answer a chat that does not exist gets.
13457
+ * - **allow**: when it has anything on it, the only chats an agent may see.
13458
+ * - Denied wins over allowed. With neither list, every chat is visible — status updates are still left out of
13459
+ * listings and searches unless asked for, which is a default, not a list.
13460
+ *
13461
+ * An entry is a chat id, or a phone number kept as its `…@s.whatsapp.net` id. A number names a person rather than one
13462
+ * chat: their one-to-one chat and their own status posts (`<number>@status`) — and, in a status feed, where each post
13463
+ * is its author's, the posts they wrote.
13464
+ *
13465
+ * **A status post needs an author the lists can be checked against.** Its author is the person WhatsApp recorded as
13466
+ * sending it, or — in a contact's own `<number>@status` session — that contact; one the person posted is theirs. A post
13467
+ * with no author (no `ZFROMJID`, no group-member row, or an id that names no person) could be anyone's, including
13468
+ * someone denied, so it is shown only while the lists hide nobody, and otherwise hidden and counted
13469
+ * (`unattributed`). Failing closed costs posts nobody can be named for; failing open showed a denied person's.
13470
+ *
13471
+ * **A group is a chat**, allowed or denied whole: denying someone does not remove what they wrote in a group the agent
13472
+ * may see, whether WhatsApp recorded them as its author or not — so an unknown author in a group hides nothing either.
13473
+ * A status post is its author's alone, and a feed only files them together. A group message is part of a conversation
13474
+ * the others answer and quote, so hiding one person's lines would leave the rest describing them. It could not be
13475
+ * done reliably either: a member can appear under a hidden-number id (`…@lid`) that their number does not match, and
13476
+ * an allowed group would lose every member not on the allow list. Denying the group is what hides it.
13477
+ *
13478
+ * Every read applies this — `WhatsAppIndex` cannot be opened without one — and so does `sync`, which leaves what it
13479
+ * hides out of the index, so a hidden chat's messages are not kept in a second copy on disk either.
13480
+ */
13481
+ interface ChatLists {
13482
+ readonly allow: readonly string[];
13483
+ readonly deny: readonly string[];
13484
+ }
13485
+ /** One entry per person or chat: a phone number and the ids that carry it compare equal. */
13486
+ export declare function listKey(jid: string): string;
13487
+ /**
13488
+ * The one visibility unit a raw row belongs to. The daemon persists this key, but only this channel defines the
13489
+ * vocabulary: an ordinary chat is its list key; a status post is the pair of its status chat and its author (or no
13490
+ * author), because the lists can hide either one — denying the status feed hides every author in it.
13491
+ */
13492
+ export declare function visibilityUnitKey(chatId: string, chatKind: ChatKind | string, senderJid: string | null): string;
13493
+ export declare class Visibility {
13494
+ #private;
13495
+ constructor(lists: ChatLists | undefined);
13496
+ /** Whether an agent may see this chat at all. */
13497
+ seesChat(chatId: string): boolean;
13498
+ /**
13499
+ * Whether an agent may see this message: its chat must be visible, and in a status chat, its author — the person's
13500
+ * own posts always, anyone else's only when they are known and visible, or when the lists hide nobody at all.
13501
+ */
13502
+ seesMessage(chatId: string, chatKind: ChatKind | string, senderJid: string | null, fromMe: boolean): boolean;
13503
+ /** Whether a durable source unit remains visible under these same lists. */
13504
+ seesUnit(unitKey: string): boolean;
13505
+ /** A status post someone else wrote with no author the lists can be checked against: hidden while they hide anyone. */
13506
+ unattributed(chatId: string, chatKind: ChatKind | string, senderJid: string | null, fromMe: boolean): boolean;
13507
+ }
13508
+ /**
13509
+ * Who wrote a status post, as an id the lists can be checked against — or null when nobody can be named.
13510
+ *
13511
+ * The sender WhatsApp recorded, when it is a person (a number, or a hidden number); otherwise, in a contact's own
13512
+ * session, that contact. A group's, a feed's or a broadcast's id is no author.
13513
+ */
13514
+ export declare function statusAuthor(chatId: string, senderJid: string | null): string | null;
13515
+ //#endregion
13516
+ //#region src/lists.d.ts
13517
+ declare class ChatListStore {
13518
+ #private;
13519
+ readonly path: string;
13520
+ constructor(configDir: string);
13521
+ /** One account's lists; none when the account has no entry. */
13522
+ of(accountId: string): Promise<ChatLists>;
13523
+ /**
13524
+ * Reads the live list while holding the same inter-process gate as allow/deny/clear. Event consumers get the
13525
+ * parsed policy and its canonical digest, never a second mutable copy of the list file.
13526
+ */
13527
+ withCurrent<T>(accountId: string, work: (current: Readonly<{
13528
+ version: 1;
13529
+ lists: ChatLists;
13530
+ digest: string;
13531
+ }>) => Promise<T> | T): Promise<T>;
13532
+ /** Changes one account's lists under the file's lock; an account left with neither list has no entry. */
13533
+ update(accountId: string, change: (lists: ChatLists) => ChatLists): Promise<{
13534
+ before: ChatLists;
13535
+ after: ChatLists;
13536
+ }>;
13537
+ /** Forgets an account's lists: when the account is removed. */
13538
+ forget(accountId: string): Promise<void>;
13539
+ }
13355
13540
  //#endregion
13356
13541
  //#region src/source/snapshot.d.ts
13357
13542
  /**
@@ -13409,6 +13594,43 @@ interface SourceIo {
13409
13594
  /** Opens a file read-only, refusing a link and never waiting on a pipe. */
13410
13595
  open(path: string): Promise<SourceHandle>;
13411
13596
  }
13597
+ interface SourceOptions {
13598
+ /**
13599
+ * What a refusal names to run — `status` at a terminal, an `add` for another store — located as the package printing
13600
+ * it finds its own command (`WhatsAppContext.handoffs`). Required: there is no bare `agent-whatsapp` to fall back on.
13601
+ */
13602
+ handoffs: CliHandoffs;
13603
+ io?: SourceIo | undefined;
13604
+ env?: NodeJS.ProcessEnv | undefined;
13605
+ platform?: NodeJS.Platform | undefined;
13606
+ timeoutMs?: number | undefined;
13607
+ /** Copies attempted before a store that keeps changing is refused. */
13608
+ attempts?: number | undefined;
13609
+ sleep?: ((ms: number) => Promise<void>) | undefined;
13610
+ /** The account's default store, so "not found" can say WhatsApp may not be installed rather than blame a path. */
13611
+ isDefault?: boolean | undefined;
13612
+ }
13613
+ //#endregion
13614
+ //#region src/spike-migration.d.ts
13615
+ interface SpikeMigration {
13616
+ /** Moved into `config.json` by this run. */
13617
+ migrated: string[];
13618
+ /** Already there, by id, from an earlier run that stopped before it could put the file aside. */
13619
+ alreadyThere: string[];
13620
+ /**
13621
+ * Left in the old file, with why — and, when the spike built one for it, where its index still is: a plaintext copy
13622
+ * of its messages that no account reads now and no command deletes.
13623
+ */
13624
+ skipped: {
13625
+ name: string;
13626
+ reason: string;
13627
+ index?: string | undefined;
13628
+ }[];
13629
+ /** Where the old file went, or null when it stayed because nothing could be moved yet (`deferred`). */
13630
+ keptAs: string | null;
13631
+ /** Why nothing moved at all, when nothing could. */
13632
+ deferred?: string | undefined;
13633
+ }
13412
13634
  //#endregion
13413
13635
  //#region src/context.d.ts
13414
13636
  /**
@@ -13443,6 +13665,58 @@ interface WhatsAppContextOptions {
13443
13665
  sourceTimeoutMs?: number | undefined;
13444
13666
  platform?: NodeJS.Platform | undefined;
13445
13667
  }
13668
+ interface ResolvedAccount {
13669
+ name: string;
13670
+ account: WhatsAppAccount;
13671
+ lists: ChatLists;
13672
+ }
13673
+ declare class WhatsAppContext {
13674
+ #private;
13675
+ readonly env: NodeJS.ProcessEnv;
13676
+ readonly now: () => Date;
13677
+ readonly surface: "cli" | "mcp";
13678
+ readonly platform: NodeJS.Platform;
13679
+ readonly core: Core;
13680
+ /** The commands this tells a person to run, located, and quoted for this context's shell. */
13681
+ readonly handoffs: CliHandoffs;
13682
+ readonly lists: ChatListStore;
13683
+ /** The name a pinned server was started for, as given. */
13684
+ readonly pinned: string | undefined;
13685
+ constructor(options?: WhatsAppContextOptions);
13686
+ /** The spike's accounts moved in, once per process; what happened, when there was a spike file to move. */
13687
+ migration(): Promise<SpikeMigration | null>;
13688
+ /** core's configuration, after the spike's accounts have moved into it. */
13689
+ config(): Promise<Config$1>;
13690
+ /**
13691
+ * The server's pin, checked to name a WhatsApp account, and remembered by its id.
13692
+ *
13693
+ * By id, as Slack's server does, because a rename can move the name under a running server: the pin follows the
13694
+ * account, and a new account that took the name afterwards is not one anybody pinned this server to.
13695
+ */
13696
+ checkPin(): Promise<void>;
13697
+ /** One account, resolved and checked, with the person's lists for it — which every read applies. */
13698
+ account(named: string | undefined): Promise<ResolvedAccount>;
13699
+ /** The accounts a call without one covers: every WhatsApp account, or the pinned one alone. */
13700
+ accountNames(): Promise<string[]>;
13701
+ /** Where an account's store is, and whether that is the WhatsApp for Mac default. */
13702
+ storeOf(account: WhatsAppAccount): {
13703
+ path: string;
13704
+ isDefault: boolean;
13705
+ };
13706
+ sourceOptions(isDefault: boolean): SourceOptions;
13707
+ /** The account's own directory under core's state directory: its index and its brief snapshots. */
13708
+ accountDir(account: WhatsAppAccount): string;
13709
+ /**
13710
+ * The lock a sync and a remove of this account both hold, so neither runs while the other does. Beside the
13711
+ * account's directory rather than in it, because a remove deletes the directory while it holds the lock.
13712
+ */
13713
+ syncLock(account: WhatsAppAccount): string;
13714
+ /**
13715
+ * The account with this id as it is now, under whatever name it has now, with its lists as they are now — or null
13716
+ * when it is gone. For a sync to look again, under its lock, at what it started with.
13717
+ */
13718
+ accountById(id: string): Promise<ResolvedAccount | null>;
13719
+ }
13446
13720
  //#endregion
13447
13721
  //#region src/mcp/server.d.ts
13448
13722
  /**
@@ -13495,4 +13769,67 @@ export declare function composeDraft(request: {
13495
13769
  text: string;
13496
13770
  }): DraftResult;
13497
13771
  //#endregion
13498
- export type { DraftResult, WhatsAppMcpOptions, WhatsAppMcpServer };
13772
+ //#region src/source/event-reader.d.ts
13773
+ /**
13774
+ * The event reader deliberately keeps the source tuple before the presentation index derives sender information.
13775
+ * A row-id is available only to produce a stable checked-copy read order; it is never returned as identity.
13776
+ */
13777
+ interface RawEventMessage {
13778
+ readonly sourceOrder: number;
13779
+ readonly chatJid: string | null;
13780
+ readonly chatKind: ChatKind | null;
13781
+ readonly senderJidRaw: string | null;
13782
+ readonly stanzaId: string | null;
13783
+ /** Exactly false is eligible. A malformed, absent, or non-boolean Core Data value remains unknown. */
13784
+ readonly fromMe: boolean | null;
13785
+ readonly at: string | null;
13786
+ readonly kind: MessageKind;
13787
+ readonly body: string | null;
13788
+ }
13789
+ //#endregion
13790
+ //#region src/operations/events.d.ts
13791
+ /**
13792
+ * The held event owner receives this deliberately narrow, read-only operation object. In particular it never
13793
+ * exposes ChatListStore, whose update/forget methods are local list writes rather than event-reader capabilities.
13794
+ */
13795
+ interface WhatsAppEventOperations {
13796
+ withCurrentEventVisibility<T>(input: Readonly<{
13797
+ accountId: string;
13798
+ }>, work: (visibility: CurrentEventVisibility) => Promise<T> | T): Promise<T>;
13799
+ withEventSnapshot<T>(input: Readonly<{
13800
+ accountId: string;
13801
+ }>, work: (snapshot: EventSnapshot) => Promise<T> | T): Promise<T>;
13802
+ }
13803
+ /** Opens this channel's own context from the owner's config directory and returns no write-capable local-store handle. */
13804
+ export declare function openWhatsAppEventOperations(input: Readonly<{
13805
+ configDir: string;
13806
+ }>): WhatsAppEventOperations;
13807
+ interface CurrentEventVisibility {
13808
+ /** The list-file format version, not the daemon's durable journal version. */
13809
+ readonly version: 1;
13810
+ readonly digest: string;
13811
+ readonly seesMessage: (chatJid: string, chatKind: string, senderJidRaw: string, fromMe: boolean) => boolean;
13812
+ readonly seesUnit?: ((unitKey: string) => boolean) | undefined;
13813
+ }
13814
+ interface EventSnapshot {
13815
+ readonly accountId: string;
13816
+ readonly accountName: string;
13817
+ readonly visibility: CurrentEventVisibility;
13818
+ readonly messages: readonly RawEventMessage[];
13819
+ }
13820
+ /**
13821
+ * The channel's live-list capability boundary. The callback runs while `.whatsapp-chats.lock` is held, so a daemon
13822
+ * may place a synchronous SQLite decision immediately after this read without a list command racing it.
13823
+ */
13824
+ export declare function withCurrentEventVisibility<T>(context: WhatsAppContext, input: Readonly<{
13825
+ accountId: string;
13826
+ }>, work: (visibility: CurrentEventVisibility) => Promise<T> | T): Promise<T>;
13827
+ /**
13828
+ * Runs the checked-copy lifecycle for the event source. The normal index is rebuilt first, but the source copies
13829
+ * only raw values from the checked copy and never observes the rebuilt index.
13830
+ */
13831
+ export declare function withEventSnapshot<T>(context: WhatsAppContext, input: Readonly<{
13832
+ accountId: string;
13833
+ }>, work: (snapshot: EventSnapshot) => Promise<T> | T): Promise<T>;
13834
+ //#endregion
13835
+ export type { ChatKind, CurrentEventVisibility, DraftResult, EventSnapshot, RawEventMessage, WhatsAppEventOperations, WhatsAppMcpOptions, WhatsAppMcpServer };
package/dist/index.mjs CHANGED
@@ -1,11 +1,96 @@
1
- import { gt as VERSION, ht as PACKAGE_NAME, u as composeDraft } from "./sync-Ce-u-zuv.mjs";
2
- import { t as createWhatsAppMcpServer } from "./server-DFGzE4pA.mjs";
1
+ import { C as chatKindOf, D as text, E as numeric, Et as VERSION, R as CommsError, T as kindOf, Tt as PACKAGE_NAME, _ as WhatsAppContext, d as composeDraft, g as visibilityUnitKey, h as statusAuthor, m as listKey, n as withCheckedCopy, p as Visibility, w as coreDataToIso, x as inspectSchema, y as rebuildIndex } from "./sync-Dx4QM1gQ.mjs";
2
+ import { t as createWhatsAppMcpServer } from "./server-Du5WnAAo.mjs";
3
+ //#region src/source/event-reader.ts
4
+ function rawFromMe(value) {
5
+ const number = numeric(value);
6
+ if (number === 0) return false;
7
+ if (number === 1) return true;
8
+ return null;
9
+ }
10
+ /**
11
+ * Reads only raw, checked-copy facts needed by the local event source. It intentionally does not call
12
+ * `rebuildIndex`, `WhatsAppIndex`, or any presentation sender fallback.
13
+ */
14
+ function readRawEventMessages(database, _report) {
15
+ return database.prepare(`SELECT m.Z_PK AS sourceOrder, c.ZCONTACTJID AS chatJid, m.ZFROMJID AS senderJidRaw,
16
+ m.ZSTANZAID AS stanzaId, m.ZISFROMME AS fromMe, m.ZMESSAGEDATE AS at,
17
+ m.ZMESSAGETYPE AS typeCode, m.ZTEXT AS body
18
+ FROM ZWAMESSAGE AS m
19
+ LEFT JOIN ZWACHATSESSION AS c ON c.Z_PK = m.ZCHATSESSION
20
+ ORDER BY m.Z_PK`).all().map((row) => {
21
+ const chatJid = text(row.chatJid) || null;
22
+ const type = numeric(row.typeCode);
23
+ return {
24
+ sourceOrder: numeric(row.sourceOrder) ?? 0,
25
+ chatJid,
26
+ chatKind: chatJid === null ? null : chatKindOf(chatJid),
27
+ senderJidRaw: text(row.senderJidRaw) || null,
28
+ stanzaId: text(row.stanzaId) || null,
29
+ fromMe: rawFromMe(row.fromMe),
30
+ at: coreDataToIso(numeric(row.at)),
31
+ kind: kindOf(type).kind,
32
+ body: text(row.body)
33
+ };
34
+ });
35
+ }
36
+ //#endregion
37
+ //#region src/operations/events.ts
38
+ /** Opens this channel's own context from the owner's config directory and returns no write-capable local-store handle. */
39
+ function openWhatsAppEventOperations(input) {
40
+ const context = new WhatsAppContext({ pathOverrides: { configDir: input.configDir } });
41
+ return {
42
+ withCurrentEventVisibility: (visibilityInput, work) => withCurrentEventVisibility(context, visibilityInput, work),
43
+ withEventSnapshot: (snapshotInput, work) => withEventSnapshot(context, snapshotInput, work)
44
+ };
45
+ }
46
+ /**
47
+ * The channel's live-list capability boundary. The callback runs while `.whatsapp-chats.lock` is held, so a daemon
48
+ * may place a synchronous SQLite decision immediately after this read without a list command racing it.
49
+ */
50
+ async function withCurrentEventVisibility(context, input, work) {
51
+ return context.lists.withCurrent(input.accountId, async ({ version, lists, digest }) => {
52
+ const visibility = new Visibility(lists);
53
+ return work({
54
+ version,
55
+ digest,
56
+ seesMessage: (chatJid, chatKind, senderJidRaw, fromMe) => visibility.seesMessage(chatJid, chatKind, senderJidRaw, fromMe),
57
+ seesUnit: (unitKey) => visibility.seesUnit(unitKey)
58
+ });
59
+ });
60
+ }
61
+ /**
62
+ * Runs the checked-copy lifecycle for the event source. The normal index is rebuilt first, but the source copies
63
+ * only raw values from the checked copy and never observes the rebuilt index.
64
+ */
65
+ async function withEventSnapshot(context, input, work) {
66
+ return withCheckedCopy(context, input, async ({ account, accountName, directory, database, snapshot }) => {
67
+ const report = inspectSchema(database);
68
+ const current = await context.accountById(account.id);
69
+ if (current === null) throw new CommsError("NOT_FOUND", "the account was removed while its event snapshot was being read");
70
+ await rebuildIndex(directory, database, report, {
71
+ indexedAt: context.now().toISOString(),
72
+ copied: snapshot.copied
73
+ }, new Visibility(current.lists), async () => {
74
+ const latest = await context.accountById(account.id);
75
+ return latest !== null && JSON.stringify(latest.lists) === JSON.stringify(current.lists);
76
+ });
77
+ if (await context.accountById(account.id) === null) throw new CommsError("NOT_FOUND", "the account was removed while its event snapshot was being read");
78
+ const messages = readRawEventMessages(database, report).filter((message) => (message.fromMe === false || message.fromMe === true) && message.chatJid !== null && message.chatKind !== null);
79
+ return withCurrentEventVisibility(context, { accountId: account.id }, (visibility) => work({
80
+ accountId: account.id,
81
+ accountName,
82
+ visibility,
83
+ messages
84
+ }));
85
+ });
86
+ }
87
+ //#endregion
3
88
  //#region src/index.ts
4
89
  /**
5
90
  * The library entry of `@agentcomms/whatsapp`.
6
91
  *
7
- * Small on purpose, as the Slack package's is: the server, the draft composer and the version. Everything else is
8
- * reached through the `agent-whatsapp` command.
92
+ * Small on purpose, as the Slack package's is: the server, the draft composer, the event list-lock capability and the
93
+ * version. Everything else is reached through the `agent-whatsapp` command.
9
94
  */
10
95
  //#endregion
11
- export { PACKAGE_NAME, VERSION, composeDraft, createWhatsAppMcpServer };
96
+ export { PACKAGE_NAME, VERSION, Visibility, chatKindOf, composeDraft, createWhatsAppMcpServer, listKey, openWhatsAppEventOperations, statusAuthor, visibilityUnitKey, withCurrentEventVisibility, withEventSnapshot };
@@ -1,4 +1,4 @@
1
- import { E as strictToolArguments, a as listChats, d as draftMessage, gt as VERSION, i as CHAT_KINDS, j as UNTRUSTED_NOTICE, m as WhatsAppContext, n as whatsappStatus, o as readChat, s as searchMessages, st as toCommsError, t as syncAccount, ut as updateToolGate } from "./sync-Ce-u-zuv.mjs";
1
+ import { Et as VERSION, I as strictToolArguments, V as UNTRUSTED_NOTICE, _ as WhatsAppContext, _t as toCommsError, a as CHAT_KINDS, bt as updateToolGate, c as searchMessages, f as draftMessage, o as listChats, r as whatsappStatus, s as readChat, t as syncAccount } from "./sync-Dx4QM1gQ.mjs";
2
2
  import { c as boolean, h as number, x as string } from "./iso-USuVU_sk.mjs";
3
3
  import { t as McpServer } from "./mcp-DXXb3Vv3-kCxsVLO4.mjs";
4
4
  //#region src/mcp/server.ts
@@ -1,4 +1,4 @@
1
- import { t as createWhatsAppMcpServer } from "./server-DFGzE4pA.mjs";
1
+ import { t as createWhatsAppMcpServer } from "./server-Du5WnAAo.mjs";
2
2
  //#region src/mcp/stdio-entry.ts
3
3
  /**
4
4
  * Starting the stdio server. On stdio, stdout is the protocol, so the console is redirected to stderr first — the