pi-session-mail 0.6.0 → 0.6.2
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 +20 -1
- package/package.json +1 -1
- package/src/index.ts +13 -7
- package/src/tools.ts +40 -35
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,23 @@ follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.6.2] - 2026-11-01
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Shorter declarations for the four session-mail tools reduce model context use.
|
|
14
|
+
Delivery, ask waits, reply rules, result fields, and errors are unchanged.
|
|
15
|
+
|
|
16
|
+
## [0.6.1] - 2026-10-31
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- The label on the answer to an ask now calls it an ask: "It is a reply to your
|
|
21
|
+
ask …" and "[mailbox] Your ask …, quoted from …" (and "Ask cut at 2 KiB").
|
|
22
|
+
It said "request" before, which now names a different kind of mail. A reply
|
|
23
|
+
to a request, or to an id with no copy in `sent/`, keeps the request wording.
|
|
24
|
+
The test that pinned the old wording for an ask's answer is updated.
|
|
25
|
+
|
|
9
26
|
## [0.6.0] - 2026-10-31
|
|
10
27
|
|
|
11
28
|
### Added
|
|
@@ -240,7 +257,9 @@ author's dotfiles; the changes below are against that copy.
|
|
|
240
257
|
- CI on Node 22.19 and 24: lint, typecheck, tests, and a check of the
|
|
241
258
|
published file list.
|
|
242
259
|
|
|
243
|
-
[Unreleased]: https://github.com/gvanderclay/pi-session-mail/compare/v0.6.
|
|
260
|
+
[Unreleased]: https://github.com/gvanderclay/pi-session-mail/compare/v0.6.2...HEAD
|
|
261
|
+
[0.6.2]: https://github.com/gvanderclay/pi-session-mail/compare/v0.6.1...v0.6.2
|
|
262
|
+
[0.6.1]: https://github.com/gvanderclay/pi-session-mail/compare/v0.6.0...v0.6.1
|
|
244
263
|
[0.6.0]: https://github.com/gvanderclay/pi-session-mail/compare/v0.5.0...v0.6.0
|
|
245
264
|
[0.5.0]: https://github.com/gvanderclay/pi-session-mail/compare/v0.4.0...v0.5.0
|
|
246
265
|
[0.4.0]: https://github.com/gvanderclay/pi-session-mail/compare/v0.3.0...v0.4.0
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -75,14 +75,15 @@ function cap(text: string, max: number): string | undefined {
|
|
|
75
75
|
*/
|
|
76
76
|
const plain = (text: string): string => text.replace(/[\p{Cc}\u2028\u2029]/gu, " ");
|
|
77
77
|
|
|
78
|
-
/** A reply quotes each request it answers: the id, the copy's path, then the capped body. */
|
|
78
|
+
/** A reply quotes each request or ask it answers: the id, the copy's path, then the capped body. */
|
|
79
79
|
function quoteRequest(request: SentCopy): string {
|
|
80
|
+
const ask = request.envelope.kind === "ask";
|
|
80
81
|
const cut = cap(request.envelope.body, QUOTE_CAP);
|
|
81
82
|
const body =
|
|
82
83
|
cut === undefined
|
|
83
84
|
? request.envelope.body
|
|
84
|
-
: `${cut}\n[mailbox] Request cut at 2 KiB; the full copy is ${plain(request.path)}`;
|
|
85
|
-
return `[mailbox] Your request ${plain(request.envelope.id)}, quoted from ${plain(request.path)}:\n${body
|
|
85
|
+
: `${cut}\n[mailbox] ${ask ? "Ask" : "Request"} cut at 2 KiB; the full copy is ${plain(request.path)}`;
|
|
86
|
+
return `[mailbox] Your ${ask ? "ask" : "request"} ${plain(request.envelope.id)}, quoted from ${plain(request.path)}:\n${body
|
|
86
87
|
.split("\n")
|
|
87
88
|
.map((line) => `> ${line}`)
|
|
88
89
|
.join("\n")}`;
|
|
@@ -136,10 +137,10 @@ function inboundText(
|
|
|
136
137
|
envelope: Envelope,
|
|
137
138
|
path: string,
|
|
138
139
|
quotes: readonly string[],
|
|
139
|
-
opts: { lateAnswer: boolean; byUser: boolean },
|
|
140
|
+
opts: { lateAnswer: boolean; byUser: boolean; answersAsks: boolean },
|
|
140
141
|
senderRecord: RunningRecord | undefined,
|
|
141
142
|
): string {
|
|
142
|
-
const { lateAnswer, byUser } = opts;
|
|
143
|
+
const { lateAnswer, byUser, answersAsks } = opts;
|
|
143
144
|
const sender = senderLine(envelope.from, senderRecord);
|
|
144
145
|
const id = plain(envelope.id);
|
|
145
146
|
const replyIds = envelope.in_reply_to.map(plain);
|
|
@@ -152,7 +153,9 @@ function inboundText(
|
|
|
152
153
|
if (envelope.kind === "message")
|
|
153
154
|
header.push(`It expects no answer; if one is wanted, send it with session_mail_send to ${envelope.from}.`);
|
|
154
155
|
if (envelope.in_reply_to.length > 0)
|
|
155
|
-
header.push(
|
|
156
|
+
header.push(
|
|
157
|
+
`It is a reply to your ${answersAsks ? "ask" : "request"}${envelope.in_reply_to.length > 1 ? "s" : ""} ${replyIds.join(", ")}.`,
|
|
158
|
+
);
|
|
156
159
|
if (byUser)
|
|
157
160
|
header.push("The user made that request, typing it with /mailbox or through an extension such as delegate.");
|
|
158
161
|
if (envelope.in_reply_to.length > 0 && envelope.status !== "done")
|
|
@@ -395,6 +398,9 @@ export default function mailbox(pi: ExtensionAPI) {
|
|
|
395
398
|
const lateAnswer = envelope.kind === "reply" && requests.some((copy) => copy.envelope.kind === "ask");
|
|
396
399
|
const wakes = envelope.kind !== "reply" || lateAnswer;
|
|
397
400
|
const byUser = envelope.kind === "reply" && requests.some((copy) => copy.envelope.kind === "request");
|
|
401
|
+
// The label says "ask" only when every id it answers is a known ask; otherwise "request", as before.
|
|
402
|
+
const answersAsks =
|
|
403
|
+
requests.length === envelope.in_reply_to.length && requests.every((copy) => copy.envelope.kind === "ask");
|
|
398
404
|
// Listeners run synchronously inside emit, so `handled` is final when it returns.
|
|
399
405
|
const inbound: InboundPayload = { envelope, path, requests, handled: false };
|
|
400
406
|
pi.events.emit(INBOUND, inbound);
|
|
@@ -415,7 +421,7 @@ export default function mailbox(pi: ExtensionAPI) {
|
|
|
415
421
|
envelope,
|
|
416
422
|
path,
|
|
417
423
|
quotes,
|
|
418
|
-
{ lateAnswer, byUser },
|
|
424
|
+
{ lateAnswer, byUser, answersAsks },
|
|
419
425
|
listRunning().find((r) => r.address === envelope.from),
|
|
420
426
|
),
|
|
421
427
|
display: true,
|
package/src/tools.ts
CHANGED
|
@@ -126,49 +126,54 @@ const SENDS_MAIL = { readOnlyHint: false, destructiveHint: false, idempotentHint
|
|
|
126
126
|
const SessionList = Type.Object({
|
|
127
127
|
sessions: Type.Array(
|
|
128
128
|
Type.Object({
|
|
129
|
-
id: Type.String({ description: "
|
|
130
|
-
name: Type.Optional(Type.String(
|
|
129
|
+
id: Type.String({ description: "Full session id; usable as `to`" }),
|
|
130
|
+
name: Type.Optional(Type.String()),
|
|
131
131
|
cwd: Type.String(),
|
|
132
132
|
state: Type.Union([Type.Literal("idle"), Type.Literal("busy")]),
|
|
133
133
|
waitingOn: Type.Optional(
|
|
134
|
-
Type.String({ description: "
|
|
134
|
+
Type.String({ description: "Full session id this session's ask waits on; absent if none" }),
|
|
135
135
|
),
|
|
136
|
-
self: Type.Boolean({ description: "True for the
|
|
136
|
+
self: Type.Boolean({ description: "True for the caller" }),
|
|
137
137
|
}),
|
|
138
138
|
),
|
|
139
139
|
});
|
|
140
140
|
|
|
141
141
|
const SendResult = Type.Object({
|
|
142
|
-
id: Type.String({ description: "
|
|
143
|
-
to: Type.String({ description: "
|
|
144
|
-
running: Type.Boolean({ description: "False
|
|
142
|
+
id: Type.String({ description: "Message id" }),
|
|
143
|
+
to: Type.String({ description: "Recipient's full session id" }),
|
|
144
|
+
running: Type.Boolean({ description: "False if queued in a closed session's inbox" }),
|
|
145
145
|
});
|
|
146
146
|
|
|
147
147
|
const asked = {
|
|
148
|
-
id: Type.String({ description: "
|
|
149
|
-
to: Type.String({ description: "
|
|
148
|
+
id: Type.String({ description: "Ask id" }),
|
|
149
|
+
to: Type.String({ description: "Asked session's full id" }),
|
|
150
150
|
};
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
Type.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
152
|
+
const AskResult = Type.Intersect([
|
|
153
|
+
Type.Object(asked),
|
|
154
|
+
Type.Union([
|
|
155
|
+
Type.Object({
|
|
156
|
+
outcome: Type.Literal("answered"),
|
|
157
|
+
status: Type.String({
|
|
158
|
+
description: '"done" means an answer; otherwise the run ended without an answer and body is not one',
|
|
159
|
+
}),
|
|
160
|
+
body: Type.String(),
|
|
161
|
+
}),
|
|
162
|
+
Type.Object({
|
|
163
|
+
outcome: Type.Union([
|
|
164
|
+
Type.Literal("timed-out"),
|
|
165
|
+
Type.Literal("stopped-running"),
|
|
166
|
+
Type.Literal("stopped-by-user"),
|
|
167
|
+
Type.Literal("background"),
|
|
168
|
+
]),
|
|
159
169
|
}),
|
|
160
|
-
|
|
161
|
-
}),
|
|
162
|
-
Type.Object({ ...asked, outcome: Type.Literal("timed-out") }),
|
|
163
|
-
Type.Object({ ...asked, outcome: Type.Literal("stopped-running") }),
|
|
164
|
-
Type.Object({ ...asked, outcome: Type.Literal("stopped-by-user") }),
|
|
165
|
-
Type.Object({ ...asked, outcome: Type.Literal("background") }),
|
|
170
|
+
]),
|
|
166
171
|
]);
|
|
167
172
|
|
|
168
173
|
const ReplyResult = Type.Object({
|
|
169
|
-
id: Type.String({ description: "
|
|
170
|
-
ask: Type.String({ description: "
|
|
171
|
-
to: Type.String({ description: "
|
|
174
|
+
id: Type.String({ description: "Reply id" }),
|
|
175
|
+
ask: Type.String({ description: "Answered ask id" }),
|
|
176
|
+
to: Type.String({ description: "Asker's full session id" }),
|
|
172
177
|
});
|
|
173
178
|
|
|
174
179
|
/** One running session as the list shows it. */
|
|
@@ -215,7 +220,7 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
215
220
|
name: "session_mail_list",
|
|
216
221
|
label: "Session mail: list",
|
|
217
222
|
description:
|
|
218
|
-
"List
|
|
223
|
+
"List running Pi sessions across all agent roots on this machine, with name (short id if unnamed), full id, cwd, idle/busy state and whom each waits on. Marks the caller. Address running sessions by name, full id or 8+ character id prefix.",
|
|
219
224
|
parameters: Type.Object({}),
|
|
220
225
|
outputSchema: SessionList,
|
|
221
226
|
// Listing deletes the records of sessions whose process is gone, which is housekeeping, not a change.
|
|
@@ -249,13 +254,13 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
249
254
|
name: "session_mail_send",
|
|
250
255
|
label: "Session mail: send",
|
|
251
256
|
description:
|
|
252
|
-
"Send
|
|
257
|
+
"Send mail to a Pi session on this machine. An idle recipient starts a turn; a busy one reads it at the next tool-call gap. No answer is expected or automatic; any answer uses session_mail_send. Address running sessions by name, full id or 8+ character id prefix. Closed sessions require a full id; mail queues until they start. Discover running sessions with session_mail_list.",
|
|
253
258
|
parameters: Type.Object({
|
|
254
259
|
to: Type.String({
|
|
255
|
-
description: "
|
|
260
|
+
description: "Recipient's name or 8+ character id prefix if running; full session id otherwise.",
|
|
256
261
|
}),
|
|
257
262
|
message: Type.String({
|
|
258
|
-
description: "
|
|
263
|
+
description: "Message text, labelled as from this session.",
|
|
259
264
|
}),
|
|
260
265
|
}),
|
|
261
266
|
outputSchema: SendResult,
|
|
@@ -286,13 +291,13 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
286
291
|
name: "session_mail_ask",
|
|
287
292
|
label: "Session mail: ask",
|
|
288
293
|
description:
|
|
289
|
-
"Ask
|
|
294
|
+
"Ask a running Pi session on this machine. Its final message at run settle or an early reply answers. In a watched TUI, returns at once; the later answer starts a turn. Unattended sessions, including delegates, wait up to 10 minutes for the answer as this tool's result. Address by name, full id or 8+ character id prefix; discover with session_mail_list. Use session_mail_send for notes needing no answer.",
|
|
290
295
|
parameters: Type.Object({
|
|
291
296
|
to: Type.String({
|
|
292
|
-
description: "
|
|
297
|
+
description: "Running recipient's name, full id or 8+ character id prefix.",
|
|
293
298
|
}),
|
|
294
299
|
message: Type.String({
|
|
295
|
-
description: "
|
|
300
|
+
description: "Question, labelled as from this session.",
|
|
296
301
|
}),
|
|
297
302
|
}),
|
|
298
303
|
outputSchema: AskResult,
|
|
@@ -354,10 +359,10 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
354
359
|
name: "session_mail_reply",
|
|
355
360
|
label: "Session mail: reply",
|
|
356
361
|
description:
|
|
357
|
-
"Answer
|
|
362
|
+
"Answer a received ask at once without ending this run. Each ask allows one reply, replacing this run's final-settle answer for it. Use the ask id from its label. Only asks qualify; requests are answered by their consuming extension, and messages expect no answer.",
|
|
358
363
|
parameters: Type.Object({
|
|
359
|
-
ask: Type.String({ description: "
|
|
360
|
-
message: Type.String({ description: "
|
|
364
|
+
ask: Type.String({ description: "Ask id from its label." }),
|
|
365
|
+
message: Type.String({ description: "Answer text." }),
|
|
361
366
|
}),
|
|
362
367
|
outputSchema: ReplyResult,
|
|
363
368
|
// A repeated reply to the same ask is refused and sends nothing.
|