@hraness/message-like-me 0.8.8 → 0.8.10

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.10 (2026-09-13)
4
+
5
+ - Show a compact ASCII robot before interactive root help. Piped output,
6
+ machine-readable commands, version output, and command help stay unchanged.
7
+ - Refine the informational site's shared surfaces and robot identity. The native
8
+ Textbutler app remains in development; this package does not enable replies.
9
+ - Route legacy installation to the exact public `@hraness/message-like-me@0.8.10` npm package after release admission, with the same reviewed bytes mirrored in the immutable GitHub Release.
10
+
11
+ ## 0.8.9 (2026-09-10)
12
+
13
+ - Use the shared Paper theme on the informational website, with warm neutral surfaces, compact Nebula Sans headings, and a blue action color in light and dark appearances.
14
+ - Verify the theme snapshot's immutable source and file digests independently of the existing component package versions.
15
+ - Route installation to the exact public `@hraness/message-like-me@0.8.9` npm package after release admission, with the same reviewed bytes mirrored in the immutable GitHub Release. CLI behavior and private data boundaries remain unchanged.
16
+
3
17
  ## 0.8.8 (2026-09-09)
4
18
 
5
19
  - Route installation to the exact public `@hraness/message-like-me@0.8.8` npm package after release admission, with the same reviewed bytes in the immutable GitHub Release.
package/README.md CHANGED
@@ -1,7 +1,44 @@
1
- # Message Like Me
1
+ # Textbutler
2
2
 
3
3
  [![skills.sh](https://skills.sh/b/hraness/message-like-me)](https://skills.sh/hraness/message-like-me)
4
4
 
5
+ Textbutler is a personal message butler for macOS, powered by the coding agent
6
+ you choose. Activate a few contacts, give each relationship a private folder of
7
+ context, and let a clearly identified assistant help when it is useful.
8
+
9
+ The new product lives at [textbutler.app](https://textbutler.app). Its default
10
+ response looks like `🤖{ hello this is my response }`. Each contact can choose
11
+ the three symbols, a keyword, and smart or keyword-only response mode.
12
+
13
+ **Development status:** the source includes the Mac settings app, background
14
+ daemon lifecycle, owner-selected Ghostget conversation enrollment, optional
15
+ history initialization, private memory, executable hooks, reply policy and send
16
+ journal. Agentrouter includes a restricted Claude Agent SDK adapter and shared
17
+ account custody. Live automated replies remain unavailable until Ghostget's
18
+ durable events and scoped automation grants, and the provider's contact-only
19
+ execution, are qualified. Codex execution and native rich actions are also
20
+ unavailable. The Mac app has not been released as a signed/notarized download.
21
+ The website is informational; it has no connection to private messages or
22
+ contact folders.
23
+
24
+ Start with the [architecture and capability status](docs/textbutler/architecture.md),
25
+ [Textbutler runtime](packages/textbutler/README.md),
26
+ [Agentrouter](packages/agentrouter/README.md),
27
+ [transport adapter](packages/transport/README.md), or
28
+ [Mac app](apps/macos/README.md).
29
+
30
+ ```sh
31
+ bun install --frozen-lockfile --ignore-scripts
32
+ bun run check:textbutler
33
+ ```
34
+
35
+ The source is MIT licensed. New packages remain unpublished while their
36
+ contracts are developed. Message Like Me's published history readers and
37
+ message-bundle contracts are retained below for existing consumers; they are
38
+ not the new live messaging runtime.
39
+
40
+ ## Legacy Message Like Me history tools
41
+
5
42
  **A local-first CLI and Agent Skill for studying private messaging history and
6
43
  drafting messages that sound like you.**
7
44
 
@@ -43,7 +80,7 @@ Message Like Me requires Bun 1.3.14 or newer. Install the exact public npm
43
80
  package, then install both bundled Agent Skills:
44
81
 
45
82
  ```sh
46
- bun add --global @hraness/message-like-me@0.8.8
83
+ bun add --global @hraness/message-like-me@0.8.10
47
84
  messagelikeme skill install
48
85
  ```
49
86
 
package/dist/cli.js CHANGED
@@ -24750,7 +24750,7 @@ function rejectUnused(parsed, allowedOptions, allowedFlags) {
24750
24750
  import { isAbsolute as isAbsolute6, resolve as resolve8 } from "path";
24751
24751
 
24752
24752
  // src/version.ts
24753
- var MESSAGE_LIKE_ME_VERSION = "0.8.8";
24753
+ var MESSAGE_LIKE_ME_VERSION = "0.8.10";
24754
24754
 
24755
24755
  // src/command-input.ts
24756
24756
  var HELP = `Message Like Me ${MESSAGE_LIKE_ME_VERSION}
@@ -27504,6 +27504,19 @@ function runClosed(effect2) {
27504
27504
  return exports_Effect.runPromise(effect2);
27505
27505
  }
27506
27506
 
27507
+ // src/cli-intro.ts
27508
+ function terminalIntro(terminal) {
27509
+ if (terminal.isTTY !== true || terminal.term === "dumb" || (terminal.columns ?? 80) < 48)
27510
+ return "";
27511
+ return ` _|_
27512
+ .----- . textbutler
27513
+ | o o | A little help in your conversations.
27514
+ | === |
27515
+ '-----'
27516
+
27517
+ `;
27518
+ }
27519
+
27507
27520
  // src/io.ts
27508
27521
  var processIo = {
27509
27522
  stdout: (text3) => process.stdout.write(text3),
@@ -27514,7 +27527,12 @@ var processIo = {
27514
27527
  // src/cli.ts
27515
27528
  async function main(argv, io = processIo) {
27516
27529
  try {
27517
- await runCommand(argv, io);
27530
+ const rootHelp = argv.length === 0 || argv.length === 1 && argv[0] === "--help";
27531
+ const output = rootHelp && io === processIo ? {
27532
+ ...io,
27533
+ stdout: (text3) => io.stdout((text3 === HELP ? terminalIntro({ isTTY: process.stdout.isTTY, columns: process.stdout.columns, term: process.env.TERM }) : "") + text3)
27534
+ } : io;
27535
+ await runCommand(argv, output);
27518
27536
  return 0;
27519
27537
  } catch (error) {
27520
27538
  io.stderr(`${errorMessage(error)}
@@ -1,5 +1,72 @@
1
1
  # Publish Message Like Me
2
2
 
3
+ ## Textbutler informational-site delivery
4
+
5
+ The following explicit site-subject route supersedes the package-publication
6
+ prerequisite below for the informational Textbutler website only. It grants no
7
+ package publication, native app release, provider qualification, or live message
8
+ authority. The legacy tagged package route and its exact-byte/npm checks remain
9
+ unchanged. Source repository identity remains `hraness/message-like-me` until
10
+ the separately reviewed repository-name migration is applied.
11
+
12
+ Use the existing `Promote website production` workflow with `site_sha` set to
13
+ the exact reviewed current `main` commit, `site_ci_run_id` and
14
+ `site_ci_run_attempt` set to its successful `CI` run, and `release_tag` empty.
15
+ Do not rerun a promotion attempt; make a fresh attempt-1 dispatch. The route
16
+ requires exactly the current standalone/site job and macOS fixture/native job,
17
+ rejects an older attempt after CI is rerun, rebuilds the site, and admits a
18
+ bounded immutable Actions build-manifest artifact by exact run, attempt, source
19
+ tree, site subtree, lockfile digest, artifact ID/digest, and manifest bytes.
20
+
21
+ An advancing site target uses the same `website-production` ref, shared
22
+ promotion concurrency, existing status-only App, environment, rulesets, and
23
+ ordinary GitHub Actions ref writer as the legacy route. Before the App key is
24
+ available, complete governed Git histories and all ordered workflow-tree
25
+ changes must pass the site control-epoch admission. A changed workflow range
26
+ requires the independently reviewed exact digest emitted by preflight in
27
+ `control_epoch_digest`; unchanged ranges reject a supplied digest. Site epochs
28
+ use domain `textbutler/control-epoch/site/v1`, bind target and workflow source to
29
+ the same current `main` commit, and carry `tag: null`. They cannot authorize a
30
+ legacy package release. Both routes retain checked helper hashes and code-owner
31
+ review for the complete authority implementation.
32
+
33
+ Preserve the environment's actual protection rules. If the existing key
34
+ environment requires a reviewer, satisfy that exact run's review through
35
+ GitHub's normal environment approval interface after source admission and
36
+ independent control review. The site redesign does not remove reviewers, wait
37
+ timers or other runtime gates; the legacy setup policy below is not permission
38
+ to bypass an existing protection.
39
+
40
+ The site writer consumes prior status, proves that the ordinary writer is
41
+ denied by the exact required App status, attests one target, revokes the App
42
+ token, verifies current rules/status/source again, and makes one sterile
43
+ fast-forward push with the expected-old lease. App-only steps receive no ref
44
+ token; writer/admission steps receive no App key. The terminal consumption step
45
+ runs even after failure or cancellation when admission completed. The final
46
+ read-only jobs verify status consumption, revocations, unchanged rules, and
47
+ twice-confirmed REST/GraphQL Vercel Production identity for the exact source.
48
+
49
+ `textbutler-site-attempt` retains the explicitly listed public phase receipts
50
+ for 30 days when the runner can upload them. Hard termination may prevent this
51
+ upload; remote ref/status/provider readback and the existing interrupted
52
+ authority cleanup remain authoritative. Never blindly repeat an uncertain
53
+ write. If the ref already equals the site source, a fresh dispatch takes a
54
+ read-only qualification route with no App key or ref token. It requires the
55
+ status already consumed by the exact App bot (including its numeric actor
56
+ identity), the unchanged App-pinned rules, and the matching Production
57
+ deployment after the original successful CI completion. The retry's newly
58
+ built manifest is not treated as that existing deployment's publication time.
59
+ Leave `control_epoch_digest` empty for this already-exact route. An outstanding
60
+ success status must first pass the documented custody cleanup, not be silently
61
+ cleared by read-only qualification.
62
+
63
+ After provider success, verify `textbutler.app` serves the exact intended
64
+ deployment, truthful development status, canonical metadata and legacy links.
65
+ The current Vercel project/production branch remain the target; domain or
66
+ repository renaming is a separate inspected provider mutation.
67
+
68
+ ## Legacy package publication
69
+
3
70
  Message Like Me builds one exact public package tarball, validates those bytes
4
71
  on macOS and Linux, and publishes the same tarball plus `SHA256SUMS` to an
5
72
  immutable GitHub Release. Only then does it publish that tarball to npm through
@@ -0,0 +1,154 @@
1
+ # Textbutler architecture
2
+
3
+ Textbutler is a personal message butler for macOS. An owner activates a bounded set of contacts. Each contact gets a private workspace that a coding agent can read and evolve. A separate daemon decides when to invoke that agent and controls every outward action.
4
+
5
+ The source includes the owner daemon, contact reply loop, versioned Ghostget automation protocol and Mac application. Synthetic tests establish their control and recovery behavior. Live provider delivery and signed distribution have separate acceptance requirements below.
6
+
7
+ ## Ownership
8
+
9
+ ```mermaid
10
+ flowchart LR
11
+ App[Mac settings app] --> Control[Owner-only control socket]
12
+ Control --> Butler[Textbutler daemon]
13
+ Butler --> Router[Agentrouter]
14
+ Router --> Provider[Admitted agent provider]
15
+ Provider --> Tools[Contact-bound tool broker]
16
+ Tools --> Memory[One contact workspace]
17
+ Tools --> Web[Public web broker]
18
+ Tools --> Intents[Proposed actions]
19
+ Intents --> Butler
20
+ Butler --> Transport[Versioned transport adapter]
21
+ Transport --> Ghostget[Ghostget]
22
+ Ghostget --> Messages[iMessage and WhatsApp]
23
+ ```
24
+
25
+ Ghostget owns native permissions, message and contact acquisition, provider actions, and receipts. Textbutler does not open chat.db or automate Messages directly. Agentrouter owns provider selection, shared-account leases, cancellation, model catalogs, qualification evidence, and a bounded tool interface. Textbutler owns conversation policy and the durable send transaction. The Mac app changes owner settings through a small local protocol; it never gives a model arbitrary Tauri commands.
26
+
27
+ The background lifecycle uses a user LaunchAgent: native messaging belongs to the signed-in Mac user, and login persistence is independent of the settings window. Installation records the exact runtime, entrypoint, data directory and generation; removal verifies its private receipt and loaded service identity. Uninstall preserves contact data. New settings start paused. A private SQLite custody lock prevents duplicate daemon ownership and permits recovery only for a dead recorded process and its exact unserved socket. A temporary live launchd install/start/uninstall test passed while preserving synthetic owner data. Signed distribution remains a separate gate.
28
+
29
+ The daemon serves owner controls, enrollment jobs and a reply loop through its own supervised Ghostget process. Startup, enablement and recovery establish a silent event boundary, refresh bounded history and admit only subsequent eligible inbound messages. A failed catch-up pauses the affected conversation. No provider work starts without explicit owner account configuration.
30
+
31
+ ## Contact data
32
+
33
+ The intended application-support tree is:
34
+
35
+ ```text
36
+ Textbutler/
37
+ daemon.sock owner-only native control socket
38
+ state/
39
+ settings.json owner configuration and contact bindings
40
+ host.json optional private installed Ghostget CLI/account binding
41
+ runs.sqlite private run and send journal
42
+ daemon-custody.sqlite exclusive process/socket ownership
43
+ launch-agent-custody.sqlite lifecycle serialization
44
+ launch-agent.json exact LaunchAgent installation receipt
45
+ ghostget-automation-custody.json exact supervised messaging process claim
46
+ plugins/
47
+ extensions.json explicit owner-installed hook manifest
48
+ quiet-hours.ts example trusted executable extension
49
+ contacts/
50
+ <opaque-contact-id>/ the only model-visible workspace for one run
51
+ AGENTS.md editable response guidance; never authority
52
+ ABOUT.md relationship context and owner instructions
53
+ MEMORY.md concise, dated, source-attributed working notes
54
+ STYLE.md owner-style evidence and response preferences
55
+ history/ bounded, attributed conversation excerpts
56
+ notes/ task-specific notes and outstanding questions
57
+ attachments/ broker-admitted incoming files
58
+ outbox/ files proposed for this conversation
59
+ ```
60
+
61
+ Directory names use opaque identifiers, not contact names or phone numbers. Files are private to the Mac user. Models cannot traverse parent paths, links, other contact folders, or host configuration. The file broker supports reads, conditional writes, and exact edits. It exposes no symlink, directory, delete-tree, shell, or executable permission operations. Atomic replacement preserves the preceding file if a write fails. A stale memory revision produces a conflict instead of overwriting an owner's correction.
62
+
63
+ The owner chooses one verified direct conversation from a bounded Ghostget list. Enrollment rechecks the account incarnation and participant identity and creates a disabled contact. History import is a separate opt-in, limited to 200 recent, explicitly scoped messages, with message ID, time, and author preserved. The import records shortening and omissions; it does not fetch media. These records are context only, and historical automation may be unobservable. A later qualified initialization run may summarize preferences, conversational style, open tasks, and useful context into memory. It must distinguish evidence from inference and retain uncertainty. Later runs correct outdated notes and record sources. Proven butler output never becomes owner-style training evidence. No global person model or cross-contact retrieval is supplied by default.
64
+
65
+ The original Message Like Me corpus and profile tools remain an optional bounded bootstrap source. They do not become the live message transport. Old databases are not reset or silently migrated. There is no need to carry every previous archive/source feature into the new UI.
66
+
67
+ ## Reply admission
68
+
69
+ Default contact mode is smart, but a new contact starts disabled. Activating more than the configured limit fails atomically; no existing contact is displaced. The initial limit is five, with owner settings from one to fifty.
70
+
71
+ An inbound event must identify one activated direct conversation. Historical, outgoing, butler-authored, unknown-author, group, reaction-only, and delivery events do not start reply runs. Persisted event identity prevents a duplicate send. One run may own a contact at a time.
72
+
73
+ The daemon waits eight seconds after an incoming message to collect a burst. Newer messages supersede older candidates. Owner typing suppresses a reply when that signal exists. Any recent owner message causes a five-minute cooldown. The runtime checks current messages and settings again after composition. Disabling a contact or pressing global pause cancels pending runs and invalidates their grants.
74
+
75
+ A whole-word, case-insensitive `butler` invocation permits a response after those deterministic gates. In keyword mode, other messages stay silent. In smart mode, a tool-free cheap model returns a strict structured classification. It should answer useful assistance requests, and stay silent during ordinary conversation, acknowledgments, emotional exchanges, or uncertain intent. Confidence below 0.85 stays silent. Malformed results, exhausted accounts, stale model catalogs, or timeouts never escalate to a more expensive agent automatically.
76
+
77
+ Classifier choice comes from the selected provider's fresh available-model catalog, with explicit cost metadata and structured-output capability. No permanently hard-coded "cheap" model alias is assumed. An owner can pin a classifier or reply model after availability validation. Classifier and responder share the chosen provider/account policy; one classifier gets no tool authority.
78
+
79
+ No typing signal can prove the owner is absent. The current adapters expose no typing signal; the daemon retains its message-based cooldown and final revision checks. The selected provider/account must independently qualify before smart mode can invoke a model.
80
+
81
+ ## Send transaction and disclosure
82
+
83
+ The model proposes actions. It does not dispatch them. Owner-installed hooks may shape the work or veto a reply, but all action validation, contact binding, limits, and disclosure run afterwards.
84
+
85
+ Every text action is wrapped by trusted code with the contact's three symbols. Each field must be one visible grapheme; an empty or invisible disclosure is rejected. Default rendering is `🤖{ hello this is my response }`.
86
+
87
+ Reactions, stickers, link previews, and app cards cannot literally carry that text prefix. A disclosed text companion is therefore the first action in a nontext response, and counts toward the eight-action maximum. The provider must execute in order and stop if that companion fails. App-specific cards may additionally identify Textbutler in their content, but never remove the companion requirement.
88
+
89
+ Before send, the runtime rechecks owner activity, conversation revision, current settings, capability availability, cancellation, attachment ownership, target message membership, and the contact grant. It asks the transport to prepare an exact plan with an expiry and digest. Immediately before submission, it journals the dispatch intent. The transport must atomically validate the grant, contact route, context revision, and plan digest at its own effect boundary.
90
+
91
+ `submitted` is not `delivered`. Partial and indeterminate outcomes pause further automated activity for that contact until explicit reconciliation. A crash while dispatching becomes indeterminate on recovery; it never causes a blind retry. A crash before dispatch abandons the run without sending. Provider failover cannot replay a possibly submitted action.
92
+
93
+ ## Hooks and plugins
94
+
95
+ The initial lifecycle is `message.received`, `reply.decide`, `reply.compose`, `reply.before-send`, `reply.sent`, `memory.updated`, and `run.failed`. Hooks have a named/versioned owner-installed extension, deterministic registration order, a deadline, and a cancellation signal. Failure before dispatch closes admission. A notification-hook failure after a receipt cannot change that receipt or trigger resend.
96
+
97
+ Executable extensions are application code with the daemon's trust. They are installed outside contact folders; the agent cannot write them or turn message text into imports. Agent self-evolution means revising guidance and memory, not installing executable code. A future untrusted plugin mode needs its own process or language sandbox and explicit capabilities. In-process hooks are never described as a plugin security boundary.
98
+
99
+ The daemon reads a bounded private `plugins/extensions.json` manifest and preflights its complete inventory before importing listed TypeScript/JavaScript entry modules. Each default export must match the manifest ID/version and known hook names. Source digests appear in the loaded extension metadata. No directory scanning, package installation or hot reload occurs; changes require a full daemon process restart. The routed agent emits `memory.updated` only after a successful conditional write, with its path and committed revision. A notification failure does not undo that write or replay it.
100
+
101
+ ## Agentrouter
102
+
103
+ Agentrouter begins as an MIT-licensed source package independent of Textbutler's product model. Its account lease coordinates the selected provider account without embedding credentials in a contact workspace. Credential resolvers remain trusted host services. A lease cannot be stolen merely because its time elapsed while a process might still be alive.
104
+
105
+ Provider adapters declare observed, exact-version qualification. A launch plan is not proof of a sandbox. Codex's shell-disable setting and Claude's exact tool list are useful inputs, but an adapter is not admitted until attempted shell/process calls, host file reads, inherited MCP/plugin configuration, auth-file access, alternate agents, and additional workspaces are demonstrably blocked. No bypass-permissions mode is acceptable.
106
+
107
+ The source implements a pinned Claude Agent SDK adapter with explicit API-key account binding, a private verified executable snapshot, isolated runtime directories, no built-in tools or inherited settings, broker-only MCP, bounded raw output, and joined process-group termination. Its production gate requires independent qualification of the exact executable and SDK identity. Synthetic protocol and native-runtime fixtures do not activate it. The experimental Codex app-server driver verifies each model request's tool inventory through a host relay and keeps contact storage outside native scratch. It has no live account transport or production registration; native confinement and adversarial custody qualification remain incomplete. See the [Agentrouter implementation and evidence](../../packages/agentrouter/README.md).
108
+
109
+ A separately selected Claude API adapter executes the bounded tool loop in the trusted host. It exposes only the six broker tools and never starts a model-selected process. Account checks bind the actual packaged runtime and private credential generation; replacing a credential invalidates old discovery and running account work. Fresh model availability and price metadata select a cheap classifier. This explicit API choice does not silently replace Claude Code or Codex and does not use their subscription authentication.
110
+
111
+ The model receives a fixed contact/workspace identity and a fixed run ID. File operations are brokered and conditional. Public web requests are separately bounded and must not reach loopback, private networks, local sockets, or cloud metadata through DNS or redirects. Message tools stage recipient-free intents for the one conversation. Unknown tool names and unknown input fields fail. Credential/account services are never model tools.
112
+
113
+ Oompa's existing runtime provider port and account/process-custody patterns are source references. Its ordinary workspace-write execution profile is not the requested contact-only sandbox. AI Charts is a prospective second consumer. Do not migrate either product to Agentrouter until an adapter has equivalent feature and recovery evidence; avoid changing their active work in this redesign.
114
+
115
+ ## Ghostget contract and rich features
116
+
117
+ WhatsApp follows the same Ghostget ownership boundary. Its private wacli-backed transport provides durable observations and recipient-bound actions; pairing, session state and synchronization stay in Ghostget. The [WhatsApp guide](whatsapp.md) describes setup and action support. Textbutler does not embed WPPConnect or invoke wacli directly.
118
+
119
+ Current source inspection found Ghostget's Tauri 2 webview with a packaged Bun helper. That helper lives with the app, and its private socket handles approvals/control, not a public messaging subscription service. Textbutler must not couple to it.
120
+
121
+ The older generic messaging APIs retain expiring route references and owner-confirmed previews. Automation uses a separate explicit owner protocol with durable enrollment, revocable grants and event observations. The iMessage provider negotiates attachments, reactions, stickers, rich links and polls separately; unsupported App Clips and arbitrary experiences remain unavailable. Native Contacts directory discovery is not implemented in this protocol.
122
+
123
+ Text and file sending use the native helper's AppleScript path. The upstream rich-action bridge injects into Messages and requires SIP to be disabled. Textbutler and Ghostget never change that security setting or install the injection automatically. Rich actions therefore remain unavailable unless the required bridge is already working. This is a significant installation constraint, not a completed rich-messaging experience on a stock Mac.
124
+
125
+ Ghostget 0.18.2 includes the admitted imsg `0.14.1+private-transport.3` helper. Automated rich links use `send.rich` with `fetch_metadata: false`, constructing the URL and host-title card without helper metadata or image fetching. Availability still requires current managed permission and a compatible native bridge; this does not claim network isolation for Messages itself. The host never falls back to fetching an agent-provided URL.
126
+
127
+ The [Ghostget integration contract](ghostget-contract.md) documents the implemented binding, grant, event, action and receipt semantics.
128
+
129
+ The Textbutler transport supports capability negotiation, conversations, bounded history, cursor-based events, exact preparation, authorization and receipts. File paths become admitted bytes before crossing the boundary. Per-action permission and context checks stop a rich batch when conversation activity changes. Private database access stays inside Ghostget's provider implementation.
130
+
131
+ Linq's documented iMessage API includes attachments, reactions, stickers, rich links, App Clips, and experiences. App cards and rich links are standalone messages. Those hosted capabilities do not establish availability through native macOS Messages. Optional Linq would be a separate transport with explicit account setup, sender identity, webhook signature verification, replay protection, and the same Textbutler policy gates. It is not a way to silently route an owner's personal conversation through a different phone number.
132
+
133
+ Ghostget currently imports published Message Like Me bundle contracts. Keep that immutable package a leaf. Do not repoint it at the Textbutler runtime. Extract the neutral bundle contracts before reversing a live package dependency, or consume Ghostget's installed CLI contract without a package import in the interim. Preserve historical wire-format identifiers.
134
+
135
+ ## Mac application
136
+
137
+ The app has an explicit conversation picker, optional history initialization, a contact list, per-contact activation and mode settings, disclosure preview, memory editing, activity, provider status, and global pause. Long provider reads use bounded asynchronous jobs; Pause stays available and preserves unsaved choices. Unsupported capabilities show their actual setup or transport limitation. A separate synthetic demo is clearly labeled and is not included in the native app's live data graph.
138
+
139
+ One narrow native command accepts the versioned control request. It connects to the private user socket, bounds requests/responses, applies timeouts, and verifies same-user ownership. The webview has no generic shell, filesystem, opener, or network plugin. The app does not inherit access to arbitrary Ghostget operations.
140
+
141
+ ## Admission still required
142
+
143
+ 1. Use the verified Ghostget 0.18.2 package, which includes the reviewed automation source and native helpers. Real account synchronization, recipient identity, rich actions and revocation still require a bounded owner-authorized live test; artifact admission and synthetic fixtures do not prove delivery.
144
+ 2. Independently qualify the native Claude SDK and Codex adapters for the requested no-shell, contact-only profile before enabling those choices. The separate Claude API path requires explicit account setup and packaged-runtime admission.
145
+ 3. Sign and notarize the exact Mac artifact with an available Apple Developer identity, then verify the final downloaded bytes and installation lifecycle. The unsigned local package and successful launchd test are not a signed release.
146
+ 4. Keep historical repository and published package identities as compatibility and provenance anchors. The Textbutler site is assigned to `textbutler.app`; later identity migrations must preserve immutable artifacts and existing release protections.
147
+
148
+ ## Sources
149
+
150
+ - [Codex App Server](https://learn.chatgpt.com/docs/app-server): programmatic threads, turns, tool requests, account operations, and sandbox configuration.
151
+ - [Codex security](https://learn.chatgpt.com/docs/security): sandbox and approval boundaries.
152
+ - [Claude Agent SDK permissions](https://platform.claude.com/docs/en/agent-sdk/permissions): tool permission controls.
153
+ - [Linq messages](https://docs.linqapp.com/channel/imessage/api/resources/chats/subresources/messages/): transport-specific rich message behavior.
154
+ - [Linq reactions](https://docs.linqapp.com/channel/imessage/api/resources/messages/methods/add_reaction/): emoji and sticker reactions.
@@ -0,0 +1,88 @@
1
+ # Ghostget integration contract
2
+
3
+ Textbutler owns reply policy and contact memory. Ghostget owns messaging
4
+ accounts, native permissions, synchronization, event storage and outward
5
+ actions. Textbutler communicates with its own Ghostget owner process through
6
+ `ghostget messaging automation serve --stdio`; it does not share the Ghostget
7
+ Mac app's private helper or open provider databases.
8
+
9
+ [Ghostget 0.18.2](https://github.com/hraness/ghostget/releases/tag/v0.18.2) is the
10
+ verified published dependency for this contract. Its package includes both
11
+ private messaging runtimes; installation is explicit and starts no provider.
12
+ Older generic CLI routes do not become automation grants.
13
+
14
+ ## Owner process
15
+
16
+ The private protocol is `ghostget.messaging-automation/1`. Every bounded JSON
17
+ request has an ID, method and parameters; every response repeats its protocol
18
+ and ID and carries either a checked result or a typed error. Initialization
19
+ selects up to two explicit accounts, one per network, through stdin. Contact
20
+ memory and model tools cannot configure that process or invoke its control API.
21
+
22
+ Textbutler records process custody before launch, bounds its streams and queue,
23
+ and removes custody only after a successful close response and verified clean
24
+ process exit. Cancellation and revocation can interrupt an active submission.
25
+ A malformed response, crash or uncertain cleanup retains the recovery fence.
26
+ Restarting Textbutler does not silently clear it.
27
+
28
+ ## Contacts, events and grants
29
+
30
+ | Surface | Implemented contract |
31
+ | --- | --- |
32
+ | `status`, `start` | Observe capabilities; explicitly start supported synchronization. |
33
+ | `conversations`, `enroll`, `enrollments` | Exact account generation and direct participant-bound conversation enrollment. |
34
+ | `poll`, `history`, `events` | Bounded history, durable observation cursors, revisions, catch-up and gap detection. |
35
+ | `grant`, `grant.get`, `grant.by-intent`, `revoke` | Recipient, action, expiry and quota limits; idempotent issuance lookup and immediate revocation. |
36
+ | `asset`, `prepare` | Admit exact attachment bytes and bind the ordered action list to a context revision and expiry. |
37
+ | `submit`, `cancel`, `run` | Journal an action claim before dispatch and retain accepted, failed, partial or indeterminate results. |
38
+ | `close` | Wait for owned provider cleanup before acknowledging shutdown. |
39
+
40
+ Enrollment records contain the provider account incarnation, source generation,
41
+ implementation identity, exact conversation coordinate and participants. Display
42
+ titles are labels, not authority. Account or participant replacement invalidates
43
+ the binding. Existing Textbutler version 1 read bindings remain readable;
44
+ automation requires explicit version 2 enrollment.
45
+
46
+ Ghostget's managed permissions must explicitly allow each requested operation.
47
+ A broad capability advertisement does not create a grant. Textbutler activation
48
+ issues a bounded grant only after checking the selected agent account and
49
+ conversation. It renews standing enabled-contact grants within their limits,
50
+ and persists grant intent before issuance so a lost response can be resolved
51
+ without blindly creating another grant. Disabled and uncommitted grants are
52
+ reconciled through revocation.
53
+
54
+ Startup, re-enablement and recovery drain old events silently. An unresolved gap
55
+ pauses automation. Textbutler waits for a new eligible inbound event and applies
56
+ debounce, owner cooldown, classification and rate limits. Ghostget rechecks
57
+ identity, permission, context and grant before dispatch, and observes intervening
58
+ conversation changes between actions. Neither component retries an uncertain
59
+ send automatically.
60
+
61
+ ## Rich actions
62
+
63
+ Attachments, reactions, stickers, rich links and native polls are separate
64
+ capabilities. They become available only when the installed provider, current
65
+ account and managed permission admit them. Message targets must belong to the
66
+ enrolled conversation. Attachment and sticker paths are resolved by Textbutler's
67
+ contact file broker; Ghostget receives admitted bytes, not arbitrary paths.
68
+
69
+ Every response starts with disclosed text. For a nontext response, Textbutler
70
+ inserts a companion such as `🤖{ … }` before the rich actions. Execution stops
71
+ when a preceding action fails or the conversation changes. An accepted receipt
72
+ does not claim delivery.
73
+
74
+ Native iMessage exposes text and files through the pinned helper and its
75
+ currently usable rich methods for six standard tapbacks, stickers, links and
76
+ polls. Rich methods require the owner's separately configured native bridge.
77
+ App Clips and arbitrary mini-app experiences remain unavailable: no reviewed
78
+ native executor exists. Linq's hosted APIs are a design reference and cannot
79
+ silently substitute another sender for the owner's personal conversation.
80
+
81
+ ## Verification boundary
82
+
83
+ The source tests cover account changes, cursor restart and gaps, grant revocation
84
+ and lost responses, exact byte admission, ordered actions, cancellation and
85
+ uncertain results using synthetic accounts. They do not establish live provider
86
+ delivery. A bounded live acceptance test needs explicit account, recipient and
87
+ message authorization and must record the exact provider/runtime versions.
88
+ See [WhatsApp](whatsapp.md) and [the architecture](architecture.md).
@@ -0,0 +1,74 @@
1
+ # WhatsApp through Ghostget
2
+
3
+ WhatsApp uses the same Textbutler contact settings, memory, disclosure, hooks
4
+ and reply policy as iMessage. Ghostget owns the linked device, pairing,
5
+ credentials, synchronization and send implementation. Textbutler never runs
6
+ `wacli` directly or imports a WhatsApp session database.
7
+
8
+ ```mermaid
9
+ flowchart LR
10
+ App[Textbutler Mac app] --> Butler[Textbutler daemon]
11
+ Butler --> Agents[Agentrouter]
12
+ Butler --> Ghostget[Ghostget owner process]
13
+ Ghostget --> Messages[iMessage helper]
14
+ Ghostget --> WhatsApp[Pinned wacli linked device]
15
+ ```
16
+
17
+ ## Setup and behavior
18
+
19
+ Configure the WhatsApp account and its managed automation permissions in
20
+ Ghostget, install its verified private messaging helper, then select that
21
+ account in Textbutler's private `state/host.json`. The Mac app's messaging setup
22
+ explicitly starts synchronization. Choose one returned direct conversation,
23
+ optionally import recent history, and enable it only after the selected agent
24
+ account passes its checks. New contacts and new installations start inactive.
25
+ See [runtime setup](../../packages/textbutler/README.md).
26
+
27
+ Version 2 enrollment preserves the canonical conversation JID, exact account
28
+ incarnation, source generation and participant identity. Phone-number and
29
+ linked-identity JIDs are never equated from similar digits. Self chats,
30
+ broadcasts, newsletters and unsupported groups cannot be enrolled.
31
+
32
+ The Ghostget provider uses a reviewed private transport patch on
33
+ [wacli](https://github.com/openclaw/wacli) 0.15.0. A single owned synchronization
34
+ process maintains a bounded SQLite event journal and accepts generation-bound
35
+ private requests. Each outward request gets a durable claim and one application
36
+ dispatch attempt. It does not reuse stock send retry behavior after a timeout.
37
+ The exact binary, patch and resource hashes are recorded in Ghostget's package.
38
+
39
+ Events preserve message identity, authored time, edits, deletion and reactions.
40
+ Cursor anchors detect retention gaps and replaced stores. Catch-up and old
41
+ history never trigger replies. A new owner message cancels pending composition
42
+ and starts the contact's cooldown. Uncertain send or process cleanup blocks
43
+ further automatic activity until reconciled.
44
+
45
+ ## Actions
46
+
47
+ | Action | Implementation |
48
+ | --- | --- |
49
+ | Text and files | Recipient-bound sends with admitted bytes and durable result claims. |
50
+ | Reactions | Add/remove a supported reaction to a message in the selected conversation. |
51
+ | Stickers | Bounded admitted media through the private provider action. |
52
+ | Links and polls | Native provider operations when observed and explicitly allowed. |
53
+ | App Clips and mini-app experiences | Unavailable; no corresponding reviewed WhatsApp executor. |
54
+
55
+ Capabilities are observed per account and intersected with its managed
56
+ permissions. An unavailable capability is never converted to another action.
57
+ Textbutler adds its configured disclosure before all rich responses.
58
+
59
+ [WPPConnect](https://github.com/wppconnect-team/wppconnect) remains an alternative
60
+ Ghostget provider implementation if a specific missing capability warrants it.
61
+ It is not a second linked-device stack inside Textbutler. Replacing the provider
62
+ must preserve identity and pending-action reconciliation or require explicit
63
+ re-enrollment.
64
+
65
+ ## Evidence
66
+
67
+ Synthetic adapter, SQLite journal, private transport, and process tests exercise
68
+ the implemented boundary without pairing a real account or messaging anyone.
69
+ The native patch has plain and FTS Go tests, vet checks and repeat-build evidence.
70
+ These checks establish source behavior, not live WhatsApp delivery. Real pairing,
71
+ reconnect and rich-action acceptance still require a bounded owner-authorized
72
+ test. The older `createGhostgetWhatsAppTransport()` stays a read-only compatibility
73
+ adapter; automation uses `createGhostgetAutomationTransport()` and the
74
+ [versioned owner protocol](ghostget-contract.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hraness/message-like-me",
3
- "version": "0.8.8",
3
+ "version": "0.8.10",
4
4
  "description": "A local-first CLI and Agent Skill for studying private messaging history and drafting messages that sound like you.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -77,16 +77,23 @@
77
77
  "check:standalone": "bun scripts/check-standalone.ts",
78
78
  "check:dist": "bun scripts/check-dist.ts",
79
79
  "check:package": "bun scripts/package-smoke.ts",
80
- "check": "bun run typecheck && bun run check:effect && bun run test && bun run check:skill && bun run check:standalone && bun run build && bun run check:public-graphs && bun run check:dist && bun run check:package",
80
+ "check": "bun run typecheck && bun run check:effect && bun run test && bun run check:skill && bun run check:standalone && bun run build && bun run check:public-graphs && bun run check:dist && bun run check:package && bun run check:textbutler",
81
+ "check:textbutler": "bun test packages && tsc --noEmit -p packages/agentrouter/tsconfig.json && tsc --noEmit -p packages/transport/tsconfig.json && tsc --noEmit -p packages/textbutler/tsconfig.json && bun run --cwd apps/macos check",
82
+ "textbutler": "bun packages/textbutler/src/cli.ts",
81
83
  "prepack": "bun run check",
82
84
  "check:effect": "bun scripts/check-effect-architecture.ts",
83
85
  "check:public-graphs": "bun scripts/check-public-graphs.ts"
84
86
  },
85
87
  "devDependencies": {
88
+ "@tauri-apps/cli": "2.11.4",
89
+ "@anthropic-ai/claude-agent-sdk": "0.3.268",
90
+ "@anthropic-ai/sdk": "0.125.0",
91
+ "@modelcontextprotocol/sdk": "1.30.0",
86
92
  "@types/bun": "1.3.14",
87
93
  "effect": "3.22.1",
88
94
  "fast-check": "4.9.0",
89
95
  "sigstore": "4.1.1",
90
- "typescript": "6.0.3"
96
+ "typescript": "6.0.3",
97
+ "zod": "4.6.2"
91
98
  }
92
99
  }