@danypops/papyrus 0.29.4 → 0.29.6

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.
@@ -95,7 +95,7 @@ export function renderTaskWidgetLines(theme: Theme, projection: TaskWidgetProjec
95
95
  return lines;
96
96
  }
97
97
 
98
- class TaskOverlay {
98
+ export class TaskOverlay {
99
99
  private uiCtx: ExtensionUIContext | undefined;
100
100
  private registered = false;
101
101
  private tui: any | undefined;
@@ -116,6 +116,12 @@ class TaskOverlay {
116
116
  // concurrent agent's focused task never shows as active in this session's widget.
117
117
  setSessionId(sessionId: string): void { this.sessionId = sessionId; }
118
118
 
119
+ /**
120
+ * Never throws: called from several pi.on(...) handlers, some of which (session_compact,
121
+ * session_tree, tool_execution_end) don't wrap it themselves -- Pi's event emitter does not
122
+ * guarantee catching a handler's rejection, so an unguarded throw here would become an
123
+ * unhandled rejection at the call site instead of a stability issue contained to this widget.
124
+ */
119
125
  async refresh(): Promise<void> {
120
126
  if (!this.projectRoot) return;
121
127
  try {
@@ -123,7 +129,11 @@ class TaskOverlay {
123
129
  } catch {
124
130
  this.snapshot = { nodes: [], rootIds: [] };
125
131
  }
126
- this.render();
132
+ try {
133
+ this.render();
134
+ } catch {
135
+ // A rendering bug must not crash the extension host over a best-effort status widget.
136
+ }
127
137
  }
128
138
 
129
139
  private render(): void {
@@ -519,7 +529,7 @@ export default async function (pi: ExtensionAPI) {
519
529
  const usage = ctx.getContextUsage?.();
520
530
  // Real tree (not just the linear current-branch path): surfaces content sitting in an
521
531
  // abandoned /tree branch, which cost real tokens to generate but isn't in context now.
522
- const tree = ctx.sessionManager.getTree() as unknown as SessionTreeNodeLike[];
532
+ const tree = ctx.sessionManager.getTree() as SessionTreeNodeLike[];
523
533
  // buildContextEntries(), NOT getBranch(): getBranch() returns every raw entry on the
524
534
  // current path including everything a real compaction has already summarized away.
525
535
  // A session with 3 real compactions confirmed this made "active" message-history
@@ -528,8 +538,8 @@ export default async function (pi: ExtensionAPI) {
528
538
  // LLM"); buildContextEntries() is the compaction-aware entry list matching what the
529
539
  // LLM actually sees (the latest compaction entry itself, plus kept entries from its
530
540
  // firstKeptEntryId onward, plus everything after -- older summarized entries omitted).
531
- const activeEntryIds = new Set((ctx.sessionManager.buildContextEntries() as unknown as SessionEntryLike[]).map((entry) => entry.id));
532
- const branchEntryIds = new Set((ctx.sessionManager.getBranch() as unknown as SessionEntryLike[]).map((entry) => entry.id));
541
+ const activeEntryIds = new Set((ctx.sessionManager.buildContextEntries() as SessionEntryLike[]).map((entry) => entry.id));
542
+ const branchEntryIds = new Set((ctx.sessionManager.getBranch() as SessionEntryLike[]).map((entry) => entry.id));
533
543
  const messageHistory = buildMessageHistoryTree(tree, activeEntryIds, branchEntryIds);
534
544
  const breakdown = buildContextBreakdown({
535
545
  totalTokens: usage?.tokens ?? null,
@@ -120,7 +120,8 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
120
120
  if (action.type === "graph") { await showTaskGraph(ctx, graph); continue; }
121
121
  if (action.type !== "action" || !action.row) continue;
122
122
 
123
- const node = graph.nodes.find((entry) => entry.task.id === action.row!.id);
123
+ const rowId = action.row.id;
124
+ const node = graph.nodes.find((entry) => entry.task.id === rowId);
124
125
  const active = node?.active === true;
125
126
  const focusStatus = node?.focusStatus;
126
127
  const choices = [
@@ -136,8 +137,8 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
136
137
  const choice = await ctx.ui.select(action.row.title, choices);
137
138
  if (!choice) continue;
138
139
 
139
- if (choice === "Remove dependency" || choice === "Remove from parent") {
140
- const relatedIds = choice === "Remove dependency" ? node!.dependencyIds : node!.parentIds;
140
+ if ((choice === "Remove dependency" || choice === "Remove from parent") && node) {
141
+ const relatedIds = choice === "Remove dependency" ? node.dependencyIds : node.parentIds;
141
142
  const relatedTasks = relatedIds.map((relatedId) => graph.nodes.find((entry) => entry.task.id === relatedId)?.task).filter((task): task is Artifact => task !== undefined);
142
143
  const relatedTitles = taskChoiceLabels(relatedTasks);
143
144
  const selected = await ctx.ui.select(choice === "Remove dependency" ? "Remove which dependency?" : "Remove from which parent?", relatedTitles);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.29.4",
3
+ "version": "0.29.6",
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"],
package/src/cli.ts CHANGED
@@ -112,7 +112,8 @@ const USAGE = `Usage:
112
112
  papyrus skills instantiate <template-id> [--title <title>] [--body <body>] [--status <status>] [--labels-json <json>] [--extra-json <json>] [--json]
113
113
  papyrus skills assign-project <id> [project-root] [--json]
114
114
  papyrus skills update <id> [--title <title>] [--body <body>] [--labels-json <json>] [--json]
115
- papyrus playbooks create --title <title> [--body <body>] [--trigger <text>] [--steps-json <json>] [--tools-json <json>] [--labels-json <json>] [--extra-json <json>] [--arguments-json <json>] [--project-root <path>] [--json]
115
+ papyrus playbooks create --title <title> [--body <body>] [--trigger <text>] [--steps-json <json>] [--tools-json <json>] [--labels-json <json>] [--extra-json <json>] [--arguments-json <json array>] [--project-root <path>] [--json]
116
+ papyrus playbooks invoke <id> [--arguments-json <json object>] [--json]
116
117
  papyrus playbooks list [--status <status>] [--text <query>] [--limit <count>] [--project-root <path>] [--json]
117
118
  papyrus playbooks show <id> [--json]
118
119
  papyrus playbooks invoke <id> [--json]
@@ -201,12 +202,15 @@ function parseJsonStringArrayFlag(value: string | undefined, flag: string): stri
201
202
  return parsed as string[];
202
203
  }
203
204
 
204
- /** Top-level shape only -- element shape (e.g. {name, description?, required?}) is validated server-side. */
205
- function parseJsonArrayFlag(value: string | undefined, flag: string): unknown[] {
205
+ /**
206
+ * No shape assertion here -- unlike every other JSON flag, playbooks --arguments-json is genuinely
207
+ * polymorphic (an array on create, a {name: value} map on invoke), and the two actions share one
208
+ * flag-parsing pass in runPlaybooksCli. The service validates the shape for whichever operation
209
+ * actually receives it.
210
+ */
211
+ function parseJsonAnyFlag(value: string | undefined, flag: string): unknown {
206
212
  if (value === undefined) throw new Error(`${flag} requires a value`);
207
- const parsed = JSON.parse(value) as unknown;
208
- if (!Array.isArray(parsed)) throw new Error(`${flag} must be a JSON array`);
209
- return parsed;
213
+ return JSON.parse(value) as unknown;
210
214
  }
211
215
 
212
216
  function artifactLabel(artifact: CliArtifact): string {
@@ -794,7 +798,7 @@ export async function runPlaybooksCli(args: string[], client: TaskCliClient): Pr
794
798
  let tools: string[] | undefined;
795
799
  let labels: string[] | undefined;
796
800
  let extra: Record<string, unknown> | undefined;
797
- let playbookArguments: unknown[] | undefined;
801
+ let playbookArguments: unknown;
798
802
  let status: string | undefined;
799
803
  let text: string | undefined;
800
804
  let limit: number | undefined;
@@ -809,7 +813,7 @@ export async function runPlaybooksCli(args: string[], client: TaskCliClient): Pr
809
813
  if (argument === "--tools-json") { tools = parseJsonStringArrayFlag(args[++index], "--tools-json"); continue; }
810
814
  if (argument === "--labels-json") { labels = parseJsonStringArrayFlag(args[++index], "--labels-json"); continue; }
811
815
  if (argument === "--extra-json") { extra = parseJsonObjectFlag(args[++index], "--extra-json"); continue; }
812
- if (argument === "--arguments-json") { playbookArguments = parseJsonArrayFlag(args[++index], "--arguments-json"); continue; }
816
+ if (argument === "--arguments-json") { playbookArguments = parseJsonAnyFlag(args[++index], "--arguments-json"); continue; }
813
817
  if (argument === "--status") { status = args[++index]; if (!status) throw new Error("--status requires a value"); continue; }
814
818
  if (argument === "--text") { text = args[++index]; if (text === undefined) throw new Error("--text requires a value"); continue; }
815
819
  if (argument === "--project-root") { playbookProjectRoot = args[++index]; if (!playbookProjectRoot) throw new Error("--project-root requires a value"); continue; }
@@ -850,7 +854,7 @@ export async function runPlaybooksCli(args: string[], client: TaskCliClient): Pr
850
854
  }
851
855
  case "invoke": {
852
856
  if (!id || second) throw new Error("playbooks invoke requires exactly one playbook id");
853
- const invocation = await client.call<Record<string, unknown>, string>("playbooks.invoke", { id });
857
+ const invocation = await client.call<Record<string, unknown>, string>("playbooks.invoke", { id, arguments: playbookArguments });
854
858
  result = invocation;
855
859
  human = invocation;
856
860
  break;
package/src/daemon.ts CHANGED
@@ -52,7 +52,11 @@ export function serveMain(): void {
52
52
  clearInterval(purgeTrashTimer);
53
53
  clearDaemonPort(stateDir);
54
54
  service.close();
55
- void server.stop(true).finally(() => process.exit(0));
55
+ // .finally() re-throws rather than handling a rejection -- catching it first turns a bare
56
+ // unhandled-rejection warning into a real, queryable shutdown-failure log line.
57
+ void server.stop(true)
58
+ .catch((error) => logEvent("error", "server_stop_failed", { message: error instanceof Error ? error.message : String(error) }))
59
+ .finally(() => process.exit(0));
56
60
  };
57
61
  process.on("SIGINT", shutdown);
58
62
  process.on("SIGTERM", shutdown);
package/src/db.ts CHANGED
@@ -12,13 +12,23 @@ import { SQLITE_BUSY_TIMEOUT_MS, SQLITE_SCHEMA_VERSION } from "./constants.ts";
12
12
 
13
13
  const require_ = createRequire(import.meta.url);
14
14
  const IS_BUN = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
15
- const backend = IS_BUN
16
- ? (require_("bun:sqlite") as typeof import("bun:sqlite"))
17
- : (require_("node:sqlite") as unknown as typeof import("bun:sqlite"));
18
15
 
19
- const DatabaseCtor = (
20
- "DatabaseSync" in backend ? (backend as { DatabaseSync: unknown }).DatabaseSync : backend.Database
21
- ) as new (path: string, opts?: { create?: boolean }) => Db;
16
+ /** Bun's bun:sqlite exports Database; Node's node:sqlite exports DatabaseSync -- neither module is
17
+ * actually the other, but both satisfy this shape at the methods Papyrus calls through Db/DbStatement. */
18
+ interface SqliteBackendModule {
19
+ Database?: new (path: string, opts?: { create?: boolean }) => Db;
20
+ DatabaseSync?: new (path: string, opts?: { create?: boolean }) => Db;
21
+ }
22
+
23
+ const backend = require_(IS_BUN ? "bun:sqlite" : "node:sqlite") as SqliteBackendModule;
24
+ // IIFE + explicit return type, not a bare `const DatabaseCtor = backend.DatabaseSync ?? backend.Database`
25
+ // with a following throw-guard: that guard's narrowing wouldn't propagate into openDb() below, a
26
+ // separate function closing over this module-level binding.
27
+ const DatabaseCtor: new (path: string, opts?: { create?: boolean }) => Db = (() => {
28
+ const ctor = backend.DatabaseSync ?? backend.Database;
29
+ if (!ctor) throw new Error("no compatible sqlite backend found (expected bun:sqlite's Database or node:sqlite's DatabaseSync)");
30
+ return ctor;
31
+ })();
22
32
 
23
33
  export interface DbStatement {
24
34
  /** changes: number of rows the statement affected. Both bun:sqlite and node:sqlite's real run() return this at runtime; declared here so callers (e.g. reapStale) can rely on it without an unsafe cast. */
@@ -286,10 +286,11 @@ export class Tasks {
286
286
  const byId = new Map(tasks.map((task) => [task.id, task]));
287
287
  const focus = this.focusStore.get(filter.sessionId);
288
288
  const focusedId = focus?.taskId;
289
+ const focusStatus = focus?.status;
289
290
  const nodes = new Map(tasks.map((task) => [task.id, {
290
291
  task,
291
292
  active: task.id === focusedId,
292
- ...(task.id === focusedId ? { focusStatus: focus!.status } : {}),
293
+ ...(task.id === focusedId ? { focusStatus } : {}),
293
294
  parentIds: [] as string[],
294
295
  childIds: [] as string[],
295
296
  dependencyIds: [] as string[],