@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.18",
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",