@danypops/papyrus 0.38.2 → 0.38.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.38.2",
3
+ "version": "0.38.4",
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"],
@@ -35,6 +35,7 @@
35
35
  "files": ["src", "README.md"],
36
36
  "dependencies": {
37
37
  "@danypops/vehicle-core": "^0.1.1",
38
+ "@danypops/vehicle-client": "^0.1.1",
38
39
  "@danypops/vehicle-server": "^0.3.2"
39
40
  }
40
41
  }
package/src/client.ts CHANGED
@@ -1,5 +1,8 @@
1
- import { DAEMON_CLIENT_TIMEOUT_MS, DAEMON_PROBE_TIMEOUT_MS } from "./constants.ts";
2
- import { daemonStateDir, readDaemonHandle } from "./daemon-state.ts";
1
+ import { spawn as spawnProcess } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+ import { connectWithPolicy, spawnDetachedDaemon } from "@danypops/vehicle-client/daemon-client";
4
+ import { DAEMON_CLIENT_TIMEOUT_MS, DAEMON_DIR_ENV, DAEMON_PROBE_TIMEOUT_MS } from "./constants.ts";
5
+ import { daemonStateDir, readDaemonHandle, type DaemonHandle } from "./daemon-state.ts";
3
6
  import type { OperationName, SchemaState } from "./service.ts";
4
7
 
5
8
  export type FetchAdapter = (request: Request) => Promise<Response>;
@@ -46,9 +49,7 @@ export class PapyrusClient {
46
49
  }
47
50
  }
48
51
 
49
- export async function connectPapyrusClient(dir: string = daemonStateDir()): Promise<PapyrusClient> {
50
- const handle = readDaemonHandle(dir);
51
- if (!handle) throw new Error("Papyrus daemon is not running; install/start papyrus.service");
52
+ async function probedPapyrusClient(handle: DaemonHandle): Promise<PapyrusClient> {
52
53
  const probe = new PapyrusClient(handle.baseUrl, handle.token, (request) => fetch(request), DAEMON_PROBE_TIMEOUT_MS);
53
54
  try {
54
55
  await probe.health();
@@ -58,6 +59,54 @@ export async function connectPapyrusClient(dir: string = daemonStateDir()): Prom
58
59
  }
59
60
  }
60
61
 
62
+ /** packages/papyrus/src/cli.ts, resolved relative to this file's own installed location, not require.resolve('@danypops/papyrus') -- this module IS that package, no cross-package lookup needed. */
63
+ function papyrusCliPath(): string {
64
+ return fileURLToPath(new URL("cli.ts", import.meta.url));
65
+ }
66
+
67
+ export interface ConnectPapyrusClientOptions {
68
+ /**
69
+ * Environment passed to an auto-spawned daemon child. Defaults to the current
70
+ * process.env. Always carries DAEMON_DIR_ENV=dir so the spawned child computes
71
+ * the exact same state directory this call itself reads/polls -- without this,
72
+ * a caller-supplied `dir` (every real test; production always uses the default
73
+ * daemonStateDir(), which the child would derive identically on its own) would
74
+ * silently diverge from wherever the child actually starts writing its handle.
75
+ */
76
+ env?: Record<string, string | undefined>;
77
+ }
78
+
79
+ /**
80
+ * Transparently starts the daemon first if it is not already running -- matches
81
+ * every other daemon-backed ecosystem package that opted into auto-start (see
82
+ * @danypops/vehicle-client's connectWithPolicy doc comment: web-spider opts in,
83
+ * lector/pi-packed fail closed by design). Papyrus previously failed closed with
84
+ * "install/start papyrus.service", requiring a human to separately discover and
85
+ * run `papyrus service install` before the very first tool call could succeed.
86
+ * A handle file that exists but points at a dead/unreachable daemon is a distinct
87
+ * failure (stale, not "never started") and is NOT auto-recovered here -- it still
88
+ * throws its own actionable "restart manually" error, unchanged from before.
89
+ */
90
+ export async function connectPapyrusClient(dir: string = daemonStateDir(), options: ConnectPapyrusClientOptions = {}): Promise<PapyrusClient> {
91
+ return connectWithPolicy({
92
+ readHandle: () => readDaemonHandle(dir) ?? null,
93
+ buildClient: probedPapyrusClient,
94
+ autoStart: true,
95
+ spawn: () => {
96
+ spawnDetachedDaemon({
97
+ binPath: papyrusCliPath(),
98
+ args: ["serve"],
99
+ env: { ...(options.env ?? process.env), [DAEMON_DIR_ENV]: dir },
100
+ spawn: (command, args, spawnOptions) => {
101
+ const child = spawnProcess(command, args, spawnOptions);
102
+ child.unref();
103
+ },
104
+ });
105
+ },
106
+ fallbackMessage: "Papyrus daemon failed to start automatically; run `papyrus service install` or `papyrus serve` manually.",
107
+ });
108
+ }
109
+
61
110
  export interface PushChannelTarget {
62
111
  /** ws:// URL for the daemon's push-invalidation channel (see push-channel.ts in vehicle-server). */
63
112
  url: string;
@@ -12,6 +12,9 @@ import {
12
12
  export interface DaemonHandle {
13
13
  baseUrl: string;
14
14
  token: string;
15
+ host: string;
16
+ port: number;
17
+ pid: number;
15
18
  }
16
19
 
17
20
  export function daemonStateDir(
@@ -38,9 +41,9 @@ export function loadOrCreateToken(dir: string): string {
38
41
  return token;
39
42
  }
40
43
 
41
- export function writeDaemonPort(dir: string, port: number): void {
44
+ export function writeDaemonPort(dir: string, port: number, pid: number = process.pid): void {
42
45
  mkdirSync(dir, { recursive: true });
43
- writeFileSync(join(dir, DAEMON_PORT_FILE), `${port}\n`, { mode: 0o600 });
46
+ writeFileSync(join(dir, DAEMON_PORT_FILE), `${port}\n${pid}\n`, { mode: 0o600 });
44
47
  }
45
48
 
46
49
  export function clearDaemonPort(dir: string): void {
@@ -50,9 +53,14 @@ export function clearDaemonPort(dir: string): void {
50
53
  export function readDaemonHandle(dir: string): DaemonHandle | undefined {
51
54
  try {
52
55
  const token = readFileSync(join(dir, DAEMON_TOKEN_FILE), "utf8").trim();
53
- const port = Number(readFileSync(join(dir, DAEMON_PORT_FILE), "utf8").trim());
56
+ const lines = readFileSync(join(dir, DAEMON_PORT_FILE), "utf8").trim().split("\n");
57
+ const port = Number(lines[0]);
58
+ // pid is absent for a handle written before this field existed -- 0 is a safe
59
+ // "unknown" sentinel (never a real pid), not a crash. Nothing currently reads
60
+ // pid to make a kill/staleness decision, so a stale 0 is inert, not unsafe.
61
+ const pid = lines[1] ? Number(lines[1]) : 0;
54
62
  if (!token || !Number.isInteger(port) || port < 1 || port > 65_535) return undefined;
55
- return { baseUrl: `http://${DAEMON_HOST}:${port}`, token };
63
+ return { baseUrl: `http://${DAEMON_HOST}:${port}`, token, host: DAEMON_HOST, port, pid };
56
64
  } catch {
57
65
  return undefined;
58
66
  }
package/src/index.ts CHANGED
@@ -24,6 +24,7 @@ export type { DiscussionAndRounds } from "./discussion-service.ts";
24
24
  export { NOTE_DISPOSITIONS } from "./note-service.ts";
25
25
  export type { OperationName, SchemaState } from "./service.ts";
26
26
  export type { WorkflowRunResult } from "./workflow-execution.ts";
27
+ export type { PlaybookInvocationResult, PlaybookMissingArguments } from "./playbook-execution.ts";
27
28
  export { projectArtifactRelationships } from "./artifact-relationship-view.ts";
28
29
  export { taskContext } from "./task-context.ts";
29
30
  export { projectTaskExecution, type TaskExecutionPlan, type TaskExecutionState } from "./task-execution.ts";