@hraness/message-like-me 0.8.9 → 0.8.11

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.11 (2026-09-16)
4
+
5
+ - Bind legacy installation to the exact public `@hraness/message-like-me@0.8.11` npm package, with the same reviewed bytes mirrored in the immutable GitHub Release. This does not install the Textbutler menu companion or enable automatic replies.
6
+ - Add optional development support to CLI discovery, the Message Like Me skill, the Textbutler menu, and the public website. Task output and private message boundaries stay unchanged.
7
+ - Bundle the shared local support protocol separately from all public SDK graphs. Paid support remains a human choice; no newsletter is offered for this product.
8
+
9
+ ## 0.8.10 (2026-09-13)
10
+
11
+ - Show a compact ASCII robot before interactive root help. Piped output,
12
+ machine-readable commands, version output, and command help stay unchanged.
13
+ - Refine the informational site's shared surfaces and robot identity. The native
14
+ Textbutler app remains in development; this package does not enable replies.
15
+ - 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.
16
+
3
17
  ## 0.8.9 (2026-09-10)
4
18
 
5
19
  - 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.
package/README.md CHANGED
@@ -1,7 +1,75 @@
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 macOS daemon lifecycle,
14
+ owner-selected Ghostget conversation enrollment, optional history
15
+ initialization, private memory, executable hooks, reply policy and send journal.
16
+ The supported local surface is the `textbutler` CLI plus its native menu-bar
17
+ companion. It runs from a prebuilt binary and does not require an app bundle,
18
+ signing, notarization, or a download manager. Agentrouter includes a
19
+ restricted Claude Agent SDK adapter and shared account custody. Live automated
20
+ replies remain unavailable until Ghostget's durable events and scoped
21
+ automation grants, and the provider's contact-only execution, are qualified.
22
+ Codex execution and native rich actions are also unavailable. The website is
23
+ informational; it has no connection to private messages or contact folders.
24
+
25
+ The menu-bar companion uses the shared, unsigned desktop-foundation runner. The
26
+ `textbutler menubar` commands download and verify a pinned release binary on
27
+ first start; nothing compiles source or opens a windowed application.
28
+
29
+ Start with the [architecture and capability status](docs/textbutler/architecture.md),
30
+ [Textbutler runtime](packages/textbutler/README.md),
31
+ [Agentrouter](packages/agentrouter/README.md),
32
+ [transport adapter](packages/transport/README.md), or the
33
+ [menu companion adapter](packages/textbutler/src/menubar.ts).
34
+
35
+ ```sh
36
+ bun install --frozen-lockfile --ignore-scripts
37
+ bun run check:textbutler
38
+ ```
39
+
40
+ The source is MIT licensed. New packages remain unpublished while their
41
+ contracts are developed. The menu-bar CLI and daemon are the only supported
42
+ Textbutler runtime surfaces. Message Like Me history readers and message-bundle
43
+ contracts are retained below only as compatibility documentation for existing
44
+ consumers; they are not Textbutler runtime components.
45
+
46
+ ## Run the Textbutler menu companion
47
+
48
+ Start the companion in the background; it stays running until `menubar stop` or
49
+ its menu-bar **Quit** action:
50
+
51
+ ```sh
52
+ bun run textbutler menubar
53
+ bun run textbutler menubar status
54
+ bun run textbutler menubar stop
55
+ ```
56
+
57
+ One companion runs per user and data directory. To start it automatically when
58
+ you sign in, register login startup (a per-user LaunchAgent on macOS):
59
+
60
+ ```sh
61
+ bun run textbutler menubar install
62
+ bun run textbutler menubar uninstall
63
+ ```
64
+
65
+ `menubar doctor` reports the runner artifact, platform capability, and any
66
+ operating-system approval step needed on an unsigned binary. The companion
67
+ shows daemon state, contact and account readiness, capabilities, and recent
68
+ activity. It controls global pause and opens the informational Textbutler
69
+ website. It does not provide contact enrollment or account setup.
70
+
71
+ ## Legacy Message Like Me history tools
72
+
5
73
  **A local-first CLI and Agent Skill for studying private messaging history and
6
74
  drafting messages that sound like you.**
7
75
 
@@ -43,7 +111,7 @@ Message Like Me requires Bun 1.3.14 or newer. Install the exact public npm
43
111
  package, then install both bundled Agent Skills:
44
112
 
45
113
  ```sh
46
- bun add --global @hraness/message-like-me@0.8.9
114
+ bun add --global @hraness/message-like-me@0.8.11
47
115
  messagelikeme skill install
48
116
  ```
49
117
 
@@ -797,3 +865,30 @@ Ghostget is the current name of Wrench. The original `--wrench-context` and
797
865
  `ghostgetContext` and retains the original `wrenchContext` input alias.
798
866
  Existing bundle, handoff, receipt, and database formats retain their versioned
799
867
  `wrench` identifiers so saved evidence remains verifiable.
868
+
869
+ ## Optional support
870
+
871
+ `messagelikeme support` shows optional ways to support Textbutler.
872
+ The person reviews current options and confirms any payment in their browser.
873
+ These options do not restrict the tool's features.
874
+
875
+ Agents can inspect `messagelikeme support protocol --json` without claiming an
876
+ invitation. After completed useful CLI work, stderr may contain a compact
877
+ discovery notice; stdout keeps the requested data format. The default, including
878
+ a pseudo-terminal, is agent-oriented discovery. `HRANESS_SUPPORT_AUDIENCE=human`
879
+ selects a direct invitation in an interactive terminal; `agent` selects discovery
880
+ and `off` suppresses incidental notices and due offers. CI and child tools stay
881
+ quiet. Programmatic entrypoints do not emit incidental notices.
882
+
883
+ At a human-facing task closeout, use `support offer --json` once and follow the
884
+ returned protocol. A discovery notice does not count as a shown invitation.
885
+ Acknowledge `shown <id>` only after persistent human-facing output. If the host
886
+ can only retain the final answer and cannot call tools afterward, include the
887
+ invitation there without acknowledging it. That offer expires after ten minutes;
888
+ the weekly cooldown begins only after acknowledged output.
889
+
890
+ `messagelikeme support dismiss` opts out across participating Hraness tools on this
891
+ machine; `snooze` pauses invitations for thirty days and `enable` restores them.
892
+ Support state stays local; these commands do not sign up or charge anyone.
893
+ The new source-only `textbutler support` exposes the same protocol. Its daemon
894
+ and administrative commands do not emit incidental invitations.
package/dist/cli.js CHANGED
@@ -14,6 +14,162 @@ var __export = (target, all) => {
14
14
  set: __exportSetter.bind(all, name)
15
15
  });
16
16
  };
17
+
18
+ // src/errors.ts
19
+ var EXIT_CODES = {
20
+ usage: 2,
21
+ "not-found": 3,
22
+ conflict: 4,
23
+ permission: 5,
24
+ "unsafe-path": 6,
25
+ "invalid-data": 7,
26
+ internal: 1
27
+ };
28
+
29
+ class CliError extends Error {
30
+ exitCode;
31
+ kind;
32
+ constructor(kind, message, options) {
33
+ super(message, options);
34
+ this.name = "CliError";
35
+ this.kind = kind;
36
+ this.exitCode = EXIT_CODES[kind];
37
+ }
38
+ }
39
+ function errorMessage(error) {
40
+ return error instanceof Error ? error.message : String(error);
41
+ }
42
+ function exitCodeFor(error) {
43
+ return error instanceof CliError ? error.exitCode : 1;
44
+ }
45
+
46
+ // src/args.ts
47
+ var VALUE_OPTIONS = new Set([
48
+ "addressbook",
49
+ "after",
50
+ "before",
51
+ "burst-gap",
52
+ "data-dir",
53
+ "database",
54
+ "input",
55
+ "limit",
56
+ "min-outgoing",
57
+ "output",
58
+ "overlap-source",
59
+ "prompt-output",
60
+ "project",
61
+ "reference-output",
62
+ "request",
63
+ "scope",
64
+ "session-gap",
65
+ "subject",
66
+ "target",
67
+ "draft",
68
+ "ghostget-context",
69
+ "ghostget-receipt"
70
+ ]);
71
+ var FLAG_OPTIONS = new Set(["force", "help", "json", "private", "version"]);
72
+ var LEGACY_OPTION_ALIASES = new Map([
73
+ ["wrench-context", "ghostget-context"],
74
+ ["wrench-receipt", "ghostget-receipt"]
75
+ ]);
76
+ function parseArguments(argv) {
77
+ const positionals = [];
78
+ const options = new Map;
79
+ const flags = new Set;
80
+ let positionalOnly = false;
81
+ for (let index = 0;index < argv.length; index += 1) {
82
+ const argument = argv[index];
83
+ if (argument === undefined)
84
+ continue;
85
+ if (positionalOnly || !argument.startsWith("--")) {
86
+ positionals.push(argument);
87
+ continue;
88
+ }
89
+ if (argument === "--") {
90
+ positionalOnly = true;
91
+ continue;
92
+ }
93
+ const separator = argument.indexOf("=");
94
+ const suppliedKey = argument.slice(2, separator < 0 ? undefined : separator);
95
+ const key = LEGACY_OPTION_ALIASES.get(suppliedKey) ?? suppliedKey;
96
+ if (key.length === 0)
97
+ throw new CliError("usage", "Empty option name");
98
+ if (VALUE_OPTIONS.has(key)) {
99
+ if (options.has(key))
100
+ throw new CliError("usage", `--${key} may be provided only once`);
101
+ const inline = separator < 0 ? undefined : argument.slice(separator + 1);
102
+ const next = inline ?? argv[index + 1];
103
+ if (next === undefined || next.startsWith("--") || next.length === 0) {
104
+ throw new CliError("usage", `--${key} requires a value`);
105
+ }
106
+ options.set(key, next);
107
+ if (inline === undefined)
108
+ index += 1;
109
+ continue;
110
+ }
111
+ if (FLAG_OPTIONS.has(key) && separator < 0) {
112
+ if (flags.has(key))
113
+ throw new CliError("usage", `--${key} may be provided only once`);
114
+ flags.add(key);
115
+ continue;
116
+ }
117
+ throw new CliError("usage", `Unknown option --${key}`);
118
+ }
119
+ return { positionals, options, flags };
120
+ }
121
+ function integerOption(parsed, key, fallback, minimum, maximum) {
122
+ const value = parsed.options.get(key);
123
+ if (value === undefined)
124
+ return fallback;
125
+ if (!/^(?:0|[1-9][0-9]*)$/u.test(value)) {
126
+ throw new CliError("usage", `--${key} must be an integer`);
127
+ }
128
+ const result = Number(value);
129
+ if (!Number.isSafeInteger(result) || result < minimum || result > maximum) {
130
+ throw new CliError("usage", `--${key} must be between ${minimum} and ${maximum}`);
131
+ }
132
+ return result;
133
+ }
134
+ function rejectUnused(parsed, allowedOptions, allowedFlags) {
135
+ const options = new Set(allowedOptions);
136
+ const flags = new Set(allowedFlags);
137
+ for (const key of parsed.options.keys()) {
138
+ if (!options.has(key))
139
+ throw new CliError("usage", `--${key} is not valid for this command`);
140
+ }
141
+ for (const key of parsed.flags) {
142
+ if (!flags.has(key))
143
+ throw new CliError("usage", `--${key} is not valid for this command`);
144
+ }
145
+ }
146
+
147
+ // src/support.ts
148
+ import { maybeShowSupportInvitation, runSupportCommand } from "./support-runtime.js";
149
+ var supportProfile = {
150
+ id: "message-like-me",
151
+ name: "Textbutler",
152
+ valueProposition: "Support ongoing development of local tools for your messaging workflows.",
153
+ updates: false
154
+ };
155
+ function standaloneSupportEnvironment() {
156
+ const env = { ...process.env };
157
+ process.env.HRANESS_SUPPORT_AUDIENCE = "off";
158
+ return env;
159
+ }
160
+ async function runProductSupportCommand(args, output, options = {}) {
161
+ const result = await runSupportCommand(supportProfile, args, { command: ["messagelikeme"], gitEmail: false, ...options });
162
+ if (result.stdout !== "")
163
+ output.stdout(result.stdout);
164
+ if (result.stderr !== "")
165
+ output.stderr(result.stderr);
166
+ return result.exitCode;
167
+ }
168
+ async function showProductSupportInvitation(options = {}) {
169
+ try {
170
+ await maybeShowSupportInvitation(supportProfile, { usefulResult: true, command: ["messagelikeme"], gitEmail: false, ...options });
171
+ } catch {}
172
+ }
17
173
  // node_modules/effect/dist/esm/Cause.js
18
174
  var exports_Cause = {};
19
175
  __export(exports_Cause, {
@@ -15290,34 +15446,6 @@ function agentMessageRouteCandidateId(sourceId, conversationId) {
15290
15446
  }))}`;
15291
15447
  }
15292
15448
 
15293
- // src/errors.ts
15294
- var EXIT_CODES = {
15295
- usage: 2,
15296
- "not-found": 3,
15297
- conflict: 4,
15298
- permission: 5,
15299
- "unsafe-path": 6,
15300
- "invalid-data": 7,
15301
- internal: 1
15302
- };
15303
-
15304
- class CliError extends Error {
15305
- exitCode;
15306
- kind;
15307
- constructor(kind, message, options) {
15308
- super(message, options);
15309
- this.name = "CliError";
15310
- this.kind = kind;
15311
- this.exitCode = EXIT_CODES[kind];
15312
- }
15313
- }
15314
- function errorMessage(error) {
15315
- return error instanceof Error ? error.message : String(error);
15316
- }
15317
- function exitCodeFor(error) {
15318
- return error instanceof CliError ? error.exitCode : 1;
15319
- }
15320
-
15321
15449
  // src/command-failure.ts
15322
15450
  function commandFailure(cause3) {
15323
15451
  return cause3 instanceof CliError ? { _tag: "CommandFailure", cause: cause3 } : { _tag: "ForeignFailure", cause: cause3 };
@@ -24645,117 +24773,18 @@ function commandPlatformLive(io, cleanupFailure) {
24645
24773
  });
24646
24774
  }
24647
24775
 
24648
- // src/args.ts
24649
- var VALUE_OPTIONS = new Set([
24650
- "addressbook",
24651
- "after",
24652
- "before",
24653
- "burst-gap",
24654
- "data-dir",
24655
- "database",
24656
- "input",
24657
- "limit",
24658
- "min-outgoing",
24659
- "output",
24660
- "overlap-source",
24661
- "prompt-output",
24662
- "project",
24663
- "reference-output",
24664
- "request",
24665
- "scope",
24666
- "session-gap",
24667
- "subject",
24668
- "target",
24669
- "draft",
24670
- "ghostget-context",
24671
- "ghostget-receipt"
24672
- ]);
24673
- var FLAG_OPTIONS = new Set(["force", "help", "json", "private", "version"]);
24674
- var LEGACY_OPTION_ALIASES = new Map([
24675
- ["wrench-context", "ghostget-context"],
24676
- ["wrench-receipt", "ghostget-receipt"]
24677
- ]);
24678
- function parseArguments(argv) {
24679
- const positionals = [];
24680
- const options = new Map;
24681
- const flags = new Set;
24682
- let positionalOnly = false;
24683
- for (let index = 0;index < argv.length; index += 1) {
24684
- const argument = argv[index];
24685
- if (argument === undefined)
24686
- continue;
24687
- if (positionalOnly || !argument.startsWith("--")) {
24688
- positionals.push(argument);
24689
- continue;
24690
- }
24691
- if (argument === "--") {
24692
- positionalOnly = true;
24693
- continue;
24694
- }
24695
- const separator = argument.indexOf("=");
24696
- const suppliedKey = argument.slice(2, separator < 0 ? undefined : separator);
24697
- const key = LEGACY_OPTION_ALIASES.get(suppliedKey) ?? suppliedKey;
24698
- if (key.length === 0)
24699
- throw new CliError("usage", "Empty option name");
24700
- if (VALUE_OPTIONS.has(key)) {
24701
- if (options.has(key))
24702
- throw new CliError("usage", `--${key} may be provided only once`);
24703
- const inline = separator < 0 ? undefined : argument.slice(separator + 1);
24704
- const next = inline ?? argv[index + 1];
24705
- if (next === undefined || next.startsWith("--") || next.length === 0) {
24706
- throw new CliError("usage", `--${key} requires a value`);
24707
- }
24708
- options.set(key, next);
24709
- if (inline === undefined)
24710
- index += 1;
24711
- continue;
24712
- }
24713
- if (FLAG_OPTIONS.has(key) && separator < 0) {
24714
- if (flags.has(key))
24715
- throw new CliError("usage", `--${key} may be provided only once`);
24716
- flags.add(key);
24717
- continue;
24718
- }
24719
- throw new CliError("usage", `Unknown option --${key}`);
24720
- }
24721
- return { positionals, options, flags };
24722
- }
24723
- function integerOption(parsed, key, fallback, minimum, maximum) {
24724
- const value = parsed.options.get(key);
24725
- if (value === undefined)
24726
- return fallback;
24727
- if (!/^(?:0|[1-9][0-9]*)$/u.test(value)) {
24728
- throw new CliError("usage", `--${key} must be an integer`);
24729
- }
24730
- const result = Number(value);
24731
- if (!Number.isSafeInteger(result) || result < minimum || result > maximum) {
24732
- throw new CliError("usage", `--${key} must be between ${minimum} and ${maximum}`);
24733
- }
24734
- return result;
24735
- }
24736
- function rejectUnused(parsed, allowedOptions, allowedFlags) {
24737
- const options = new Set(allowedOptions);
24738
- const flags = new Set(allowedFlags);
24739
- for (const key of parsed.options.keys()) {
24740
- if (!options.has(key))
24741
- throw new CliError("usage", `--${key} is not valid for this command`);
24742
- }
24743
- for (const key of parsed.flags) {
24744
- if (!flags.has(key))
24745
- throw new CliError("usage", `--${key} is not valid for this command`);
24746
- }
24747
- }
24748
-
24749
24776
  // src/command-input.ts
24750
24777
  import { isAbsolute as isAbsolute6, resolve as resolve8 } from "path";
24751
24778
 
24752
24779
  // src/version.ts
24753
- var MESSAGE_LIKE_ME_VERSION = "0.8.9";
24780
+ var MESSAGE_LIKE_ME_VERSION = "0.8.11";
24754
24781
 
24755
24782
  // src/command-input.ts
24756
24783
  var HELP = `Message Like Me ${MESSAGE_LIKE_ME_VERSION}
24757
24784
 
24758
24785
  Usage:
24786
+ messagelikeme support [protocol --json|offer --json|shown ID|release ID|dismiss|snooze|enable|status --json]
24787
+ Optional support; agents use support protocol --json at closeout.
24759
24788
  messagelikeme [--data-dir PATH] init [--json]
24760
24789
  messagelikeme [--data-dir PATH] ingest imessage [--database PATH] [--json]
24761
24790
  messagelikeme [--data-dir PATH] ingest bundle --input ABS_PATH
@@ -27504,6 +27533,19 @@ function runClosed(effect2) {
27504
27533
  return exports_Effect.runPromise(effect2);
27505
27534
  }
27506
27535
 
27536
+ // src/cli-intro.ts
27537
+ function terminalIntro(terminal) {
27538
+ if (terminal.isTTY !== true || terminal.term === "dumb" || (terminal.columns ?? 80) < 48)
27539
+ return "";
27540
+ return ` _|_
27541
+ .----- . textbutler
27542
+ | o o | A little help in your conversations.
27543
+ | === |
27544
+ '-----'
27545
+
27546
+ `;
27547
+ }
27548
+
27507
27549
  // src/io.ts
27508
27550
  var processIo = {
27509
27551
  stdout: (text3) => process.stdout.write(text3),
@@ -27512,9 +27554,27 @@ var processIo = {
27512
27554
  };
27513
27555
 
27514
27556
  // src/cli.ts
27515
- async function main(argv, io = processIo) {
27557
+ function isUsefulSupportResult(argv) {
27558
+ try {
27559
+ const parsed = parseArguments(argv);
27560
+ if (parsed.flags.has("help") || parsed.flags.has("version"))
27561
+ return false;
27562
+ const [command, action] = parsed.positionals;
27563
+ return command === "context" || ["sources list", "sources show", "contacts list", "contacts show", "contacts resolve", "inspect tempo", "inspect sessions", "study prepare", "ensoul prepare", "evaluate prepare", "profile apply", "profile show", "profile export", "routes list", "handoff prepare"].includes(`${command} ${action}`);
27564
+ } catch {
27565
+ return false;
27566
+ }
27567
+ }
27568
+ async function main(argv, io = processIo, supportEnv) {
27516
27569
  try {
27517
- await runCommand(argv, io);
27570
+ if (argv[0] === "support")
27571
+ return await runProductSupportCommand(argv.slice(1), io, supportEnv === undefined ? {} : { env: supportEnv });
27572
+ const rootHelp = argv.length === 0 || argv.length === 1 && argv[0] === "--help";
27573
+ const output = rootHelp && io === processIo ? {
27574
+ ...io,
27575
+ stdout: (text3) => io.stdout((text3 === HELP ? terminalIntro({ isTTY: process.stdout.isTTY, columns: process.stdout.columns, term: process.env.TERM }) : "") + text3)
27576
+ } : io;
27577
+ await runCommand(argv, output);
27518
27578
  return 0;
27519
27579
  } catch (error) {
27520
27580
  io.stderr(`${errorMessage(error)}
@@ -27523,8 +27583,14 @@ async function main(argv, io = processIo) {
27523
27583
  }
27524
27584
  }
27525
27585
  if (import.meta.main) {
27526
- process.exitCode = await main(process.argv.slice(2));
27586
+ const supportEnv = standaloneSupportEnvironment();
27587
+ const args2 = process.argv.slice(2);
27588
+ const exitCode = await main(args2, processIo, supportEnv);
27589
+ process.exitCode = exitCode;
27590
+ if (exitCode === 0 && isUsefulSupportResult(args2))
27591
+ await showProductSupportInvitation({ env: supportEnv });
27527
27592
  }
27528
27593
  export {
27529
- main
27594
+ main,
27595
+ isUsefulSupportResult
27530
27596
  };