appduct 0.9.0

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.
Files changed (190) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -0
  3. package/bin.js +2 -0
  4. package/dist/artifact-inspect.d.ts +110 -0
  5. package/dist/artifact-inspect.d.ts.map +1 -0
  6. package/dist/artifact-inspect.js +315 -0
  7. package/dist/bin.d.ts +3 -0
  8. package/dist/bin.d.ts.map +1 -0
  9. package/dist/bin.js +4 -0
  10. package/dist/cli/command-options.d.ts +44 -0
  11. package/dist/cli/command-options.d.ts.map +1 -0
  12. package/dist/cli/command-options.js +95 -0
  13. package/dist/cli/create-cli.d.ts +2 -0
  14. package/dist/cli/create-cli.d.ts.map +1 -0
  15. package/dist/cli/create-cli.js +56 -0
  16. package/dist/cli/dispatch.d.ts +3 -0
  17. package/dist/cli/dispatch.d.ts.map +1 -0
  18. package/dist/cli/dispatch.js +275 -0
  19. package/dist/cli/open-target.d.ts +112 -0
  20. package/dist/cli/open-target.d.ts.map +1 -0
  21. package/dist/cli/open-target.js +438 -0
  22. package/dist/cli/result-types.d.ts +194 -0
  23. package/dist/cli/result-types.d.ts.map +1 -0
  24. package/dist/cli/result-types.js +6 -0
  25. package/dist/cli/runner.d.ts +35 -0
  26. package/dist/cli/runner.d.ts.map +1 -0
  27. package/dist/cli/runner.js +111 -0
  28. package/dist/cli/types.d.ts +20 -0
  29. package/dist/cli/types.d.ts.map +1 -0
  30. package/dist/cli/types.js +10 -0
  31. package/dist/cli.d.ts +4 -0
  32. package/dist/cli.d.ts.map +1 -0
  33. package/dist/cli.js +2 -0
  34. package/dist/client/app-client.d.ts +126 -0
  35. package/dist/client/app-client.d.ts.map +1 -0
  36. package/dist/client/app-client.js +223 -0
  37. package/dist/client/bootstrap.d.ts +48 -0
  38. package/dist/client/bootstrap.d.ts.map +1 -0
  39. package/dist/client/bootstrap.js +108 -0
  40. package/dist/client/connect.d.ts +17 -0
  41. package/dist/client/connect.d.ts.map +1 -0
  42. package/dist/client/connect.js +38 -0
  43. package/dist/client/errors.d.ts +26 -0
  44. package/dist/client/errors.d.ts.map +1 -0
  45. package/dist/client/errors.js +40 -0
  46. package/dist/client/index.d.ts +19 -0
  47. package/dist/client/index.d.ts.map +1 -0
  48. package/dist/client/index.js +16 -0
  49. package/dist/commands/daemon.d.ts +22 -0
  50. package/dist/commands/daemon.d.ts.map +1 -0
  51. package/dist/commands/daemon.js +128 -0
  52. package/dist/commands/doctor.d.ts +23 -0
  53. package/dist/commands/doctor.d.ts.map +1 -0
  54. package/dist/commands/doctor.js +42 -0
  55. package/dist/commands/events.d.ts +37 -0
  56. package/dist/commands/events.d.ts.map +1 -0
  57. package/dist/commands/events.js +76 -0
  58. package/dist/commands/init.d.ts +51 -0
  59. package/dist/commands/init.d.ts.map +1 -0
  60. package/dist/commands/init.js +231 -0
  61. package/dist/commands/invoke.d.ts +22 -0
  62. package/dist/commands/invoke.d.ts.map +1 -0
  63. package/dist/commands/invoke.js +54 -0
  64. package/dist/commands/keygen.d.ts +23 -0
  65. package/dist/commands/keygen.d.ts.map +1 -0
  66. package/dist/commands/keygen.js +46 -0
  67. package/dist/commands/link.d.ts +30 -0
  68. package/dist/commands/link.d.ts.map +1 -0
  69. package/dist/commands/link.js +43 -0
  70. package/dist/commands/ls.d.ts +9 -0
  71. package/dist/commands/ls.d.ts.map +1 -0
  72. package/dist/commands/ls.js +7 -0
  73. package/dist/commands/mcp.d.ts +49 -0
  74. package/dist/commands/mcp.d.ts.map +1 -0
  75. package/dist/commands/mcp.js +98 -0
  76. package/dist/commands/revoke.d.ts +12 -0
  77. package/dist/commands/revoke.d.ts.map +1 -0
  78. package/dist/commands/revoke.js +7 -0
  79. package/dist/commands/tools.d.ts +23 -0
  80. package/dist/commands/tools.d.ts.map +1 -0
  81. package/dist/commands/tools.js +52 -0
  82. package/dist/daemon/address.d.ts +30 -0
  83. package/dist/daemon/address.d.ts.map +1 -0
  84. package/dist/daemon/address.js +71 -0
  85. package/dist/daemon/audit.d.ts +107 -0
  86. package/dist/daemon/audit.d.ts.map +1 -0
  87. package/dist/daemon/audit.js +237 -0
  88. package/dist/daemon/calls.d.ts +84 -0
  89. package/dist/daemon/calls.d.ts.map +1 -0
  90. package/dist/daemon/calls.js +158 -0
  91. package/dist/daemon/config.d.ts +79 -0
  92. package/dist/daemon/config.d.ts.map +1 -0
  93. package/dist/daemon/config.js +170 -0
  94. package/dist/daemon/daemon.d.ts +43 -0
  95. package/dist/daemon/daemon.d.ts.map +1 -0
  96. package/dist/daemon/daemon.js +560 -0
  97. package/dist/daemon/event-bus.d.ts +49 -0
  98. package/dist/daemon/event-bus.d.ts.map +1 -0
  99. package/dist/daemon/event-bus.js +111 -0
  100. package/dist/daemon/links.d.ts +54 -0
  101. package/dist/daemon/links.d.ts.map +1 -0
  102. package/dist/daemon/links.js +118 -0
  103. package/dist/daemon/listener.d.ts +34 -0
  104. package/dist/daemon/listener.d.ts.map +1 -0
  105. package/dist/daemon/listener.js +132 -0
  106. package/dist/daemon/log-rotation.d.ts +82 -0
  107. package/dist/daemon/log-rotation.d.ts.map +1 -0
  108. package/dist/daemon/log-rotation.js +136 -0
  109. package/dist/daemon/pidfile.d.ts +38 -0
  110. package/dist/daemon/pidfile.d.ts.map +1 -0
  111. package/dist/daemon/pidfile.js +90 -0
  112. package/dist/daemon/policy.d.ts +22 -0
  113. package/dist/daemon/policy.d.ts.map +1 -0
  114. package/dist/daemon/policy.js +24 -0
  115. package/dist/daemon/registry.d.ts +19 -0
  116. package/dist/daemon/registry.d.ts.map +1 -0
  117. package/dist/daemon/registry.js +31 -0
  118. package/dist/daemon/rpc-server.d.ts +54 -0
  119. package/dist/daemon/rpc-server.d.ts.map +1 -0
  120. package/dist/daemon/rpc-server.js +203 -0
  121. package/dist/daemon/sessions.d.ts +86 -0
  122. package/dist/daemon/sessions.d.ts.map +1 -0
  123. package/dist/daemon/sessions.js +429 -0
  124. package/dist/daemon/socket-probe.d.ts +16 -0
  125. package/dist/daemon/socket-probe.d.ts.map +1 -0
  126. package/dist/daemon/socket-probe.js +28 -0
  127. package/dist/daemon/state-dir.d.ts +31 -0
  128. package/dist/daemon/state-dir.d.ts.map +1 -0
  129. package/dist/daemon/state-dir.js +43 -0
  130. package/dist/daemon/timers.d.ts +16 -0
  131. package/dist/daemon/timers.d.ts.map +1 -0
  132. package/dist/daemon/timers.js +12 -0
  133. package/dist/daemon/tls.d.ts +58 -0
  134. package/dist/daemon/tls.d.ts.map +1 -0
  135. package/dist/daemon/tls.js +79 -0
  136. package/dist/errors.d.ts +28 -0
  137. package/dist/errors.d.ts.map +1 -0
  138. package/dist/errors.js +162 -0
  139. package/dist/host-certificate.d.ts +8 -0
  140. package/dist/host-certificate.d.ts.map +1 -0
  141. package/dist/host-certificate.js +111 -0
  142. package/dist/index.d.ts +4 -0
  143. package/dist/index.d.ts.map +1 -0
  144. package/dist/index.js +3 -0
  145. package/dist/key-material.d.ts +14 -0
  146. package/dist/key-material.d.ts.map +1 -0
  147. package/dist/key-material.js +42 -0
  148. package/dist/link.d.ts +76 -0
  149. package/dist/link.d.ts.map +1 -0
  150. package/dist/link.js +117 -0
  151. package/dist/mcp/connect-tool.d.ts +115 -0
  152. package/dist/mcp/connect-tool.d.ts.map +1 -0
  153. package/dist/mcp/connect-tool.js +396 -0
  154. package/dist/mcp/daemon-tools.d.ts +13 -0
  155. package/dist/mcp/daemon-tools.d.ts.map +1 -0
  156. package/dist/mcp/daemon-tools.js +29 -0
  157. package/dist/mcp/events-tool.d.ts +111 -0
  158. package/dist/mcp/events-tool.d.ts.map +1 -0
  159. package/dist/mcp/events-tool.js +322 -0
  160. package/dist/mcp/server.d.ts +58 -0
  161. package/dist/mcp/server.d.ts.map +1 -0
  162. package/dist/mcp/server.js +538 -0
  163. package/dist/mcp/tool-mapping.d.ts +58 -0
  164. package/dist/mcp/tool-mapping.d.ts.map +1 -0
  165. package/dist/mcp/tool-mapping.js +143 -0
  166. package/dist/mcp/tool-namespace.d.ts +40 -0
  167. package/dist/mcp/tool-namespace.d.ts.map +1 -0
  168. package/dist/mcp/tool-namespace.js +65 -0
  169. package/dist/native-scheme.d.ts +59 -0
  170. package/dist/native-scheme.d.ts.map +1 -0
  171. package/dist/native-scheme.js +465 -0
  172. package/dist/output.d.ts +28 -0
  173. package/dist/output.d.ts.map +1 -0
  174. package/dist/output.js +342 -0
  175. package/dist/package-version.d.ts +30 -0
  176. package/dist/package-version.d.ts.map +1 -0
  177. package/dist/package-version.js +47 -0
  178. package/dist/qr-terminal.d.ts +13 -0
  179. package/dist/qr-terminal.d.ts.map +1 -0
  180. package/dist/qr-terminal.js +45 -0
  181. package/dist/rpc/client.d.ts +137 -0
  182. package/dist/rpc/client.d.ts.map +1 -0
  183. package/dist/rpc/client.js +819 -0
  184. package/dist/scheme.d.ts +181 -0
  185. package/dist/scheme.d.ts.map +1 -0
  186. package/dist/scheme.js +321 -0
  187. package/dist/spki-pin.d.ts +3 -0
  188. package/dist/spki-pin.d.ts.map +1 -0
  189. package/dist/spki-pin.js +14 -0
  190. package/package.json +65 -0
package/dist/output.js ADDED
@@ -0,0 +1,342 @@
1
+ import pc from "picocolors";
2
+ import { formatAgentWebSocketUrl } from "@appduct/shared";
3
+ import { renderQrToTerminal } from "./qr-terminal.js";
4
+ const indentJson = (value) => {
5
+ return JSON.stringify(value, null, 2);
6
+ };
7
+ const renderMetaLines = (meta) => {
8
+ if (!meta) {
9
+ return [];
10
+ }
11
+ return [
12
+ "Meta",
13
+ ` Command: ${meta.command}`,
14
+ ` Timestamp: ${meta.timestamp}`,
15
+ ...(typeof meta.duration_ms === "number" ? [` Duration: ${meta.duration_ms} ms`] : []),
16
+ ];
17
+ };
18
+ const formatScalar = (value) => {
19
+ if (typeof value === "string") {
20
+ return value;
21
+ }
22
+ if (typeof value === "number" || typeof value === "boolean") {
23
+ return String(value);
24
+ }
25
+ if (value === null || value === undefined) {
26
+ return "null";
27
+ }
28
+ return indentJson(value);
29
+ };
30
+ const renderFields = (title, fields) => {
31
+ const visibleFields = fields.filter(([, value]) => value !== undefined);
32
+ if (visibleFields.length === 0) {
33
+ return [];
34
+ }
35
+ const width = Math.max(...visibleFields.map(([label]) => label.length));
36
+ return [
37
+ title,
38
+ ...visibleFields.map(([label, value]) => ` ${label.padEnd(width)} ${formatScalar(value)}`),
39
+ ];
40
+ };
41
+ /** Humanizes an elapsed duration for `ls`'s "age" column: seconds/minutes/hours/days, coarsest unit
42
+ * that keeps at least one digit of precision. */
43
+ const formatAge = (fromIso, toIso) => {
44
+ const elapsedMs = Math.max(0, new Date(toIso).getTime() - new Date(fromIso).getTime());
45
+ const elapsedSeconds = Math.floor(elapsedMs / 1000);
46
+ if (elapsedSeconds < 60) {
47
+ return `${elapsedSeconds}s`;
48
+ }
49
+ const elapsedMinutes = Math.floor(elapsedSeconds / 60);
50
+ if (elapsedMinutes < 60) {
51
+ return `${elapsedMinutes}m`;
52
+ }
53
+ const elapsedHours = Math.floor(elapsedMinutes / 60);
54
+ if (elapsedHours < 24) {
55
+ return `${elapsedHours}h`;
56
+ }
57
+ return `${Math.floor(elapsedHours / 24)}d`;
58
+ };
59
+ const formatDevice = (device) => {
60
+ const parts = [device.manufacturer, device.model].filter((part) => Boolean(part));
61
+ const label = parts.length > 0 ? parts.join(" ") : "unknown device";
62
+ return device.os ? `${label} (${device.os})` : label;
63
+ };
64
+ const renderLsData = (colors, data, meta) => {
65
+ if (data.length === 0) {
66
+ return [colors.dim("Sessions"), " No Appduct sessions are registered."];
67
+ }
68
+ const now = meta?.timestamp ?? new Date().toISOString();
69
+ const headers = ["Alias", "State", "Device", "Tools", "Age"];
70
+ const rows = data.map((session) => [
71
+ session.alias,
72
+ session.state,
73
+ formatDevice(session.device),
74
+ String(session.toolCount),
75
+ formatAge(session.claimedAt ?? session.createdAt, now),
76
+ ]);
77
+ const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index].length)));
78
+ return [
79
+ colors.green("Sessions"),
80
+ ` ${headers.map((header, index) => header.padEnd(widths[index])).join(" ")}`,
81
+ ...rows.map((row) => ` ${row.map((cell, index) => cell.padEnd(widths[index])).join(" ")}`),
82
+ ];
83
+ };
84
+ const renderToolSummaryTable = (tools) => {
85
+ if (tools.length === 0) {
86
+ return ["Tools", " No tools registered."];
87
+ }
88
+ const nameWidth = Math.max(...tools.map((tool) => tool.name.length), "Name".length);
89
+ return [
90
+ "Tools",
91
+ ` ${"Name".padEnd(nameWidth)} Description`,
92
+ ...tools.map((tool) => ` ${tool.name.padEnd(nameWidth)} ${tool.description}`),
93
+ ];
94
+ };
95
+ const renderToolDetail = (colors, tool) => {
96
+ return renderFields(colors.green(`Tool: ${tool.name}`), [
97
+ ["Description", tool.description],
98
+ ["Input schema", tool.input_schema],
99
+ ["Output schema", tool.output_schema],
100
+ ["Annotations", tool.annotations],
101
+ // Only rendered for a tool that declares one; `renderFields` drops undefined rows, so a tool
102
+ // on the daemon's default deadline shows no line at all rather than a misleading "10000".
103
+ ["Timeout (ms)", tool.timeout_ms],
104
+ ]);
105
+ };
106
+ const renderToolsData = (colors, data, full) => {
107
+ if (!Array.isArray(data)) {
108
+ return renderToolDetail(colors, data);
109
+ }
110
+ if (full) {
111
+ return data.length === 0
112
+ ? ["Tools", " No tools registered."]
113
+ : data.flatMap((tool, index) => [...(index > 0 ? [""] : []), ...renderToolDetail(colors, tool)]);
114
+ }
115
+ return [colors.green("Tools"), ...renderToolSummaryTable(data).slice(1)];
116
+ };
117
+ const renderInvokeData = (colors, data) => {
118
+ return [colors.green("Result"), formatScalar(data)];
119
+ };
120
+ const renderLinkData = (colors, data, qr) => {
121
+ const lines = [
122
+ colors.green("Link Created"),
123
+ ...renderFields("Link", [
124
+ ["Session", data.sessionId],
125
+ ["Deep link", data.deepLink],
126
+ ["Endpoint", formatAgentWebSocketUrl(data.endpoint)],
127
+ ["Pin", data.pin],
128
+ ["Expires", new Date(data.expiresAt * 1000).toISOString()],
129
+ ["Delivered", data.delivered ? `yes (${data.target})` : undefined],
130
+ ]),
131
+ ];
132
+ if (qr) {
133
+ lines.push("", "QR", renderQrToTerminal(data.deepLink));
134
+ }
135
+ return lines;
136
+ };
137
+ const renderRevokeData = (colors, _data) => {
138
+ return [colors.green("Session Revoked")];
139
+ };
140
+ const renderKeygenData = (colors, data) => {
141
+ return [
142
+ colors.green("Key Ready"),
143
+ ...renderFields("Key", [
144
+ ["Path", data.path],
145
+ ["Fingerprint", data.pin],
146
+ ]),
147
+ ];
148
+ };
149
+ const renderInitData = (colors, data) => {
150
+ const snippet = JSON.stringify({ mcpServers: { appduct: data.mcpServerEntry } }, null, 2);
151
+ return [
152
+ colors.green(data.changed ? "Project Initialized" : "Project Already Initialized"),
153
+ ...renderFields("Config", [
154
+ ["Path", data.path],
155
+ ["Scheme", data.scheme],
156
+ ["Source", data.source],
157
+ // Only present for a scheme discovery read off a static project file (app.json, or one of
158
+ // the native Android/iOS probes) — names the exact file/key, not just which platform.
159
+ ["Read from", data.origin],
160
+ ["Written", data.changed ? (data.created ? "created" : "updated") : "unchanged"],
161
+ ]),
162
+ ...(data.note === undefined ? [] : ["", colors.yellow(`Note: ${data.note}`)]),
163
+ "",
164
+ "MCP server entry",
165
+ ...snippet.split("\n").map((line) => ` ${line}`),
166
+ "",
167
+ "Next",
168
+ ...data.nextSteps.map((step, index) => ` ${index + 1}. ${step}`),
169
+ ];
170
+ };
171
+ const renderDaemonRunData = (colors, data) => {
172
+ return [
173
+ colors.green("Daemon Running"),
174
+ ...renderFields("Daemon", [
175
+ ["PID", data.daemon.pid],
176
+ ["State dir", data.daemon.state_dir],
177
+ ["Socket", data.daemon.socket_path],
178
+ ]),
179
+ ];
180
+ };
181
+ const renderDaemonStartData = (colors, data) => {
182
+ return [
183
+ colors.green("Daemon Started"),
184
+ ...renderFields("Daemon", [
185
+ ["PID", data.daemon.pid],
186
+ ["WSS port", data.daemon.wss_port],
187
+ ["Started at", data.daemon.started_at],
188
+ ]),
189
+ ];
190
+ };
191
+ const renderDaemonStopData = (colors, data) => {
192
+ return [
193
+ colors.green("Daemon Stopped"),
194
+ ...renderFields("Daemon", [["Method", data.daemon.method]]),
195
+ ];
196
+ };
197
+ /** Human column for a byte count (the machine-readable `bytes` stays exact in `--json`). Binary
198
+ * units, one decimal above KiB, so a growing `audit/` reads at a glance. */
199
+ const formatByteSize = (bytes) => {
200
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
201
+ let value = bytes;
202
+ let unit = 0;
203
+ while (value >= 1024 && unit < units.length - 1) {
204
+ value /= 1024;
205
+ unit += 1;
206
+ }
207
+ return `${unit === 0 ? value : value.toFixed(1)} ${units[unit]}`;
208
+ };
209
+ const renderDaemonStatusData = (colors, data) => {
210
+ return [
211
+ colors.green("Daemon Status"),
212
+ ...renderFields("Daemon", [
213
+ ["Version", data.daemon.version],
214
+ ["PID", data.daemon.pid],
215
+ ["Started at", data.daemon.started_at],
216
+ ["WSS port", data.daemon.wss_port],
217
+ ["Pinned keys", data.daemon.pinned_keys.length],
218
+ ["Sessions", data.daemon.session_count],
219
+ ]),
220
+ "",
221
+ ...renderFields("Policy", [
222
+ ["Default", data.policy.default],
223
+ ["Destructive", data.policy.destructive],
224
+ ["Overrides", data.policy.tools ? Object.keys(data.policy.tools).length : 0],
225
+ ]),
226
+ "",
227
+ // The retention rows drop out entirely against a daemon that predates them (`renderFields`
228
+ // skips `undefined`), rather than printing "undefined" or an invented zero.
229
+ ...renderFields("Audit", [
230
+ ["Path", data.audit.path],
231
+ ["Retention", data.audit.retention_days === undefined ? undefined : `${data.audit.retention_days} days`],
232
+ ["Files", data.audit.files],
233
+ ["Size", data.audit.bytes === undefined ? undefined : formatByteSize(data.audit.bytes)],
234
+ ["Failed writes", data.audit.failed_writes],
235
+ ["Failed prunes", data.audit.failed_prunes],
236
+ ]),
237
+ // Version drift (issue #30) is the one thing here an operator has to act on, so it goes last
238
+ // — the line still on screen after the block scrolls — and in yellow, not a quiet field row.
239
+ ...(data.warning ? ["", colors.yellow(`Warning: ${data.warning}`)] : []),
240
+ ];
241
+ };
242
+ const renderDoctorData = (colors, data) => {
243
+ return [
244
+ data.present ? colors.green("Appduct Present") : colors.yellow("Appduct Absent"),
245
+ ...renderFields("Artifact", [
246
+ ["Path", data.artifact],
247
+ ["Platform", data.platform],
248
+ ["Format", data.format],
249
+ ["Present", data.present],
250
+ ["Signals", data.signals.length > 0 ? data.signals.join(", ") : "none"],
251
+ ["Assertion", data.assertion ? `${data.assertion.expected} (holds)` : undefined],
252
+ ]),
253
+ ];
254
+ };
255
+ const renderSuccessData = (colors, command, data, options, meta) => {
256
+ switch (command) {
257
+ case "init":
258
+ return renderInitData(colors, data);
259
+ case "keygen":
260
+ return renderKeygenData(colors, data);
261
+ case "link":
262
+ return renderLinkData(colors, data, options.qr);
263
+ case "ls":
264
+ return renderLsData(colors, data, meta);
265
+ case "tools":
266
+ return renderToolsData(colors, data, options.full);
267
+ case "invoke":
268
+ return renderInvokeData(colors, data);
269
+ case "revoke":
270
+ return renderRevokeData(colors, data);
271
+ case "daemon run":
272
+ return renderDaemonRunData(colors, data);
273
+ case "daemon start":
274
+ return renderDaemonStartData(colors, data);
275
+ case "daemon stop":
276
+ return renderDaemonStopData(colors, data);
277
+ case "daemon status":
278
+ return renderDaemonStatusData(colors, data);
279
+ case "doctor":
280
+ return renderDoctorData(colors, data);
281
+ default:
282
+ return [colors.green("Command Complete"), indentJson(data)];
283
+ }
284
+ };
285
+ const renderHumanError = (colors, error, meta) => {
286
+ return [
287
+ colors.red("Command Failed"),
288
+ ...renderFields("Error", [
289
+ ["Type", error.type],
290
+ ["Message", error.message],
291
+ ["Details", error.details],
292
+ ]),
293
+ ...(meta ? ["", ...renderMetaLines(meta)] : []),
294
+ ].join("\n");
295
+ };
296
+ export const renderResult = (result, options) => {
297
+ if (options.json) {
298
+ return {
299
+ stdout: `${indentJson(result)}\n`,
300
+ };
301
+ }
302
+ const colors = pc.createColors(options.color);
303
+ if (!result.ok) {
304
+ return {
305
+ stderr: `${renderHumanError(colors, result.error, result.meta)}\n`,
306
+ };
307
+ }
308
+ const lines = [
309
+ ...renderSuccessData(colors, options.command, result.data, options, result.meta),
310
+ "",
311
+ ...renderMetaLines(result.meta),
312
+ ].filter((line, index, collection) => {
313
+ if (line !== "") {
314
+ return true;
315
+ }
316
+ return index > 0 && collection[index - 1] !== "" && index < collection.length - 1;
317
+ });
318
+ return {
319
+ stdout: `${lines.join("\n")}\n`,
320
+ };
321
+ };
322
+ /** Renders one `appduct events` line: NDJSON under `--json`, a compact human line otherwise. */
323
+ export const renderEventLine = (event, options) => {
324
+ if (options.json) {
325
+ return JSON.stringify(event);
326
+ }
327
+ const colors = pc.createColors(options.color);
328
+ const timestamp = new Date(event.ts).toISOString();
329
+ const target = event.alias ?? event.sessionId;
330
+ const dataSuffix = event.data === undefined ? "" : ` ${indentJson(event.data)}`;
331
+ return `${colors.dim(timestamp)} ${colors.green(event.kind)}${target ? ` ${target}` : ""}${dataSuffix}`;
332
+ };
333
+ /** Renders the trailing cursor line for `appduct events --since` (issue #6): NDJSON under
334
+ * `--json` so a scripted caller can parse the resume point without maxing `seq` over the printed
335
+ * events (impossible when the response is empty), a human note otherwise. */
336
+ export const renderEventsCursorLine = (cursor, options) => {
337
+ if (options.json) {
338
+ return JSON.stringify({ cursor });
339
+ }
340
+ const colors = pc.createColors(options.color);
341
+ return colors.dim(`cursor: ${cursor} (pass --since ${cursor} to resume from here)`);
342
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The single source of truth for "what version of Appduct is this process running".
3
+ *
4
+ * The daemon (`daemon/daemon.ts`), the MCP server (`mcp/server.ts`), the CLI's `--version`
5
+ * (`cli/create-cli.ts`) and the CLI's daemon version check (`cli/dispatch.ts`) all need the
6
+ * published package version, and every one of them used to re-read `package.json` itself. They
7
+ * read it through this module instead so a single mismatch in the relative path can't make one of
8
+ * them report a different version than the others (issue #30).
9
+ */
10
+ /**
11
+ * Test seam (issue #30): when set, the daemon reports this instead of the real package version in
12
+ * `daemon.status` and the `daemon_started` event, so an integration test can produce a genuine
13
+ * client/daemon version mismatch without installing two builds. Read only by the daemon — a
14
+ * *client* must always report the version it truly is, or the check would compare a lie to a lie.
15
+ *
16
+ * Honored **only under a test runner** (see {@link isTestRuntime}). A daemon that lied about its
17
+ * version in production would make every command restart it, forever: the value is inherited by
18
+ * whatever the shell exported, and the replacement daemon would report the same lie. The RPC
19
+ * client additionally strips this key from the environment of any daemon it spawns.
20
+ */
21
+ export declare const DAEMON_VERSION_OVERRIDE_ENV = "APPDUCT_DAEMON_VERSION_OVERRIDE";
22
+ /** The `version` field of this package's `package.json`, read once per process. */
23
+ export declare const getPackageVersion: () => string;
24
+ /**
25
+ * The version the *daemon* reports over RPC: {@link getPackageVersion}, unless
26
+ * {@link DAEMON_VERSION_OVERRIDE_ENV} overrides it. Evaluated per call (not cached) so a daemon
27
+ * started in-process by a test picks up the environment that test set.
28
+ */
29
+ export declare const getDaemonReportedVersion: (env?: NodeJS.ProcessEnv) => string;
30
+ //# sourceMappingURL=package-version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-version.d.ts","sourceRoot":"","sources":["../src/package-version.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,2BAA2B,oCAAoC,CAAC;AAW7E,mFAAmF;AACnF,eAAO,MAAM,iBAAiB,QAAO,MAGpC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GAAI,MAAK,MAAM,CAAC,UAAwB,KAAG,MAO/E,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The single source of truth for "what version of Appduct is this process running".
3
+ *
4
+ * The daemon (`daemon/daemon.ts`), the MCP server (`mcp/server.ts`), the CLI's `--version`
5
+ * (`cli/create-cli.ts`) and the CLI's daemon version check (`cli/dispatch.ts`) all need the
6
+ * published package version, and every one of them used to re-read `package.json` itself. They
7
+ * read it through this module instead so a single mismatch in the relative path can't make one of
8
+ * them report a different version than the others (issue #30).
9
+ */
10
+ import { readFileSync } from "node:fs";
11
+ import { dirname, join } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ /**
14
+ * Test seam (issue #30): when set, the daemon reports this instead of the real package version in
15
+ * `daemon.status` and the `daemon_started` event, so an integration test can produce a genuine
16
+ * client/daemon version mismatch without installing two builds. Read only by the daemon — a
17
+ * *client* must always report the version it truly is, or the check would compare a lie to a lie.
18
+ *
19
+ * Honored **only under a test runner** (see {@link isTestRuntime}). A daemon that lied about its
20
+ * version in production would make every command restart it, forever: the value is inherited by
21
+ * whatever the shell exported, and the replacement daemon would report the same lie. The RPC
22
+ * client additionally strips this key from the environment of any daemon it spawns.
23
+ */
24
+ export const DAEMON_VERSION_OVERRIDE_ENV = "APPDUCT_DAEMON_VERSION_OVERRIDE";
25
+ /** True under Vitest, or with an explicit `NODE_ENV=test`. */
26
+ const isTestRuntime = (env) => {
27
+ return Boolean(env.VITEST) || env.NODE_ENV === "test";
28
+ };
29
+ const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
30
+ let cached;
31
+ /** The `version` field of this package's `package.json`, read once per process. */
32
+ export const getPackageVersion = () => {
33
+ cached ??= JSON.parse(readFileSync(packageJsonPath, "utf8")).version;
34
+ return cached;
35
+ };
36
+ /**
37
+ * The version the *daemon* reports over RPC: {@link getPackageVersion}, unless
38
+ * {@link DAEMON_VERSION_OVERRIDE_ENV} overrides it. Evaluated per call (not cached) so a daemon
39
+ * started in-process by a test picks up the environment that test set.
40
+ */
41
+ export const getDaemonReportedVersion = (env = process.env) => {
42
+ if (!isTestRuntime(env)) {
43
+ return getPackageVersion();
44
+ }
45
+ const override = env[DAEMON_VERSION_OVERRIDE_ENV];
46
+ return override !== undefined && override.length > 0 ? override : getPackageVersion();
47
+ };
@@ -0,0 +1,13 @@
1
+ export type QrTerminalRenderOptions = {
2
+ /** Quiet zone in modules (default 0). */
3
+ margin?: number;
4
+ /**
5
+ * `toqr` error-correction index: M=0, L=1, H=2, Q=3 (default 1 = L).
6
+ */
7
+ ecLevel?: number;
8
+ };
9
+ /**
10
+ * Renders a QR payload as compact half-block terminal art (two modules per character row).
11
+ */
12
+ export declare const renderQrToTerminal: (content: string, options?: QrTerminalRenderOptions) => string;
13
+ //# sourceMappingURL=qr-terminal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qr-terminal.d.ts","sourceRoot":"","sources":["../src/qr-terminal.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,uBAAuB,GAAG;IACpC,yCAAyC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAMF;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,MAAM,EACf,UAAU,uBAAuB,KAChC,MAyCF,CAAC"}
@@ -0,0 +1,45 @@
1
+ import { toQR } from "toqr";
2
+ const HALF_UPPER = "\u2580"; // ▀
3
+ const HALF_LOWER = "\u2584"; // ▄
4
+ const FULL = "\u2588"; // █
5
+ /**
6
+ * Renders a QR payload as compact half-block terminal art (two modules per character row).
7
+ */
8
+ export const renderQrToTerminal = (content, options) => {
9
+ const margin = options?.margin ?? 0;
10
+ const ecLevel = options?.ecLevel ?? 1;
11
+ const modules = toQR(content, ecLevel);
12
+ const side = Math.sqrt(modules.length);
13
+ if (!Number.isInteger(side)) {
14
+ throw new Error("Invalid QR matrix size");
15
+ }
16
+ const dim = side + 2 * margin;
17
+ const isDark = (row, col) => {
18
+ if (row < margin ||
19
+ col < margin ||
20
+ row >= margin + side ||
21
+ col >= margin + side) {
22
+ return false;
23
+ }
24
+ const mr = row - margin;
25
+ const mc = col - margin;
26
+ return modules[mr * side + mc] === 1;
27
+ };
28
+ const lines = [];
29
+ for (let row = 0; row < dim; row += 2) {
30
+ let line = "";
31
+ for (let col = 0; col < dim; col++) {
32
+ const top = isDark(row, col);
33
+ const bottom = row + 1 < dim ? isDark(row + 1, col) : false;
34
+ line += top
35
+ ? bottom
36
+ ? FULL
37
+ : HALF_UPPER
38
+ : bottom
39
+ ? HALF_LOWER
40
+ : " ";
41
+ }
42
+ lines.push(line);
43
+ }
44
+ return lines.join("\n");
45
+ };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * The auto-spawning RPC client library (ARCHITECTURE.md §4, §5) used by the CLI, MCP server, and
3
+ * tests to talk to `daemon.sock`. On `ENOENT`/`ECONNREFUSED` it takes an exclusive spawn-lock,
4
+ * spawns `daemon run` detached, polls the socket until ready, then retries the request once.
5
+ */
6
+ import { type RpcErrorData } from "@appduct/shared";
7
+ export type SpawnContext = {
8
+ stateDir: string;
9
+ logFilePath: string;
10
+ };
11
+ /** Spawns `daemon run` detached with stdio redirected to `daemon.log`. Injectable for tests. */
12
+ export type SpawnFn = (args: string[], context: SpawnContext) => void | Promise<void>;
13
+ export declare class DaemonRpcError extends Error {
14
+ readonly code: number;
15
+ readonly data?: RpcErrorData | undefined;
16
+ constructor(code: number, message: string, data?: RpcErrorData | undefined);
17
+ }
18
+ /**
19
+ * `reason` distinguishes outcomes that look identical to a caller but mean opposite things to the
20
+ * version-drift restart (issue #30): `"closed"` is the daemon dropping the connection — expected
21
+ * while it tears itself down — whereas `"timeout"` is a daemon that is very much alive and simply
22
+ * not answering, which must never be mistaken for "it is gone, spawn another one".
23
+ */
24
+ export declare class DaemonUnavailableError extends Error {
25
+ readonly reason: "timeout" | "closed" | "unreachable";
26
+ constructor(message: string, reason?: "timeout" | "closed" | "unreachable");
27
+ }
28
+ /** What a restart would destroy, as read from one `daemon.status` reply. */
29
+ export type DaemonLiveState = {
30
+ sessions: number;
31
+ pendingLinks: number;
32
+ };
33
+ /**
34
+ * Raised when the daemon this client reached is running a different Appduct version and could
35
+ * not be replaced (issue #30). The common case is live state a restart would destroy: sessions
36
+ * (resume tokens are in-daemon memory, so a restart makes the app's resume fail closed with 1008)
37
+ * or pending links (a deep link or QR code someone is about to scan). The caller is told rather
38
+ * than silently paying that cost.
39
+ */
40
+ export declare class DaemonVersionMismatchError extends Error {
41
+ readonly daemonVersion: string;
42
+ readonly clientVersion: string;
43
+ readonly sessionCount: number;
44
+ readonly pendingLinkCount: number;
45
+ constructor(daemonVersion: string, clientVersion: string, live: DaemonLiveState, message?: string);
46
+ }
47
+ /**
48
+ * Raised when the daemon *was* restarted and the daemon now answering still reports a different
49
+ * version. Retrying would just kill daemon after daemon, so the check stops after one restart and
50
+ * reports what it actually observed rather than guessing at a cause.
51
+ */
52
+ export declare class DaemonVersionRestartIneffectiveError extends DaemonVersionMismatchError {
53
+ constructor(daemonVersion: string, clientVersion: string, live: DaemonLiveState);
54
+ }
55
+ /**
56
+ * Opt-in daemon/client version check (issue #30). Passed by the CLI dispatcher and the MCP
57
+ * server's startup stream; deliberately *not* passed by `appduct/client`, whose callers are
58
+ * test processes that must never have a daemon restarted out from under a live app session.
59
+ */
60
+ export type VersionCheckOptions = {
61
+ /** The version this client actually is — always the real package version, never an override. */
62
+ clientVersion: string;
63
+ /** Restart even when the daemon has live sessions or pending links (`--daemon-restart`, the
64
+ * `APPDUCT_DAEMON_RESTART` env var, or `config.json`'s `restartDaemonOnVersionMismatch`). */
65
+ forceRestart?: boolean;
66
+ /** Where the "the daemon is newer than this client" notice goes. Defaults to `process.stderr` —
67
+ * never stdout, which belongs to `--json` output and to the MCP transport's frames. */
68
+ onWarning?: (message: string) => void;
69
+ };
70
+ /**
71
+ * True when two version strings name the same version even though the strings differ — `1.2` and
72
+ * `1.2.0`, or two builds differing only in `+metadata`. Comparing the raw strings would report
73
+ * drift that does not exist and, worse, print a "the daemon is newer" notice about a daemon that
74
+ * is exactly this version.
75
+ */
76
+ export declare const isSameVersion: (a: string, b: string) => boolean;
77
+ export type CallDaemonOptions = {
78
+ stateDir: string;
79
+ /** Auto-spawn a daemon on `ENOENT`/`ECONNREFUSED`. Defaults to `true`. */
80
+ autoSpawn?: boolean;
81
+ spawn?: SpawnFn;
82
+ /** Per-request timeout waiting for a response line. Defaults to 10s. */
83
+ requestTimeoutMs?: number;
84
+ /** How long to poll the socket for readiness after spawning. Defaults to 5000ms. */
85
+ spawnWaitTimeoutMs?: number;
86
+ /** Poll interval while waiting for the socket to become ready. Defaults to 100ms. */
87
+ spawnPollIntervalMs?: number;
88
+ /**
89
+ * When set, the first call from this process to a given daemon socket compares the daemon's
90
+ * version with {@link VersionCheckOptions.clientVersion} and restarts the daemon when they
91
+ * differ and doing so is safe. The result is cached per socket path, so this costs one extra
92
+ * `daemon.status` round-trip per process — not per request.
93
+ */
94
+ checkVersion?: VersionCheckOptions;
95
+ };
96
+ /**
97
+ * True when `error` indicates the daemon is not reachable at all — no listener at the socket
98
+ * path (`ENOENT`/`ECONNREFUSED`), or a `DaemonUnavailableError` from a request that timed out or
99
+ * had its connection drop mid-flight. Callers use this to decide whether a fallback (e.g. `daemon
100
+ * stop`'s SIGTERM-via-pidfile path) applies, as opposed to a `DaemonRpcError` from a daemon that
101
+ * is very much running but rejected the request.
102
+ */
103
+ export declare const isDaemonUnreachableError: (error: unknown) => boolean;
104
+ /** Test seam: drops the per-process check cache so each test case starts from a clean slate. */
105
+ export declare const resetDaemonVersionChecks: () => void;
106
+ /**
107
+ * Runs the version check on its own, without issuing a command RPC — the CLI dispatcher's seam
108
+ * (issue #30). Pass `autoSpawn: false` (the default here) when the caller does not want a daemon
109
+ * started just to be checked: nothing listening means nothing to drift from.
110
+ */
111
+ export declare const ensureDaemonVersionMatches: (options: Omit<CallDaemonOptions, "checkVersion"> & {
112
+ checkVersion: VersionCheckOptions;
113
+ }) => Promise<void>;
114
+ /** Calls a daemon RPC method, auto-spawning the daemon on a missing/refused connection. */
115
+ export declare const callDaemon: <TResult>(method: string, params: unknown, options: CallDaemonOptions) => Promise<TResult>;
116
+ export type DaemonStream = {
117
+ /** Sends a request over the persistent connection and awaits its response. `timeoutMs`
118
+ * overrides this stream's default transport timeout for this call only — needed when a
119
+ * request's own server-side deadline (e.g. `tools.call`'s `timeoutMs` param) can exceed the
120
+ * stream's default, so the transport timeout never fires before the server-side one does. */
121
+ call: <TResult>(method: string, params?: unknown, timeoutMs?: number) => Promise<TResult>;
122
+ /** Subscribes to server→client `"event"` notifications; returns an unsubscribe function. */
123
+ onNotification: (callback: (payload: unknown) => void) => () => void;
124
+ /** Fires once when the underlying socket closes (daemon gone, stop(), etc.); returns an
125
+ * unsubscribe function. Lets long-lived stream consumers (e.g. `appduct events`) end
126
+ * gracefully instead of hanging once the connection is no longer usable. */
127
+ onClose: (callback: () => void) => () => void;
128
+ close: () => void;
129
+ };
130
+ /**
131
+ * Opens a persistent connection to the daemon (auto-spawning as needed) for use cases that need
132
+ * server→client notifications, e.g. `events.subscribe`.
133
+ */
134
+ export declare const openDaemonStream: (options: Omit<CallDaemonOptions, "requestTimeoutMs"> & {
135
+ requestTimeoutMs?: number;
136
+ }) => Promise<DaemonStream>;
137
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/rpc/client.ts"],"names":[],"mappings":"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"}