@danypops/pi-papyrus 0.38.1 → 0.38.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.
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import type { AgentToolUpdateCallback, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import {
|
|
4
|
-
NOTE_DISPOSITIONS,
|
|
5
4
|
PROOF_TYPES,
|
|
6
5
|
readDiscussionExtra,
|
|
7
6
|
type Artifact,
|
|
8
7
|
type DiscussionAndRounds,
|
|
9
8
|
type DiscussionRound,
|
|
10
9
|
type GateResult,
|
|
11
|
-
type NoteHistoryPage,
|
|
12
10
|
type OperationName,
|
|
13
11
|
type WorkflowRunResult,
|
|
14
12
|
type TaskCompletion,
|
|
@@ -491,69 +489,9 @@ export function registerTasksTool(pi: ExtensionAPI): void {
|
|
|
491
489
|
});
|
|
492
490
|
}
|
|
493
491
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
label: "Notes",
|
|
498
|
-
description: "Deferred human-intent inbox. ACTIONS: capture, list, show, history, consume, promote, archive. Capture stores a request without creating work. Consume marks it considered. To promote, first create the resulting Task, Doc, Rule, or Skill through its domain tool, then link it with target_id (or target_name). Archive requires an explicit disposition. history returns this note's own real append-only event log (captured/consumed/promoted/archived), not the generic cross-kind graph.history. PREFER `name` (the note's exact title) over `id` for show/history/consume/promote/archive, and `target_name` over `target_id` for promote -- all are backend implementation details, resolved from name automatically (target_name searches across every kind, since a promotion target can be a task, doc, rule, or skill).",
|
|
499
|
-
parameters: Type.Object({
|
|
500
|
-
action: Type.String(),
|
|
501
|
-
id: Type.Optional(Type.String()),
|
|
502
|
-
name: Type.Optional(Type.String()),
|
|
503
|
-
body: Type.Optional(Type.String()),
|
|
504
|
-
title: Type.Optional(Type.String()),
|
|
505
|
-
status: Type.Optional(Type.Union([Type.Literal("draft"), Type.Literal("active"), Type.Literal("archived")])),
|
|
506
|
-
text: Type.Optional(Type.String()),
|
|
507
|
-
limit: Type.Optional(Type.Number()),
|
|
508
|
-
target_id: Type.Optional(Type.String()),
|
|
509
|
-
target_name: Type.Optional(Type.String()),
|
|
510
|
-
disposition: Type.Optional(Type.Union(NOTE_DISPOSITIONS.map((value) => Type.Literal(value)))),
|
|
511
|
-
reason: Type.Optional(Type.String()),
|
|
512
|
-
session_id: Type.Optional(Type.String()),
|
|
513
|
-
project_root: Type.Optional(Type.String()),
|
|
514
|
-
}),
|
|
515
|
-
renderCall(args, theme) { return renderPapyrusToolCall("Notes", args, theme); },
|
|
516
|
-
renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
|
|
517
|
-
async execute(_id, rawParams, _signal, _onUpdate, ctx) {
|
|
518
|
-
try {
|
|
519
|
-
const params: Record<string, unknown> = { ...rawParams };
|
|
520
|
-
const action = params.action;
|
|
521
|
-
const baseRequest = { project_root: params.project_root ?? ctx.cwd, actor: "agent", source: "notes-tool" };
|
|
522
|
-
await resolveNameFields(params, [
|
|
523
|
-
{ nameKey: "name", idKey: "id", listOperation: "notes.list", baseRequest },
|
|
524
|
-
// Kind-agnostic: a promotion target can be a task, doc, rule, or skill, so this searches every kind rather than only notes.
|
|
525
|
-
{ nameKey: "target_name", idKey: "target_id", listOperation: "artifact.query", baseRequest },
|
|
526
|
-
]);
|
|
527
|
-
const request = { ...params, ...baseRequest };
|
|
528
|
-
if (action === "capture") {
|
|
529
|
-
const artifact = await callService<Record<string, unknown>, Artifact>("notes.capture", request);
|
|
530
|
-
return text(`Captured note ${artifactLine(artifact)}`, createArtifactDetails("notes.capture", artifact));
|
|
531
|
-
}
|
|
532
|
-
if (action === "list") {
|
|
533
|
-
const rows = await callService<Record<string, unknown>, Artifact[]>("notes.list", request);
|
|
534
|
-
return text(rows.length ? artifactLines(rows).join("\n") : "No open notes.", createArtifactListDetails("notes.list", rows));
|
|
535
|
-
}
|
|
536
|
-
if (action === "show") {
|
|
537
|
-
const artifact = await callService<Record<string, unknown>, Artifact>("notes.show", request);
|
|
538
|
-
return text(`${artifactLine(artifact)}\n\n${artifact.body}`, createArtifactDetails("notes.show", artifact));
|
|
539
|
-
}
|
|
540
|
-
if (action === "history") {
|
|
541
|
-
const page = await callService<Record<string, unknown>, NoteHistoryPage>("notes.history", request);
|
|
542
|
-
const lines = page.events.map((event) => `${event.occurredAt} ${event.type} · ${event.actor}/${event.source}${event.relatedId ? ` · ${event.relatedId}` : ""}${event.disposition ? ` · ${event.disposition}` : ""}${event.reason ? ` · ${event.reason}` : ""}`);
|
|
543
|
-
const output = lines.join("\n") || "No recorded history for this note.";
|
|
544
|
-
return text(output, createPreviewDetails("notes.history", "Note history", output));
|
|
545
|
-
}
|
|
546
|
-
const operations = { consume: "notes.consume", promote: "notes.promote", archive: "notes.archive" } as const;
|
|
547
|
-
const operation = operations[action as keyof typeof operations];
|
|
548
|
-
if (!operation) throw new Error(`unknown notes action: ${action}`);
|
|
549
|
-
const artifact = await callService<Record<string, unknown>, Artifact>(operation, request);
|
|
550
|
-
return text(`${action}: ${artifactLine(artifact)}`, createArtifactDetails(operation, artifact));
|
|
551
|
-
} catch (error) {
|
|
552
|
-
throw new Error(`notes failed: ${error instanceof Error ? error.message : error}`);
|
|
553
|
-
}
|
|
554
|
-
},
|
|
555
|
-
});
|
|
556
|
-
}
|
|
492
|
+
// notes.* is registered as a real Vehicle (see ../vehicle-notes-client.ts and index.ts),
|
|
493
|
+
// not a hand-rolled pi.registerTool() -- the first domain migrated off the
|
|
494
|
+
// action-dispatch mega-tool pattern this file's other tools still use.
|
|
557
495
|
|
|
558
496
|
export function registerDocsTool(pi: ExtensionAPI): void {
|
|
559
497
|
pi.registerTool({
|
|
@@ -943,7 +881,6 @@ export function registerDiscussTool(pi: ExtensionAPI): void {
|
|
|
943
881
|
/** Thin orchestrator: each domain's tool is independently navigable/testable via its own registerXTool function. */
|
|
944
882
|
export function registerDomainTools(pi: ExtensionAPI): void {
|
|
945
883
|
registerTasksTool(pi);
|
|
946
|
-
registerNotesTool(pi);
|
|
947
884
|
registerDocsTool(pi);
|
|
948
885
|
registerRulesTool(pi);
|
|
949
886
|
registerPlaybooksTool(pi);
|
package/extension/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { formatMetadata } from "./artifact-format.ts";
|
|
|
28
28
|
import { callService, subscribeTaskPushChannel } from "./service-client.ts";
|
|
29
29
|
import type { PushChannelClient } from "@danypops/daemon-kit/pi-client";
|
|
30
30
|
import { registerDomainTools, resolveNameFields } from "./domain-tools.ts";
|
|
31
|
+
import { registerNotesVehicle } from "./vehicle-notes-client.ts";
|
|
31
32
|
import { BoundedPoll } from "./bounded-poll.ts";
|
|
32
33
|
import { renderNoteWidgetLines } from "./note-widget.ts";
|
|
33
34
|
import { ensureTypingCourtesyTracking, isLiveAskPending } from "./discuss-ask-view.ts";
|
|
@@ -677,6 +678,18 @@ export default async function (pi: ExtensionAPI) {
|
|
|
677
678
|
// ── Task widget (TodoOverlay pattern: factory form, requestRender) ──
|
|
678
679
|
|
|
679
680
|
pi.on("session_start", async (_event, ctx) => {
|
|
681
|
+
// registerVehicleTools() (which registerNotesVehicle wraps) needs
|
|
682
|
+
// pi.getAllTools()/getActiveTools()/setActiveTools() -- Pi's extension
|
|
683
|
+
// runtime only finishes initializing after every extension's top-level
|
|
684
|
+
// factory (this one included) has resolved, so calling it directly from
|
|
685
|
+
// there throws "Extension runtime not initialized" (previously silently
|
|
686
|
+
// swallowed by registerNotesVehicle's own daemon-unreachable try/catch,
|
|
687
|
+
// making every projected notes.* tool invisible to the model with zero
|
|
688
|
+
// visible sign why -- confirmed live in the identical pi-tickets bug).
|
|
689
|
+
// session_start fires only after that initialization completes, and Pi
|
|
690
|
+
// awaits every session_start handler before the model's first turn, so
|
|
691
|
+
// registering here is both safe and still visible on turn one.
|
|
692
|
+
await registerNotesVehicle(pi);
|
|
680
693
|
// Registers this session's identity with the daemon as early as possible -- before any
|
|
681
694
|
// Focus-mutating call could plausibly happen -- shrinking (not eliminating; see
|
|
682
695
|
// domain/session-identity.ts) the first-touch race window. Best-effort: the daemon may be
|
|
@@ -731,7 +744,9 @@ export default async function (pi: ExtensionAPI) {
|
|
|
731
744
|
if (event.toolName.startsWith("papyrus_") || event.toolName === "tasks") {
|
|
732
745
|
await overlay?.refresh();
|
|
733
746
|
}
|
|
734
|
-
|
|
747
|
+
// notes.* projects to notes_capture/notes_list/notes_show/... (see
|
|
748
|
+
// registerNotesVehicle) -- not a single "notes" tool name anymore.
|
|
749
|
+
if (event.toolName.startsWith("notes_")) {
|
|
735
750
|
await noteOverlay?.refresh();
|
|
736
751
|
}
|
|
737
752
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { connectPushChannel, createRetryingClient, type PushChannelClient, type PushChannelState, type RetryingClient } from "@danypops/daemon-kit/pi-client";
|
|
2
|
-
import { connectPapyrusClient, resolvePushChannelTarget, type OperationName, type PapyrusClient } from "@danypops/papyrus";
|
|
2
|
+
import { connectPapyrusClient, resolvePushChannelTarget, resolveVehicleClientTarget, type OperationName, type PapyrusClient, type VehicleClientTarget } from "@danypops/papyrus";
|
|
3
3
|
|
|
4
4
|
type ClientConnector = () => Promise<PapyrusClient>;
|
|
5
5
|
|
|
@@ -37,6 +37,27 @@ export function resetPushChannelTargetResolverForTests(): void {
|
|
|
37
37
|
pushChannelTargetResolver = resolvePushChannelTarget;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
let vehicleClientTargetResolver: typeof resolveVehicleClientTarget = resolveVehicleClientTarget;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Defaults to the real daemonStateDir() -- every test that exercises the full extension
|
|
44
|
+
* entrypoint (registerPapyrus(api)), not just registerDomainTools, must override this
|
|
45
|
+
* first, the same way mockService already overrides setPapyrusClientConnectorForTests,
|
|
46
|
+
* or a hermetic unit test can silently start depending on whatever real Papyrus daemon
|
|
47
|
+
* handle happens to exist on the machine running it.
|
|
48
|
+
*/
|
|
49
|
+
export function setVehicleClientTargetResolverForTests(value: () => VehicleClientTarget | undefined): void {
|
|
50
|
+
vehicleClientTargetResolver = value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resetVehicleClientTargetResolverForTests(): void {
|
|
54
|
+
vehicleClientTargetResolver = resolveVehicleClientTarget;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function currentVehicleClientTarget(): VehicleClientTarget | undefined {
|
|
58
|
+
return vehicleClientTargetResolver();
|
|
59
|
+
}
|
|
60
|
+
|
|
40
61
|
/**
|
|
41
62
|
* Subscribes to the daemon's "tasks" push topic so a widget can refresh the moment
|
|
42
63
|
* a mutation happens, instead of waiting for its next poll tick. Returns undefined
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers notes.* as a real Vehicle instead of a hand-rolled `pi.registerTool()`
|
|
3
|
+
* mega-tool -- see @danypops/papyrus's src/vehicle/notes-vehicle.ts for the
|
|
4
|
+
* VehicleRegistry side. Same daemon, same handle file, same Bearer token every
|
|
5
|
+
* other Papyrus RPC call already uses (resolveVehicleClientTarget mirrors
|
|
6
|
+
* resolvePushChannelTarget's own resolution).
|
|
7
|
+
*
|
|
8
|
+
* Papyrus's daemon is expected to already be running as an installed service
|
|
9
|
+
* (see `packed install-service`), not auto-spawned on first use -- so unlike
|
|
10
|
+
* registerVehicleTools' README example, failure here (daemon not started yet,
|
|
11
|
+
* stale handle) is tolerated the same silent-degrade way
|
|
12
|
+
* subscribeTaskPushChannel already tolerates it, rather than letting a
|
|
13
|
+
* daemon-not-running condition abort the rest of extension setup. There is
|
|
14
|
+
* no retry-on-later-connect for a tool that was never registered at all --
|
|
15
|
+
* Pi has no way to add one after the fact outside the initial registration
|
|
16
|
+
* flow.
|
|
17
|
+
*
|
|
18
|
+
* Resolves the target through service-client.ts's currentVehicleClientTarget()
|
|
19
|
+
* (test-injectable, see setVehicleClientTargetResolverForTests), never
|
|
20
|
+
* @danypops/papyrus's resolveVehicleClientTarget() directly -- this runs from
|
|
21
|
+
* the bare extension entrypoint on every registerPapyrus(api) call, so an
|
|
22
|
+
* un-injected default would resolve the real daemonStateDir() in any test
|
|
23
|
+
* exercising the full entrypoint, not just ones about notes.
|
|
24
|
+
*/
|
|
25
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import { RemoteVehicleClient } from "@danypops/vehicle-client/http";
|
|
27
|
+
import { registerVehicleTools } from "@danypops/vehicle-client-pi";
|
|
28
|
+
import { currentVehicleClientTarget } from "./service-client.ts";
|
|
29
|
+
|
|
30
|
+
export async function registerNotesVehicle(pi: ExtensionAPI): Promise<void> {
|
|
31
|
+
const target = currentVehicleClientTarget();
|
|
32
|
+
if (!target) return;
|
|
33
|
+
try {
|
|
34
|
+
const client = new RemoteVehicleClient({ baseUrl: target.baseUrl, token: target.token });
|
|
35
|
+
await registerVehicleTools(pi, client, {
|
|
36
|
+
permissions: ["notes:read", "notes:write"],
|
|
37
|
+
principal: { id: "pi-papyrus" },
|
|
38
|
+
});
|
|
39
|
+
} catch {
|
|
40
|
+
// Daemon state is stale/unreachable -- degrade silently, matching
|
|
41
|
+
// subscribeTaskPushChannel's own tolerance for the same condition.
|
|
42
|
+
}
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-papyrus",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.2",
|
|
4
4
|
"description": "Pi host extension for Papyrus: native tools, TUI panels, and context injection over the daemon-backed graph store",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": ["pi-package"],
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@danypops/daemon-kit": "^0.10.0",
|
|
21
21
|
"@danypops/papyrus": "^0.38.0",
|
|
22
|
+
"@danypops/vehicle-core": "^0.1.0",
|
|
23
|
+
"@danypops/vehicle-client": "^0.1.1",
|
|
24
|
+
"@danypops/vehicle-client-pi": "^0.1.5",
|
|
22
25
|
"beautiful-mermaid": "1.1.3"
|
|
23
26
|
},
|
|
24
27
|
"devDependencies": {
|