1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
//! Message types, tag constants, and the `Message` decoding/formatting helpers.
//!
//! Why: Isolating the data types and tag-building from the send/receive async
//! logic keeps each file under the 500-SLOC cap and allows unit tests to
//! target decoding independently from I/O.
//! What: `Message`, tag-prefix constants, `build_message_tags`,
//! `extract_tag`, and the slug helpers (`slugify_string`, `slugify_for_palace`).
//! Test: `build_message_tags_includes_all_fields`,
//! `decode_message_from_drawer_round_trips`, `slug_derivation_cases`.
use ;
use ;
use ;
use Path;
use Drawer;
use Uuid;
/// Tag namespace prefix marking a drawer as a v1 inter-project message.
///
/// Why: A single static marker tag lets `inbox-check` filter drawers by tag
/// without having to scan every `msg:*` namespaced tag — and gives the UI a
/// cheap "is this a message?" check without parsing the other tags.
/// What: The literal `"msg:v1"`. Bump the suffix if the message envelope
/// schema ever needs a breaking change.
/// Test: Indirectly via `round_trip_send_and_inbox`.
pub const MSG_MARKER_TAG: &str = "msg:v1";
/// Tag prefix carrying the sender's palace id (e.g. `msg:from=trusty-tools`).
pub const TAG_FROM_PREFIX: &str = "msg:from=";
/// Tag prefix carrying the recipient palace id (e.g. `msg:to=claude-mpm`).
pub const TAG_TO_PREFIX: &str = "msg:to=";
/// Tag prefix carrying the sender-defined purpose (e.g. `msg:purpose=task`).
pub const TAG_PURPOSE_PREFIX: &str = "msg:purpose=";
/// Tag prefix carrying the RFC3339 send timestamp (e.g.
/// `msg:sent_at=2026-05-25T12:34:56+00:00`).
pub const TAG_SENT_AT_PREFIX: &str = "msg:sent_at=";
/// Tag prefix carrying the read flag (`msg:read=false` or `msg:read=true`).
pub const TAG_READ_PREFIX: &str = "msg:read=";
/// Decoded view of a message drawer.
///
/// Why: `inbox-check` and the HTTP `GET /api/v1/messages` endpoint both want
/// a typed view of every message field, not the raw `Vec<String>` of tags.
/// What: Owned strings plus the drawer id and content, populated by
/// [`Message::from_drawer`].
/// Test: `decode_message_from_drawer_round_trips`.
/// Extract the value of the first tag matching `prefix`.
///
/// Why: every `msg:*=...` field is encoded as a single tag entry; the
/// receiver needs to recover the value half. Returning `Option<&str>`
/// keeps the caller's error handling uniform (use `?` to bail on any
/// missing required field).
/// What: returns `Some(&str)` pointing at the substring after `prefix` of
/// the first tag whose entire text starts with `prefix`, or `None` if no
/// tag matches.
/// Test: indirectly via `decode_message_from_drawer_round_trips`.
pub
/// Build the tag vector for a freshly-sent message.
///
/// Why: the send path (MCP tool, CLI, HTTP) all want the exact same tag
/// shape — centralising it here means a future schema bump only touches
/// one function.
/// What: returns `[MSG_MARKER_TAG, msg:from=…, msg:to=…, msg:purpose=…,
/// msg:sent_at=…, msg:read=false]` in that order.
/// Test: `build_message_tags_includes_all_fields`.
/// Derive a palace slug from a filesystem path.
///
/// Why: addressing inter-project messages by repo slug means we need a
/// deterministic, reversible-ish rule that maps a working-tree path to a
/// stable palace name. Git users expect the slug to match their repo name;
/// non-git working trees fall back to the directory basename. We aggressively
/// canonicalise so casing, whitespace, and underscore vs. hyphen don't
/// produce two different palaces for the same project.
/// What: returns `basename(toplevel_or_cwd).lowercase()` with:
/// - every run of whitespace or `_` collapsed to a single `-`,
/// - every character outside `[a-z0-9-]` stripped,
/// - leading / trailing `-` trimmed,
/// - consecutive `-` collapsed to one.
///
/// Examples (all yield `trusty-tools`):
/// - `/Users/bob/Projects/trusty-tools`
/// - `/Users/bob/Projects/Trusty_Tools`
/// - `/Users/bob/Projects/trusty tools/`
/// - `/Users/bob/Projects/.trusty-tools.git` (git-suffix stripped)
///
/// Test: `tests::slug_derivation_cases`.
/// String-level slug helper used by [`slugify_for_palace`].
///
/// Why: exposed separately so the CLI can slugify an arbitrary repo name
/// (e.g. from `--to my_project`) without re-deriving from a path. As of #1348
/// the implementation lives in `trusty-common` as the single source of truth
/// (shared with `trusty-installer ensure`); this is a re-export shim
/// kept so the many `trusty_memory::messaging::slugify_string` call sites do
/// not churn.
/// What: re-exports [`trusty_common::slugify_string`] verbatim.
/// Test: canonical cases pinned in `trusty-common` (`slug::tests`); parity
/// re-asserted here in `tests::slug_derivation_cases`.
pub use slugify_string;