@fiale-plus/pi-rogue-bundle 0.1.18 → 0.1.19
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.
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it } from "vitest";
|
|
@@ -80,6 +80,7 @@ describe("context broker beta enablement", () => {
|
|
|
80
80
|
"brief",
|
|
81
81
|
"lookup",
|
|
82
82
|
"pin",
|
|
83
|
+
"export",
|
|
83
84
|
"prune",
|
|
84
85
|
]);
|
|
85
86
|
});
|
|
@@ -218,6 +219,38 @@ describe("context broker beta enablement", () => {
|
|
|
218
219
|
expect(Buffer.byteLength(payload, "utf8")).toBeLessThanOrEqual(80);
|
|
219
220
|
});
|
|
220
221
|
|
|
222
|
+
it("full payload export path writes the full artifact payload", async () => {
|
|
223
|
+
const { pi, handlers, commands } = createPiMock();
|
|
224
|
+
registerContextBrokerBeta(pi, { lookupBytes: 80, searchBytes: 50 });
|
|
225
|
+
const { ctx, notifications } = createCtx();
|
|
226
|
+
const payload = "payload_" + "x".repeat(120) + "::END";
|
|
227
|
+
|
|
228
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
229
|
+
await runHandlers(handlers, "tool_result", {
|
|
230
|
+
type: "tool_result",
|
|
231
|
+
toolCallId: "call-export",
|
|
232
|
+
toolName: "bash",
|
|
233
|
+
input: { command: "printf payload" },
|
|
234
|
+
content: [{ type: "text", text: payload }],
|
|
235
|
+
isError: false,
|
|
236
|
+
}, ctx);
|
|
237
|
+
|
|
238
|
+
const exportCompletion = commands.get("context").getArgumentCompletions("export ")?.[0];
|
|
239
|
+
expect(exportCompletion.value.startsWith("export ctx://")).toBe(true);
|
|
240
|
+
const exportHandle = exportCompletion.value.replace(/^export /, "");
|
|
241
|
+
|
|
242
|
+
await commands.get("context").handler(`export ${exportHandle}`, ctx);
|
|
243
|
+
|
|
244
|
+
const message = notifications.at(-1)?.message ?? "";
|
|
245
|
+
const exportPath = message.split(" to ").at(-1) ?? "";
|
|
246
|
+
expect(exportPath).toContain("pi-context-broker-export-");
|
|
247
|
+
expect(exportPath).toMatch(/\.txt$/);
|
|
248
|
+
const exportedPayload = readFileSync(exportPath, "utf8");
|
|
249
|
+
expect(exportedPayload).toContain("tool=bash");
|
|
250
|
+
expect(exportedPayload).toContain(payload);
|
|
251
|
+
rmSync(exportPath);
|
|
252
|
+
});
|
|
253
|
+
|
|
221
254
|
it("text search lookup returns a smaller byte-clipped excerpt", async () => {
|
|
222
255
|
const { pi, handlers, commands } = createPiMock();
|
|
223
256
|
registerContextBrokerBeta(pi, { lookupBytes: 80, searchBytes: 50 });
|
|
@@ -240,6 +273,31 @@ describe("context broker beta enablement", () => {
|
|
|
240
273
|
expect(Buffer.byteLength(payload, "utf8")).toBeLessThanOrEqual(50);
|
|
241
274
|
});
|
|
242
275
|
|
|
276
|
+
it("sanitizes control characters in context command lookup output", async () => {
|
|
277
|
+
const { pi, handlers, commands } = createPiMock();
|
|
278
|
+
registerContextBrokerBeta(pi);
|
|
279
|
+
const { ctx, notifications } = createCtx();
|
|
280
|
+
const rawPayload = `${"SAFE"}\u0000${"\x1B"}[31mBLOCK\u0000`;
|
|
281
|
+
|
|
282
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
283
|
+
await runHandlers(handlers, "tool_result", {
|
|
284
|
+
type: "tool_result",
|
|
285
|
+
toolCallId: "call-control",
|
|
286
|
+
toolName: "bash",
|
|
287
|
+
input: { command: "echo control" },
|
|
288
|
+
content: [{ type: "text", text: rawPayload }],
|
|
289
|
+
isError: false,
|
|
290
|
+
}, ctx);
|
|
291
|
+
|
|
292
|
+
const completion = commands.get("context").getArgumentCompletions("lookup ")?.[0];
|
|
293
|
+
await commands.get("context").handler(completion?.value ?? "", ctx);
|
|
294
|
+
|
|
295
|
+
const message = notifications.at(-1)?.message ?? "";
|
|
296
|
+
expect(message).toContain("\\u0000");
|
|
297
|
+
expect(message).toContain("\\u001b");
|
|
298
|
+
expect(message).not.toContain(String.fromCharCode(0));
|
|
299
|
+
});
|
|
300
|
+
|
|
243
301
|
it("context_lookup tool dereferences handles for exact evidence", async () => {
|
|
244
302
|
const { pi, handlers, commands, tools } = createPiMock();
|
|
245
303
|
registerContextBrokerBeta(pi, { lookupBytes: 500 });
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
2
5
|
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
3
6
|
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
4
7
|
import { Type } from "typebox";
|
|
@@ -69,6 +72,20 @@ function toText(value: unknown): string {
|
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
|
|
75
|
+
function sanitizeForPrompt(text: string): string {
|
|
76
|
+
return String(text).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, (char) => `\\x${char.charCodeAt(0).toString(16).padStart(2, "0")}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function renderLookupOutput(item: ContextArtifact, payloadLimit: number): string {
|
|
80
|
+
return [
|
|
81
|
+
sanitizeForPrompt(item.handle),
|
|
82
|
+
`tier=${item.tier} kind=${item.kind} bytes=${item.bytes}`,
|
|
83
|
+
`summary=${sanitizeForPrompt(item.summary)}`,
|
|
84
|
+
"payload:",
|
|
85
|
+
truncateUtf8(sanitizeForPrompt(item.payload), payloadLimit),
|
|
86
|
+
].join("\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
72
89
|
function truncateUtf8(text: string, maxBytes: number): string {
|
|
73
90
|
const limit = Math.max(0, Math.floor(maxBytes));
|
|
74
91
|
const totalBytes = Buffer.byteLength(text, "utf8");
|
|
@@ -349,10 +366,11 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
349
366
|
{ value: "brief", label: "brief", description: "Show the bounded broker brief" },
|
|
350
367
|
{ value: "lookup ", label: "lookup", description: "Lookup by ctx:// handle or current-session text" },
|
|
351
368
|
{ value: "pin ", label: "pin", description: "Pin an artifact by ctx:// handle or id" },
|
|
369
|
+
{ value: "export ", label: "export", description: "Export full payload for a ctx:// handle or id" },
|
|
352
370
|
{ value: "prune", label: "prune", description: "Run TTL/cap pruning now" },
|
|
353
371
|
];
|
|
354
372
|
|
|
355
|
-
function artifactCompletions(action: "lookup" | "pin", query: string): AutocompleteItem[] {
|
|
373
|
+
function artifactCompletions(action: "lookup" | "pin" | "export", query: string): AutocompleteItem[] {
|
|
356
374
|
const needle = query.trim().toLowerCase();
|
|
357
375
|
return broker.lookup({ sessionId: activeSessionId, limit: 10 })
|
|
358
376
|
.filter((artifact) => {
|
|
@@ -380,7 +398,7 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
380
398
|
return items.length ? items : contextActions;
|
|
381
399
|
}
|
|
382
400
|
|
|
383
|
-
if (action === "lookup" || action === "pin") {
|
|
401
|
+
if (action === "lookup" || action === "pin" || action === "export") {
|
|
384
402
|
const items = artifactCompletions(action, restParts.join(" "));
|
|
385
403
|
return items.length ? items : null;
|
|
386
404
|
}
|
|
@@ -534,18 +552,12 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
534
552
|
limit: Math.min(10, Math.max(1, Math.floor(p.limit ?? (exact ? 1 : 5)))),
|
|
535
553
|
});
|
|
536
554
|
if (!results.length) return textResult("No context artifacts matched. Missing or expired handles should be reported explicitly.");
|
|
537
|
-
return textResult(results.map((item) =>
|
|
538
|
-
item.handle,
|
|
539
|
-
`tier=${item.tier} kind=${item.kind} bytes=${item.bytes}`,
|
|
540
|
-
`summary=${item.summary}`,
|
|
541
|
-
"payload:",
|
|
542
|
-
truncateUtf8(item.payload, exact ? lookupBytes : searchBytes),
|
|
543
|
-
].join("\n")).join("\n\n---\n\n"));
|
|
555
|
+
return textResult(results.map((item) => renderLookupOutput(item, exact ? lookupBytes : searchBytes)).join("\n\n---\n\n"));
|
|
544
556
|
},
|
|
545
557
|
});
|
|
546
558
|
|
|
547
559
|
pi.registerCommand("context", {
|
|
548
|
-
description: "Inspect the beta context broker: status | brief | lookup <handle-or-text> | pin <handle> | prune",
|
|
560
|
+
description: "Inspect the beta context broker: status | brief | lookup <handle-or-text> | pin <handle-or-id> | export <handle-or-id> | prune",
|
|
549
561
|
getArgumentCompletions: contextArgumentCompletions,
|
|
550
562
|
handler: async (args, ctx) => {
|
|
551
563
|
activeSessionId = sessionIdFor(ctx);
|
|
@@ -573,13 +585,7 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
573
585
|
}
|
|
574
586
|
const exact = query.startsWith("ctx://");
|
|
575
587
|
const results = broker.lookup(exact ? { handle: query } : { sessionId: activeSessionId, text: query, limit: 5 });
|
|
576
|
-
ctx.ui.notify(results.length ? results.map((item) =>
|
|
577
|
-
item.handle,
|
|
578
|
-
`kind=${item.kind} bytes=${item.bytes}`,
|
|
579
|
-
`summary=${item.summary}`,
|
|
580
|
-
"payload:",
|
|
581
|
-
truncateUtf8(item.payload, exact ? lookupBytes : searchBytes),
|
|
582
|
-
].join("\n")).join("\n\n---\n\n") : "No context artifacts matched.", "info");
|
|
588
|
+
ctx.ui.notify(results.length ? results.map((item) => renderLookupOutput(item, exact ? lookupBytes : searchBytes)).join("\n\n---\n\n") : "No context artifacts matched.", "info");
|
|
583
589
|
return;
|
|
584
590
|
}
|
|
585
591
|
|
|
@@ -593,13 +599,33 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
593
599
|
return;
|
|
594
600
|
}
|
|
595
601
|
|
|
602
|
+
if (action === "export") {
|
|
603
|
+
if (!query) {
|
|
604
|
+
ctx.ui.notify("Usage: /context export <ctx://handle-or-id>", "warning");
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const exact = query.startsWith("ctx://");
|
|
609
|
+
const artifact = exact ? broker.lookup({ handle: query })[0] : broker.lookup({ id: query })[0];
|
|
610
|
+
if (!artifact) {
|
|
611
|
+
ctx.ui.notify("No artifact matched that handle/id.", "warning");
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const exportDir = mkdtempSync(join(tmpdir(), "pi-context-broker-export-"));
|
|
616
|
+
const exportPath = join(exportDir, `${artifact.id}.txt`);
|
|
617
|
+
writeFileSync(exportPath, artifact.payload, "utf8");
|
|
618
|
+
ctx.ui.notify(`Exported full payload for ${sanitizeForPrompt(artifact.handle)} (${artifact.bytes} bytes) to ${exportPath}`, "info");
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
596
622
|
if (action === "prune") {
|
|
597
623
|
const status = broker.prune();
|
|
598
624
|
ctx.ui.notify(`Pruned. ${status.records} records, ${status.bytes} bytes remain.`, "info");
|
|
599
625
|
return;
|
|
600
626
|
}
|
|
601
627
|
|
|
602
|
-
ctx.ui.notify("Usage: /context status | brief | lookup <handle-or-text> | pin <handle> | prune", "warning");
|
|
628
|
+
ctx.ui.notify("Usage: /context status | brief | lookup <handle-or-text> | pin <handle-or-id> | export <handle-or-id> | prune", "warning");
|
|
603
629
|
},
|
|
604
630
|
});
|
|
605
631
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiale-plus/pi-rogue-bundle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "Public Pi-Rogue bundle for advisor, orchestration, and beta context broker. Single consolidated artefact (leaf releases paused; private packages are bundled here).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|