pi-session-mail 0.2.1 → 0.3.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/CHANGELOG.md +26 -1
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/tools.ts +92 -14
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,29 @@ follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.0] - 2026-10-19
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `session_mail_list`, `session_mail_send`, `session_mail_ask` and
|
|
14
|
+
`session_mail_reply` declare an `outputSchema` and return matching
|
|
15
|
+
`structuredContent`, so a codemode script gets objects instead of text:
|
|
16
|
+
`{ sessions: [{ id, name?, cwd, state, waitingOn?, self }] }` from the list,
|
|
17
|
+
`{ id, to, running }` from a send, `{ id, to, outcome, status?, body? }`
|
|
18
|
+
from an ask (one variant per `outcome`: `answered`, `timed-out`,
|
|
19
|
+
`stopped-running`, `stopped-by-user`) and `{ id, ask, to }` from a reply.
|
|
20
|
+
The text the model sees is unchanged.
|
|
21
|
+
- The four tools declare all four MCP tool annotations: the list is read-only
|
|
22
|
+
and idempotent; send, ask and reply are not read-only and not destructive;
|
|
23
|
+
reply is idempotent, since a repeat is refused; none reaches an open world.
|
|
24
|
+
|
|
25
|
+
## [0.2.2] - 2026-10-02
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- The README links `CONTRIBUTING.md` and `SECURITY.md` on GitHub, since
|
|
30
|
+
neither is in the npm package and their relative links 404 on pi.dev.
|
|
31
|
+
|
|
9
32
|
## [0.2.1] - 2026-10-02
|
|
10
33
|
|
|
11
34
|
### Fixed
|
|
@@ -111,7 +134,9 @@ author's dotfiles; the changes below are against that copy.
|
|
|
111
134
|
- CI on Node 22.19 and 24: lint, typecheck, tests, and a check of the
|
|
112
135
|
published file list.
|
|
113
136
|
|
|
114
|
-
[Unreleased]: https://github.com/gvanderclay/pi-session-mail/compare/v0.
|
|
137
|
+
[Unreleased]: https://github.com/gvanderclay/pi-session-mail/compare/v0.3.0...HEAD
|
|
138
|
+
[0.3.0]: https://github.com/gvanderclay/pi-session-mail/compare/v0.2.2...v0.3.0
|
|
139
|
+
[0.2.2]: https://github.com/gvanderclay/pi-session-mail/compare/v0.2.1...v0.2.2
|
|
115
140
|
[0.2.1]: https://github.com/gvanderclay/pi-session-mail/compare/v0.2.0...v0.2.1
|
|
116
141
|
[0.2.0]: https://github.com/gvanderclay/pi-session-mail/compare/v0.1.0...v0.2.0
|
|
117
142
|
[0.1.0]: https://github.com/gvanderclay/pi-session-mail/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -470,10 +470,10 @@ while the package is at 0.x.
|
|
|
470
470
|
|
|
471
471
|
## More
|
|
472
472
|
|
|
473
|
-
- [CONTRIBUTING.md](CONTRIBUTING.md) explains how to set up, test and propose a
|
|
473
|
+
- [CONTRIBUTING.md](https://github.com/gvanderclay/pi-session-mail/blob/main/CONTRIBUTING.md) explains how to set up, test and propose a
|
|
474
474
|
change.
|
|
475
475
|
- [CHANGELOG.md](CHANGELOG.md) lists what changed in each release.
|
|
476
|
-
- [SECURITY.md](SECURITY.md) explains how to report a security problem.
|
|
476
|
+
- [SECURITY.md](https://github.com/gvanderclay/pi-session-mail/blob/main/SECURITY.md) explains how to report a security problem.
|
|
477
477
|
|
|
478
478
|
## License
|
|
479
479
|
|
package/package.json
CHANGED
package/src/tools.ts
CHANGED
|
@@ -118,11 +118,64 @@ function checkHops(hops: number, hooks: ToolHooks): void {
|
|
|
118
118
|
);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
/**
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
/**
|
|
122
|
+
* The one result every tool returns. `structuredContent` is what codemode scripts
|
|
123
|
+
* receive; it must match the tool's `outputSchema`, which Pi does not check.
|
|
124
|
+
*/
|
|
125
|
+
function toolResult<D, S>(text: string, details: D, structuredContent: S) {
|
|
126
|
+
return { content: [{ type: "text" as const, text }], details, structuredContent };
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
/** Hints for a tool that adds mail and never deletes or overwrites any, on this machine only. */
|
|
130
|
+
const SENDS_MAIL = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
131
|
+
|
|
132
|
+
const SessionList = Type.Object({
|
|
133
|
+
sessions: Type.Array(
|
|
134
|
+
Type.Object({
|
|
135
|
+
id: Type.String({ description: "The session's full id; pass it as `to`" }),
|
|
136
|
+
name: Type.Optional(Type.String({ description: "The session name; absent when none is set" })),
|
|
137
|
+
cwd: Type.String(),
|
|
138
|
+
state: Type.Union([Type.Literal("idle"), Type.Literal("busy")]),
|
|
139
|
+
waitingOn: Type.Optional(
|
|
140
|
+
Type.String({ description: "The full id of the session this one's ask waits on; absent when none" }),
|
|
141
|
+
),
|
|
142
|
+
self: Type.Boolean({ description: "True for the calling session" }),
|
|
143
|
+
}),
|
|
144
|
+
),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const SendResult = Type.Object({
|
|
148
|
+
id: Type.String({ description: "The message's id" }),
|
|
149
|
+
to: Type.String({ description: "The recipient's full id" }),
|
|
150
|
+
running: Type.Boolean({ description: "False when the message waits in a closed session's inbox" }),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const asked = {
|
|
154
|
+
id: Type.String({ description: "The ask's id" }),
|
|
155
|
+
to: Type.String({ description: "The asked session's full id" }),
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/** One variant per {@link AskOutcome}, told apart by `outcome`. */
|
|
159
|
+
const AskResult = Type.Union([
|
|
160
|
+
Type.Object({
|
|
161
|
+
...asked,
|
|
162
|
+
outcome: Type.Literal("answered"),
|
|
163
|
+
status: Type.String({
|
|
164
|
+
description: '"done" for an answer; any other status means the run ended otherwise, and body is not an answer',
|
|
165
|
+
}),
|
|
166
|
+
body: Type.String(),
|
|
167
|
+
}),
|
|
168
|
+
Type.Object({ ...asked, outcome: Type.Literal("timed-out") }),
|
|
169
|
+
Type.Object({ ...asked, outcome: Type.Literal("stopped-running") }),
|
|
170
|
+
Type.Object({ ...asked, outcome: Type.Literal("stopped-by-user") }),
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
const ReplyResult = Type.Object({
|
|
174
|
+
id: Type.String({ description: "The reply's id" }),
|
|
175
|
+
ask: Type.String({ description: "The id of the ask it answers" }),
|
|
176
|
+
to: Type.String({ description: "The asker's full id" }),
|
|
177
|
+
});
|
|
178
|
+
|
|
126
179
|
/** One running session as the list shows it. */
|
|
127
180
|
function describe(record: RunningRecord, byAddress: ReadonlyMap<string, RunningRecord>, me: string): string {
|
|
128
181
|
const waiting = record.waitingOn === "" ? "" : `, waiting on ${waitLabel(record.waitingOn, byAddress)}`;
|
|
@@ -170,16 +223,31 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
170
223
|
description:
|
|
171
224
|
"List the Pi sessions running on this machine, in every agent root: each one's name (or short id when it has none), full id, working directory, idle or busy state, and whom it is waiting on. The calling session is marked. A session is addressed by its name, its full id, or an id prefix of at least 8 characters.",
|
|
172
225
|
parameters: Type.Object({}),
|
|
226
|
+
outputSchema: SessionList,
|
|
227
|
+
// Listing deletes the records of sessions whose process is gone, which is housekeeping, not a change.
|
|
228
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
173
229
|
async execute(_toolCallId: string, _params: unknown, _signal: unknown, _onUpdate: unknown, ctx: ExtensionContext) {
|
|
174
230
|
const me = ctx.sessionManager.getSessionId();
|
|
175
231
|
const running = listRunning();
|
|
176
|
-
if (running.length === 0)
|
|
232
|
+
if (running.length === 0)
|
|
233
|
+
return toolResult("No Pi sessions are running with a mailbox.", { sessions: [] }, { sessions: [] });
|
|
177
234
|
const byAddress = new Map(running.map((record) => [record.address, record]));
|
|
178
235
|
const lines = running.map((record) => describe(record, byAddress, me));
|
|
179
236
|
const count = `${running.length} running session${running.length === 1 ? "" : "s"}:`;
|
|
180
|
-
return toolResult(
|
|
181
|
-
|
|
182
|
-
|
|
237
|
+
return toolResult(
|
|
238
|
+
[count, ...lines].join("\n"),
|
|
239
|
+
{ sessions: running.map((record) => ({ ...record, self: record.address === me })) },
|
|
240
|
+
{
|
|
241
|
+
sessions: running.map(({ address, name, cwd, state, waitingOn }) => ({
|
|
242
|
+
id: address,
|
|
243
|
+
...(name === undefined ? {} : { name }),
|
|
244
|
+
cwd,
|
|
245
|
+
state,
|
|
246
|
+
...(waitingOn === "" ? {} : { waitingOn }),
|
|
247
|
+
self: address === me,
|
|
248
|
+
})),
|
|
249
|
+
},
|
|
250
|
+
);
|
|
183
251
|
},
|
|
184
252
|
});
|
|
185
253
|
|
|
@@ -196,6 +264,8 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
196
264
|
description: "The message text. The recipient sees it labelled as coming from this session.",
|
|
197
265
|
}),
|
|
198
266
|
}),
|
|
267
|
+
outputSchema: SendResult,
|
|
268
|
+
annotations: SENDS_MAIL,
|
|
199
269
|
async execute(
|
|
200
270
|
_toolCallId: string,
|
|
201
271
|
params: { to: string; message: string },
|
|
@@ -216,7 +286,8 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
216
286
|
target === undefined
|
|
217
287
|
? `Message ${envelope.id} left for ${to}, which is not running: it waits in that session's inbox and is delivered when the session starts or resumes.`
|
|
218
288
|
: `Sent message ${envelope.id} to ${label(target)} (${to}). It expects no answer; any answer arrives as a message.`;
|
|
219
|
-
|
|
289
|
+
const result = { id: envelope.id, to, running: target !== undefined };
|
|
290
|
+
return toolResult(text, result, result);
|
|
220
291
|
},
|
|
221
292
|
});
|
|
222
293
|
|
|
@@ -234,6 +305,8 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
234
305
|
"The question. The recipient sees it labelled as coming from this session, with this session waiting.",
|
|
235
306
|
}),
|
|
236
307
|
}),
|
|
308
|
+
outputSchema: AskResult,
|
|
309
|
+
annotations: SENDS_MAIL,
|
|
237
310
|
async execute(
|
|
238
311
|
_toolCallId: string,
|
|
239
312
|
params: { to: string; message: string },
|
|
@@ -283,7 +356,8 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
283
356
|
if (signal?.aborted) onAbort();
|
|
284
357
|
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
285
358
|
});
|
|
286
|
-
|
|
359
|
+
const result = { id: ask.id, to, ...outcome };
|
|
360
|
+
return toolResult(askText(ask, target, outcome), result, result);
|
|
287
361
|
},
|
|
288
362
|
});
|
|
289
363
|
|
|
@@ -296,6 +370,9 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
296
370
|
ask: Type.String({ description: "The ask id, as given in the ask's label." }),
|
|
297
371
|
message: Type.String({ description: "The answer." }),
|
|
298
372
|
}),
|
|
373
|
+
outputSchema: ReplyResult,
|
|
374
|
+
// A repeated reply to the same ask is refused and sends nothing.
|
|
375
|
+
annotations: { ...SENDS_MAIL, idempotentHint: true },
|
|
299
376
|
async execute(
|
|
300
377
|
_toolCallId: string,
|
|
301
378
|
params: { ask: string; message: string },
|
|
@@ -310,11 +387,12 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
310
387
|
hooks.sent();
|
|
311
388
|
const sender = listRunning().find((record) => record.address === answer.to);
|
|
312
389
|
const who = sender === undefined ? answer.to : `${label(sender)} (${answer.to})`;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
ask
|
|
316
|
-
|
|
317
|
-
|
|
390
|
+
const result = { id: answer.id, ask: id, to: answer.to };
|
|
391
|
+
return toolResult(
|
|
392
|
+
`Answered ask ${id} from ${who}; this run's last message will not be sent for it.`,
|
|
393
|
+
result,
|
|
394
|
+
result,
|
|
395
|
+
);
|
|
318
396
|
},
|
|
319
397
|
});
|
|
320
398
|
|