@danypops/papyrus 0.33.2 → 0.33.3

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.
@@ -164,7 +164,7 @@ export function normalizeJsonEncodedField(params: Record<string, unknown>, key:
164
164
  }
165
165
 
166
166
  /** Resolves every {nameKey -> idKey} pair present and not already satisfied by an explicit id, in place. */
167
- async function resolveNameFields(
167
+ export async function resolveNameFields(
168
168
  params: Record<string, unknown>,
169
169
  fields: ReadonlyArray<{ nameKey: string; idKey: string; listOperation: OperationName; baseRequest: Record<string, unknown> }>,
170
170
  ): Promise<void> {
@@ -474,7 +474,7 @@ export function registerNotesTool(pi: ExtensionAPI): void {
474
474
  pi.registerTool({
475
475
  name: "notes",
476
476
  label: "Notes",
477
- description: "Deferred human-intent inbox. ACTIONS: capture, list, show, 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. Archive requires an explicit disposition. PREFER `name` (the note's exact title) over `id` for show/consume/promote/archive -- id is a backend implementation detail, resolved from name automatically.",
477
+ description: "Deferred human-intent inbox. ACTIONS: capture, list, show, 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. PREFER `name` (the note's exact title) over `id` for show/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).",
478
478
  parameters: Type.Object({
479
479
  action: Type.String(),
480
480
  id: Type.Optional(Type.String()),
@@ -485,6 +485,7 @@ export function registerNotesTool(pi: ExtensionAPI): void {
485
485
  text: Type.Optional(Type.String()),
486
486
  limit: Type.Optional(Type.Number()),
487
487
  target_id: Type.Optional(Type.String()),
488
+ target_name: Type.Optional(Type.String()),
488
489
  disposition: Type.Optional(Type.Union(NOTE_DISPOSITIONS.map((value) => Type.Literal(value)))),
489
490
  reason: Type.Optional(Type.String()),
490
491
  session_id: Type.Optional(Type.String()),
@@ -497,7 +498,11 @@ export function registerNotesTool(pi: ExtensionAPI): void {
497
498
  const params: Record<string, unknown> = { ...rawParams };
498
499
  const action = params.action;
499
500
  const baseRequest = { project_root: params.project_root ?? ctx.cwd, actor: "agent", source: "notes-tool" };
500
- await resolveNameFields(params, [{ nameKey: "name", idKey: "id", listOperation: "notes.list", baseRequest }]);
501
+ await resolveNameFields(params, [
502
+ { nameKey: "name", idKey: "id", listOperation: "notes.list", baseRequest },
503
+ // Kind-agnostic: a promotion target can be a task, doc, rule, or skill, so this searches every kind rather than only notes.
504
+ { nameKey: "target_name", idKey: "target_id", listOperation: "artifact.query", baseRequest },
505
+ ]);
501
506
  const request = { ...params, ...baseRequest };
502
507
  if (action === "capture") {
503
508
  const artifact = await callService<Record<string, unknown>, Artifact>("notes.capture", request);
@@ -22,7 +22,7 @@ import type { Artifact } from "../../src/domain/artifact.ts";
22
22
  import type { GateResult } from "../../src/domain/gate.ts";
23
23
  import { formatMetadata } from "./artifact-format.ts";
24
24
  import { callService } from "./service-client.ts";
25
- import { registerDomainTools } from "./domain-tools.ts";
25
+ import { registerDomainTools, resolveNameFields } from "./domain-tools.ts";
26
26
  import { ensureTypingCourtesyTracking, isLiveAskPending } from "./discuss-ask-view.ts";
27
27
  import { PLAYBOOK_BRIDGE_MAX_PLAYBOOKS, registerPlaybookBridge } from "./playbook-bridge.ts";
28
28
  import type { TaskGraph, TaskStatus } from "../../src/task-service.ts";
@@ -331,12 +331,15 @@ export default async function (pi: ExtensionAPI) {
331
331
  "ACTIONS: link (from+relation+to), unlink (from+relation+to — idempotent, no error if already absent; for Task depends_on/contains prefer the tasks tool's undepend/uncontain), " +
332
332
  "tree (id → bounded BFS subgraph), " +
333
333
  "history (who did what, when — requires id, actor, or session_id). " +
334
- "status (id+status) exists at the protocol level but is refused for every kind with its own lifecycle (Doc/Rule/Skill/Playbook/Task/Note all reject it) -- use that kind's own domain tool for status changes (docs.activate, rules.enable, tasks.start, etc), never this.",
334
+ "status (id+status) exists at the protocol level but is refused for every kind with its own lifecycle (Doc/Rule/Skill/Playbook/Task/Note all reject it) -- use that kind's own domain tool for status changes (docs.activate, rules.enable, tasks.start, etc), never this. " +
335
+ "PREFER `from_name`/`to_name` over `from`/`to` for link/unlink -- both are backend implementation details, resolved from name automatically, searching across every kind since either end of an edge can be any artifact.",
335
336
  parameters: Type.Object({
336
337
  action: Type.String({ description: "link | unlink | tree | status | history" }),
337
338
  from: Type.Optional(Type.String()),
339
+ from_name: Type.Optional(Type.String()),
338
340
  relation: Type.Optional(Type.String()),
339
341
  to: Type.Optional(Type.String()),
342
+ to_name: Type.Optional(Type.String()),
340
343
  id: Type.Optional(Type.String()),
341
344
  status: Type.Optional(Type.String()),
342
345
  depth: Type.Optional(Type.Number({ description: "tree traversal depth; bounded by a hard ceiling" })),
@@ -348,18 +351,26 @@ export default async function (pi: ExtensionAPI) {
348
351
  }),
349
352
  renderCall(args, theme) { return renderPapyrusToolCall("Artifact graph", args, theme); },
350
353
  renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
351
- async execute(_id, params, _signal, _onUpdate, _ctx) {
354
+ async execute(_id, rawParams, _signal, _onUpdate, _ctx) {
352
355
  try {
356
+ const params: Record<string, unknown> = { ...rawParams };
357
+ if (params.action === "link" || params.action === "unlink") {
358
+ // Kind-agnostic: either end of an edge can be a task, doc, rule, skill, or playbook.
359
+ await resolveNameFields(params, [
360
+ { nameKey: "from_name", idKey: "from", listOperation: "artifact.query", baseRequest: {} },
361
+ { nameKey: "to_name", idKey: "to", listOperation: "artifact.query", baseRequest: {} },
362
+ ]);
363
+ }
353
364
  if (params.action === "link") {
354
- await callService("graph.link", { from: params.from!, relation: params.relation!, to: params.to! });
355
- const names = await artifactNamesById([params.from!, params.to!]);
356
- const output = `Linked "${names.get(params.from!) ?? "unknown artifact"}" --${params.relation}--> "${names.get(params.to!) ?? "unknown artifact"}"`;
365
+ await callService("graph.link", { from: params.from as string, relation: params.relation as string, to: params.to as string });
366
+ const names = await artifactNamesById([params.from as string, params.to as string]);
367
+ const output = `Linked "${names.get(params.from as string) ?? "unknown artifact"}" --${params.relation}--> "${names.get(params.to as string) ?? "unknown artifact"}"`;
357
368
  return text(output, createPreviewDetails("graph.link", "Artifact relationship", output));
358
369
  }
359
370
  if (params.action === "unlink") {
360
- const result = await callService<Record<string, unknown>, { removed: boolean }>("graph.unlink", { from: params.from!, relation: params.relation!, to: params.to! });
361
- const names = await artifactNamesById([params.from!, params.to!]);
362
- const relationship = `"${names.get(params.from!) ?? "unknown artifact"}" --${params.relation}--> "${names.get(params.to!) ?? "unknown artifact"}"`;
371
+ const result = await callService<Record<string, unknown>, { removed: boolean }>("graph.unlink", { from: params.from as string, relation: params.relation as string, to: params.to as string });
372
+ const names = await artifactNamesById([params.from as string, params.to as string]);
373
+ const relationship = `"${names.get(params.from as string) ?? "unknown artifact"}" --${params.relation}--> "${names.get(params.to as string) ?? "unknown artifact"}"`;
363
374
  const output = result.removed ? `Unlinked ${relationship}` : `No such relationship: ${relationship}`;
364
375
  return text(output, createPreviewDetails("graph.unlink", "Artifact relationship", output));
365
376
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.33.2",
3
+ "version": "0.33.3",
4
4
  "description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],