pi-lxmf 0.2.0 → 0.2.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 +22 -0
- package/SPEC.md +7 -1
- package/package.json +1 -1
- package/src/bridge.js +5 -3
- package/src/commands.js +48 -35
- package/src/lxmf.js +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.2] - 2026-09-28
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Bridge command replies (especially `/help`, `/status`, `/session`,
|
|
15
|
+
`/model`, `/cd`) are now authored in Markdown — section headings, bullet
|
|
16
|
+
lists and code spans around commands, models, paths and hashes — matching
|
|
17
|
+
the content-format metadata (`FIELD_RENDERER: RENDERER_MARKDOWN`) every
|
|
18
|
+
outbound message already carries, so they render nicely in Sideband and
|
|
19
|
+
NomadNet instead of as plain text.
|
|
20
|
+
- The `/cd` switch banner uses a Markdown horizontal rule and a bold
|
|
21
|
+
headline instead of a `📂 ───` decoration line.
|
|
22
|
+
|
|
23
|
+
## [0.2.1] - 2026-09-28
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- Outbound LXMF messages now signal their content format: every content
|
|
28
|
+
chunk carries the `FIELD_RENDERER` field set to `RENDERER_MARKDOWN`, so
|
|
29
|
+
clients (Sideband, NomadNet) render pi's Markdown replies correctly
|
|
30
|
+
instead of showing raw markup as plain text.
|
|
31
|
+
|
|
10
32
|
## [0.2.0] - 2026-09-27
|
|
11
33
|
|
|
12
34
|
### Added
|
package/SPEC.md
CHANGED
|
@@ -181,6 +181,10 @@ example:
|
|
|
181
181
|
paragraph boundaries where possible, each chunk suffixed
|
|
182
182
|
`[… n/N]` except the last. Chunking keeps single LXMF messages
|
|
183
183
|
reasonable for phone UIs and for mesh airtime.
|
|
184
|
+
- **Content format:** every chunk carries the LXMF `FIELD_RENDERER` field
|
|
185
|
+
set to `RENDERER_MARKDOWN` (§5.9.4), since pi's replies and the bridge
|
|
186
|
+
command replies are Markdown — clients (Sideband, NomadNet) use it to
|
|
187
|
+
render them instead of showing raw markup as plain text.
|
|
184
188
|
- **Errors:** failures to deliver a reply are logged and retried once;
|
|
185
189
|
persistent failure is reported in the next successful message (LXMF has
|
|
186
190
|
no channel over which to report its own failure).
|
|
@@ -363,7 +367,9 @@ in one place so it can be retargeted per subtree.
|
|
|
363
367
|
|
|
364
368
|
Command parsing: first whitespace-separated token, case-insensitive,
|
|
365
369
|
leading `!` equivalent to `/` for bridge commands (a bare `!` is the
|
|
366
|
-
interrupt). Commands are recognized only from the owner.
|
|
370
|
+
interrupt). Commands are recognized only from the owner. Command replies
|
|
371
|
+
are Markdown-formatted (§5.1): section headings, bullet lists, code spans
|
|
372
|
+
around commands, models, paths and hashes.
|
|
367
373
|
|
|
368
374
|
## 8. Configuration and state
|
|
369
375
|
|
package/package.json
CHANGED
package/src/bridge.js
CHANGED
|
@@ -634,11 +634,13 @@ export class Bridge {
|
|
|
634
634
|
const rel = relative(this.config.workdir, absPath) || ".";
|
|
635
635
|
// A visually loud banner: project changes are the main boundaries
|
|
636
636
|
// in the message history, so they must be easy to spot while
|
|
637
|
-
// scrolling back.
|
|
638
|
-
|
|
637
|
+
// scrolling back. Replies are Markdown on the wire (§5.1), so the
|
|
638
|
+
// separator is a horizontal rule and the headline bold.
|
|
639
|
+
const lines = ["---", "", `**📂 Switched to \`${rel}\`**`];
|
|
639
640
|
lines.push(
|
|
641
|
+
"",
|
|
640
642
|
pointer
|
|
641
|
-
? `🔁 Resuming session
|
|
643
|
+
? `🔁 Resuming session \`${basename(pointer.sessionFile)}\``
|
|
642
644
|
: "✨ Fresh session",
|
|
643
645
|
);
|
|
644
646
|
return lines.join("\n");
|
package/src/commands.js
CHANGED
|
@@ -97,7 +97,9 @@ export function matchModel(models, query) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
* Formats the model list with the current model marked.
|
|
100
|
+
* Formats the model list with the current model marked. Markdown: every
|
|
101
|
+
* outbound message carries `FIELD_RENDERER: RENDERER_MARKDOWN` (§5.1),
|
|
102
|
+
* so replies render as such in Sideband/NomadNet.
|
|
101
103
|
*
|
|
102
104
|
* @param {Array<{id?: string, provider?: string, name?: string}>} models
|
|
103
105
|
* @param {{provider?: string, id?: string}|null} current
|
|
@@ -110,15 +112,15 @@ export function formatModelList(models, current) {
|
|
|
110
112
|
: null;
|
|
111
113
|
const lines = models.map((m) => {
|
|
112
114
|
const key = `${m.provider}/${m.id}`;
|
|
113
|
-
const marker = key.toLowerCase() === currentKey ? " ← current" : "";
|
|
115
|
+
const marker = key.toLowerCase() === currentKey ? " ← **current**" : "";
|
|
114
116
|
const name = m.name && m.name !== m.id ? ` — ${m.name}` : "";
|
|
115
|
-
return
|
|
117
|
+
return `- \`${key}\`${name}${marker}`;
|
|
116
118
|
});
|
|
117
|
-
return [
|
|
119
|
+
return [`**Models (${models.length})**`, "", ...lines].join("\n");
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
/**
|
|
121
|
-
* Formats `get_state` + bridge info as the `/status` reply.
|
|
123
|
+
* Formats `get_state` + bridge info as the `/status` reply (Markdown).
|
|
122
124
|
*
|
|
123
125
|
* @param {any} state - `get_state` data.
|
|
124
126
|
* @param {object} bridgeInfo
|
|
@@ -132,28 +134,30 @@ export function formatModelList(models, current) {
|
|
|
132
134
|
*/
|
|
133
135
|
export function formatStatus(state, bridgeInfo) {
|
|
134
136
|
const model = state?.model
|
|
135
|
-
?
|
|
137
|
+
? `\`${state.model.provider}/${state.model.id}\``
|
|
136
138
|
: "(none)";
|
|
137
139
|
const session = state?.sessionFile
|
|
138
|
-
? `${state.sessionName ?? "(unnamed)"}
|
|
140
|
+
? `${state.sessionName ?? "(unnamed)"} (\`${basename(state.sessionFile)}\`)`
|
|
139
141
|
: "(none)";
|
|
140
142
|
return [
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
143
|
+
"**Status**",
|
|
144
|
+
"",
|
|
145
|
+
`- Model: ${model}`,
|
|
146
|
+
`- Thinking: ${state?.thinkingLevel ?? "off"}`,
|
|
147
|
+
`- Busy: ${state?.isStreaming ? "yes" : "no"}`,
|
|
148
|
+
`- Session: ${session}`,
|
|
149
|
+
`- Cwd: \`${bridgeInfo.cwd ?? "?"}\``,
|
|
150
|
+
`- Workdir: \`${bridgeInfo.workdir ?? "?"}\``,
|
|
151
|
+
`- Node: \`${bridgeInfo.identityHash}\``,
|
|
152
|
+
`- LXMF: \`${bridgeInfo.deliveryHash}\``,
|
|
153
|
+
`- Owner: \`${bridgeInfo.owner ?? "?"}\``,
|
|
154
|
+
`- Uptime: ${formatDuration(bridgeInfo.uptimeMs)}`,
|
|
151
155
|
].join("\n");
|
|
152
156
|
}
|
|
153
157
|
|
|
154
158
|
/**
|
|
155
159
|
* Formats the `/cd` (no arguments) reply: the current repo and the
|
|
156
|
-
* recently used repos under the daemon workdir.
|
|
160
|
+
* recently used repos under the daemon workdir (Markdown).
|
|
157
161
|
*
|
|
158
162
|
* @param {object} bridgeInfo
|
|
159
163
|
* @param {string} bridgeInfo.workdir
|
|
@@ -164,19 +168,21 @@ export function formatStatus(state, bridgeInfo) {
|
|
|
164
168
|
export function formatRepoList(bridgeInfo) {
|
|
165
169
|
const workdir = bridgeInfo.workdir;
|
|
166
170
|
const cwd = bridgeInfo.cwd ?? workdir;
|
|
167
|
-
const lines = [
|
|
171
|
+
const lines = [
|
|
172
|
+
"**Repos**",
|
|
173
|
+
"",
|
|
174
|
+
`- Current: \`${relative(workdir, cwd) || "."}\` (\`${cwd}\`)`,
|
|
175
|
+
];
|
|
168
176
|
const recent = bridgeInfo.recentWorkdirs ?? [];
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
for (const w of recent) lines.push(` ${relative(workdir, w) || "."}`);
|
|
174
|
-
}
|
|
177
|
+
const names = recent.map((w) => `\`${relative(workdir, w) || "."}\``);
|
|
178
|
+
lines.push(
|
|
179
|
+
names.length > 0 ? `- Recent: ${names.join(", ")}` : "- Recent: (none)",
|
|
180
|
+
);
|
|
175
181
|
return lines.join("\n");
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
/**
|
|
179
|
-
* Formats `get_session_stats` data as the `/session` reply.
|
|
185
|
+
* Formats `get_session_stats` data as the `/session` reply (Markdown).
|
|
180
186
|
*
|
|
181
187
|
* @param {any} stats
|
|
182
188
|
* @returns {string}
|
|
@@ -184,10 +190,12 @@ export function formatRepoList(bridgeInfo) {
|
|
|
184
190
|
export function formatSessionStats(stats) {
|
|
185
191
|
if (!stats) return "No session stats available.";
|
|
186
192
|
return [
|
|
187
|
-
|
|
193
|
+
"**Session**",
|
|
194
|
+
"",
|
|
195
|
+
`- Messages: ${stats.userMessages ?? 0} in / ${stats.assistantMessages ?? 0} out` +
|
|
188
196
|
` (${stats.toolCalls ?? 0} tool calls)`,
|
|
189
|
-
|
|
190
|
-
|
|
197
|
+
`- Usage: ${formatTokens(stats)}`,
|
|
198
|
+
`- Session: \`${stats.sessionId ?? "?"}\``,
|
|
191
199
|
].join("\n");
|
|
192
200
|
}
|
|
193
201
|
|
|
@@ -217,7 +225,7 @@ export const bridgeCommands = {
|
|
|
217
225
|
description: "List bridge commands",
|
|
218
226
|
async run(ctx) {
|
|
219
227
|
const mine = Object.entries(bridgeCommands).map(
|
|
220
|
-
([name, def]) =>
|
|
228
|
+
([name, def]) => `- \`/${name}\` — ${def.description}`,
|
|
221
229
|
);
|
|
222
230
|
/** @type {string[]} */
|
|
223
231
|
let piCommands = [];
|
|
@@ -225,17 +233,22 @@ export const bridgeCommands = {
|
|
|
225
233
|
const commands = await ctx.rpc.getCommands();
|
|
226
234
|
piCommands = commands.map(
|
|
227
235
|
(/** @type {{name?: string, description?: string}} */ c) =>
|
|
228
|
-
|
|
236
|
+
`- \`/${c.name}\`${c.description ? ` — ${c.description}` : ""}`,
|
|
229
237
|
);
|
|
230
238
|
} catch {
|
|
231
239
|
/* pi unavailable: bridge commands still listed */
|
|
232
240
|
}
|
|
233
241
|
const parts = [
|
|
234
|
-
[
|
|
242
|
+
[
|
|
243
|
+
"**Bridge commands**",
|
|
244
|
+
"",
|
|
245
|
+
...mine,
|
|
246
|
+
"- `!` (bare) — quick interrupt",
|
|
247
|
+
].join("\n"),
|
|
235
248
|
];
|
|
236
249
|
if (piCommands.length > 0) {
|
|
237
250
|
parts.push(
|
|
238
|
-
["Pi commands (sent as prompts)
|
|
251
|
+
["**Pi commands** (sent as prompts)", "", ...piCommands].join("\n"),
|
|
239
252
|
);
|
|
240
253
|
}
|
|
241
254
|
parts.push("Anything else is sent to the agent as a prompt.");
|
|
@@ -274,7 +287,7 @@ export const bridgeCommands = {
|
|
|
274
287
|
const target = resolveCwdTarget(ctx.workdir, args);
|
|
275
288
|
if (!target) {
|
|
276
289
|
ctx.log?.(`pi-lxmf: /cd refused: ${args} not under workdir`);
|
|
277
|
-
return `⚠️ /cd refused: "${args}" is not a directory under
|
|
290
|
+
return `⚠️ /cd refused: "${args}" is not a directory under \`${ctx.workdir}\`.`;
|
|
278
291
|
}
|
|
279
292
|
if (target === ctx.getBridgeInfo().cwd) {
|
|
280
293
|
return `Already in ${target}.`;
|
|
@@ -331,7 +344,7 @@ export const bridgeCommands = {
|
|
|
331
344
|
const levels = await ctx.rpc.getAvailableThinkingLevels();
|
|
332
345
|
const level = (args || "").toLowerCase();
|
|
333
346
|
if (!level || !levels.includes(level)) {
|
|
334
|
-
return `Thinking levels: ${levels.join(", ")} (current model).`;
|
|
347
|
+
return `Thinking levels: ${levels.map((l) => `\`${l}\``).join(", ")} (current model).`;
|
|
335
348
|
}
|
|
336
349
|
await ctx.rpc.setThinkingLevel(level);
|
|
337
350
|
return `Thinking level set to ${level}.`;
|
package/src/lxmf.js
CHANGED
|
@@ -53,6 +53,20 @@ export function isUnknownIdentityError(e) {
|
|
|
53
53
|
return e instanceof Error && UNKNOWN_IDENTITY_MESSAGE.test(e.message);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Builds the fields map signaling the outbound message content format
|
|
58
|
+
* (FIELD_RENDERER, SPEC §5.9.4): pi's replies are Markdown, and clients
|
|
59
|
+
* such as Sideband only render them as such when the field says so —
|
|
60
|
+
* without it they show raw `**bold**`/backtick fences as plain text.
|
|
61
|
+
*
|
|
62
|
+
* @returns {Map<number, number>} `{FIELD_RENDERER: RENDERER_MARKDOWN}`
|
|
63
|
+
*/
|
|
64
|
+
export function contentFields() {
|
|
65
|
+
const fields = new Map();
|
|
66
|
+
fields.set(LXMFConstants.FIELD_RENDERER, LXMFConstants.RENDERER_MARKDOWN);
|
|
67
|
+
return fields;
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
/**
|
|
57
71
|
* Waits until `transport` can recall the identity for `destinationHash`,
|
|
58
72
|
* soliciting it first: a path request makes the destination itself (or any
|
|
@@ -526,6 +540,7 @@ export async function startLxmf(config, options = {}) {
|
|
|
526
540
|
sourceHash: /** @type {Uint8Array} */ (deliveryDest.destinationHash),
|
|
527
541
|
destinationHash: fromHex(destinationHex),
|
|
528
542
|
content,
|
|
543
|
+
fields: contentFields(),
|
|
529
544
|
...(i === 0 && sendOptions.title ? { title: sendOptions.title } : {}),
|
|
530
545
|
});
|
|
531
546
|
await sendWithRetry(message, sendOptions.link);
|