pinomad 0.6.0 → 0.7.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.
@@ -2,11 +2,11 @@
2
2
  import {
3
3
  main,
4
4
  restartService
5
- } from "./chunk-d5ypmmr7.js";
5
+ } from "./chunk-12kaxggg.js";
6
6
  import"./chunk-kzbzacz9.js";
7
- import"./chunk-c8nxym2z.js";
8
- import"./chunk-d93kvxvc.js";
9
- import"./chunk-n4tjhcb7.js";
7
+ import"./chunk-rtwxjb90.js";
8
+ import"./chunk-sj7rnvh4.js";
9
+ import"./chunk-e4ahjbmz.js";
10
10
  import"./chunk-znhn8af7.js";
11
11
  import"./chunk-ngf1zatm.js";
12
12
  import"./chunk-df32avw6.js";
@@ -13,7 +13,7 @@ import {
13
13
  import {
14
14
  commandHint,
15
15
  webRoot
16
- } from "./chunk-c8nxym2z.js";
16
+ } from "./chunk-rtwxjb90.js";
17
17
  import {
18
18
  normalizeHostName
19
19
  } from "./chunk-znhn8af7.js";
@@ -402,7 +402,7 @@ async function changePassword(options) {
402
402
 
403
403
  // apps/server/src/apns.ts
404
404
  import { createPrivateKey, sign } from "node:crypto";
405
- import { connect } from "node:http2";
405
+ import { connect, constants } from "node:http2";
406
406
  var AUTHORITIES = {
407
407
  production: "https://api.push.apple.com",
408
408
  development: "https://api.sandbox.push.apple.com"
@@ -471,19 +471,25 @@ function openApns(config) {
471
471
  state.token = { jwt, issuedAt: now };
472
472
  return jwt;
473
473
  };
474
+ const dropSession = (state, session) => {
475
+ if (state.session === session)
476
+ state.session = undefined;
477
+ if (!session.destroyed)
478
+ session.destroy();
479
+ };
480
+ const retireSession = (state, session) => {
481
+ if (state.session === session)
482
+ state.session = undefined;
483
+ if (!session.closed && !session.destroyed)
484
+ session.close();
485
+ };
474
486
  const sessionFor = (environment, state) => {
475
487
  const existing = state.session;
476
488
  if (existing !== undefined && !existing.destroyed && !existing.closed)
477
489
  return existing;
478
490
  const session = connect(state.authority);
479
- const drop = () => {
480
- if (state.session === session)
481
- state.session = undefined;
482
- if (!session.destroyed)
483
- session.destroy();
484
- };
485
- session.once("goaway", drop);
486
- session.once("error", drop);
491
+ session.once("goaway", () => dropSession(state, session));
492
+ session.once("error", () => dropSession(state, session));
487
493
  session.once("close", () => {
488
494
  if (state.session === session)
489
495
  state.session = undefined;
@@ -566,6 +572,7 @@ function openApns(config) {
566
572
  resolve({ result: "retry", reason: "session unavailable" });
567
573
  return;
568
574
  }
575
+ let responded = false;
569
576
  let status = 0;
570
577
  let responseHeaders = {};
571
578
  const chunks = [];
@@ -580,11 +587,16 @@ function openApns(config) {
580
587
  };
581
588
  const timer = setTimeout(() => finish({ result: "retry", reason: "apns timeout" }), REQUEST_TIMEOUT_MS);
582
589
  stream.on("response", (headers) => {
590
+ responded = true;
583
591
  status = Number(headers[":status"] ?? 0);
584
592
  responseHeaders = headers;
585
593
  });
586
594
  stream.on("data", (chunk) => chunks.push(chunk));
587
595
  stream.on("end", () => {
596
+ if (!responded) {
597
+ finish({ result: "retry", reason: "connection closed" });
598
+ return;
599
+ }
588
600
  let body2;
589
601
  try {
590
602
  body2 = JSON.parse(Buffer.concat(chunks).toString("utf8"));
@@ -593,7 +605,12 @@ function openApns(config) {
593
605
  }
594
606
  finish(classify(request.environment, status, responseHeaders, body2, state));
595
607
  });
596
- stream.on("error", () => finish({ result: "retry", reason: "connection error" }));
608
+ stream.on("error", () => {
609
+ const refused = stream.rstCode === constants.NGHTTP2_REFUSED_STREAM;
610
+ finish({ result: "retry", reason: "connection error" });
611
+ if (refused)
612
+ retireSession(state, session);
613
+ });
597
614
  stream.on("aborted", () => finish({ result: "retry", reason: "connection aborted" }));
598
615
  stream.end(body);
599
616
  });
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  packagedVersion
3
- } from "./chunk-c8nxym2z.js";
3
+ } from "./chunk-rtwxjb90.js";
4
4
  import {
5
5
  hostAddress
6
6
  } from "./chunk-ngf1zatm.js";
@@ -14,20 +14,43 @@ var conversationStream = (id) => `conversation:${id}`;
14
14
  var docStream = (kind, id) => `doc:${kind}:${id}`;
15
15
  var UNAUTHORIZED_CLOSE_CODE = 4401;
16
16
  var INVALID_FRAME_CLOSE_CODE = 4400;
17
- var PROTOCOL = { major: 5, minor: 12 };
17
+ var PROTOCOL = { major: 5, minor: 19 };
18
18
  var CALL_SINCE = {
19
- logout: 1,
20
- updateSettings: 2,
19
+ addProject: 0,
20
+ removeProject: 0,
21
21
  listFolders: 3,
22
+ listFiles: 10,
23
+ listSkills: 13,
24
+ listCommands: 17,
25
+ createConversation: 0,
26
+ archive: 0,
22
27
  renameConversation: 4,
28
+ submit: 0,
29
+ withdrawQueued: 14,
30
+ steerQueued: 14,
31
+ abort: 0,
32
+ compact: 0,
33
+ runCommand: 17,
34
+ setModel: 0,
35
+ setThinkingLevel: 0,
36
+ setAgent: 18,
37
+ fork: 0,
38
+ answer: 0,
39
+ changes: 0,
40
+ createPairing: 0,
41
+ revokeDevice: 0,
42
+ logout: 1,
43
+ updateSettings: 2,
23
44
  createAgent: 6,
24
45
  updateAgent: 6,
25
46
  deleteAgent: 6,
26
47
  setPushToken: 7,
27
- listFiles: 10
48
+ image: 15,
49
+ beginAttachment: 19,
50
+ appendAttachment: 19
28
51
  };
29
52
  function hostSupportsCall(hostMinor, method) {
30
- return hostMinor >= (CALL_SINCE[method] ?? 0);
53
+ return hostMinor >= CALL_SINCE[method];
31
54
  }
32
55
  var FEATURE_SINCE = {
33
56
  images: 5,
@@ -35,7 +58,10 @@ var FEATURE_SINCE = {
35
58
  apnsPush: 9,
36
59
  agentConversations: 10,
37
60
  liveBranch: 11,
38
- hostName: 12
61
+ hostName: 12,
62
+ imageRefs: 15,
63
+ boundSkills: 16,
64
+ attachments: 19
39
65
  };
40
66
  function hostSupportsFeature(hostMinor, feature) {
41
67
  return hostMinor >= FEATURE_SINCE[feature];
@@ -48,7 +74,8 @@ function parseClientFrame(value) {
48
74
  if (typeof stream !== "string")
49
75
  return { kind: "malformed" };
50
76
  if (isStreamName(stream)) {
51
- return { kind: "frame", frame: { type: value.type, stream } };
77
+ const refs = value.type === "subscribe" && value.images === "ref";
78
+ return { kind: "frame", frame: refs ? { type: "subscribe", stream, images: "ref" } : { type: value.type, stream } };
52
79
  }
53
80
  const prefix = stream.slice(0, stream.indexOf(":") === -1 ? undefined : stream.indexOf(":"));
54
81
  if (STREAM_KINDS.has(prefix))
@@ -70,31 +97,7 @@ function isStreamName(stream) {
70
97
  return stream === "tasks" || HOST_STREAM_NAMES.includes(stream) || /^(?:conversation:|doc:.+:)[1-9]\d*$/.test(stream);
71
98
  }
72
99
  var isCallMethod = (method) => CALL_METHODS.includes(method);
73
- var CALL_METHODS = [
74
- "addProject",
75
- "removeProject",
76
- "listFolders",
77
- "createConversation",
78
- "archive",
79
- "renameConversation",
80
- "submit",
81
- "abort",
82
- "compact",
83
- "setModel",
84
- "setThinkingLevel",
85
- "fork",
86
- "answer",
87
- "changes",
88
- "createPairing",
89
- "revokeDevice",
90
- "logout",
91
- "updateSettings",
92
- "createAgent",
93
- "updateAgent",
94
- "deleteAgent",
95
- "setPushToken",
96
- "listFiles"
97
- ];
100
+ var CALL_METHODS = Object.keys(CALL_SINCE);
98
101
  var SETTING_VALIDATORS = {
99
102
  defaultModel: (value) => value === null || record(value) && nonempty(value.provider) && nonempty(value.modelId),
100
103
  defaultThinkingLevel: (value) => value === null || isThinkingLevel(value),
@@ -105,17 +108,29 @@ var SETTING_VALIDATORS = {
105
108
  };
106
109
  function validCallArgs(method, args) {
107
110
  const conversationId = () => typeof args.conversationId === "number" && Number.isSafeInteger(args.conversationId) && args.conversationId >= 1;
108
- const images = () => args.images === undefined || Array.isArray(args.images) && args.images.every((image) => record(image) && typeof image.data === "string" && typeof image.mimeType === "string");
111
+ const submissionId = () => typeof args.submissionId === "number" && Number.isSafeInteger(args.submissionId) && args.submissionId >= 1;
112
+ const images = () => args.images === undefined || Array.isArray(args.images) && args.images.every((image) => record(image) && ("data" in image ? typeof image.data === "string" && typeof image.mimeType === "string" : nonempty(image.ref)));
113
+ const skills = () => args.skills === undefined || Array.isArray(args.skills) && args.skills.every(nonempty);
114
+ const attachments = () => args.attachments === undefined || Array.isArray(args.attachments) && args.attachments.every(nonempty);
109
115
  const role = () => record(args.role) && typeof args.role.name === "string" && typeof args.role.description === "string" && typeof args.role.prompt === "string" && (args.role.model === undefined || typeof args.role.model === "string") && (args.role.thinkingLevel === undefined || typeof args.role.thinkingLevel === "string");
110
116
  switch (method) {
111
117
  case "submit":
112
- return conversationId() && typeof args.text === "string" && (args.whenBusy === "steer" || args.whenBusy === "followUp") && nonempty(args.requestId) && images();
118
+ return conversationId() && typeof args.text === "string" && (args.whenBusy === "steer" || args.whenBusy === "followUp") && nonempty(args.requestId) && images() && skills() && attachments();
119
+ case "withdrawQueued":
120
+ case "steerQueued":
121
+ return conversationId() && submissionId();
113
122
  case "abort":
114
123
  return conversationId();
115
124
  case "setThinkingLevel":
116
125
  return conversationId() && nonempty(args.level);
126
+ case "setAgent":
127
+ return conversationId() && (args.agent === null || nonempty(args.agent));
117
128
  case "compact":
118
129
  return conversationId() && (args.instructions === undefined || typeof args.instructions === "string");
130
+ case "listCommands":
131
+ return conversationId();
132
+ case "runCommand":
133
+ return conversationId() && nonempty(args.name) && (args.args === undefined || typeof args.args === "string");
119
134
  case "setModel":
120
135
  return conversationId() && record(args.model) && nonempty(args.model.provider) && nonempty(args.model.modelId);
121
136
  case "fork":
@@ -124,15 +139,23 @@ function validCallArgs(method, args) {
124
139
  return conversationId() && nonempty(args.kind) && nonempty(args.requestId) && Array.isArray(args.answers) && args.answers.every((answer) => record(answer) && Array.isArray(answer.selected) && answer.selected.every((l) => typeof l === "string") && (answer.other === undefined || typeof answer.other === "string"));
125
140
  case "changes":
126
141
  return conversationId();
142
+ case "image":
143
+ return nonempty(args.ref);
144
+ case "beginAttachment":
145
+ return nonempty(args.name) && typeof args.size === "number" && Number.isSafeInteger(args.size) && args.size >= 0;
146
+ case "appendAttachment":
147
+ return nonempty(args.attachmentId) && typeof args.offset === "number" && Number.isSafeInteger(args.offset) && args.offset >= 0 && typeof args.data === "string";
127
148
  case "addProject":
128
149
  case "removeProject":
129
150
  return nonempty(args.path);
130
151
  case "listFolders":
131
152
  return args.path === undefined || nonempty(args.path);
132
153
  case "createConversation":
133
- return record(args.home) && (args.home.kind === "chat" || args.home.kind === "project" && nonempty(args.home.path)) && typeof args.text === "string" && (nonempty(args.text) || Array.isArray(args.images) && args.images.length > 0) && nonempty(args.requestId) && images() && (args.checkout === undefined || args.checkout === "worktree" || args.checkout === "project") && (args.model === undefined || record(args.model) && nonempty(args.model.provider) && nonempty(args.model.modelId)) && (args.thinkingLevel === undefined || nonempty(args.thinkingLevel)) && (args.agent === undefined || nonempty(args.agent));
154
+ return record(args.home) && (args.home.kind === "chat" || args.home.kind === "project" && nonempty(args.home.path)) && typeof args.text === "string" && (nonempty(args.text) || Array.isArray(args.images) && args.images.length > 0 || Array.isArray(args.attachments) && args.attachments.length > 0) && nonempty(args.requestId) && images() && skills() && attachments() && (args.checkout === undefined || args.checkout === "worktree" || args.checkout === "project") && (args.model === undefined || record(args.model) && nonempty(args.model.provider) && nonempty(args.model.modelId)) && (args.thinkingLevel === undefined || nonempty(args.thinkingLevel)) && (args.agent === undefined || nonempty(args.agent));
134
155
  case "listFiles":
135
156
  return typeof args.query === "string" && record(args.scope) && (args.scope.kind === "conversation" && typeof args.scope.conversationId === "number" && Number.isSafeInteger(args.scope.conversationId) && args.scope.conversationId >= 1 || args.scope.kind === "project" && nonempty(args.scope.path) && (args.scope.checkout === "worktree" || args.scope.checkout === "project"));
157
+ case "listSkills":
158
+ return record(args.scope) && (args.scope.kind === "conversation" && typeof args.scope.conversationId === "number" && Number.isSafeInteger(args.scope.conversationId) && args.scope.conversationId >= 1 || args.scope.kind === "project" && nonempty(args.scope.path) && (args.scope.checkout === "worktree" || args.scope.checkout === "project") || args.scope.kind === "chat");
136
159
  case "archive":
137
160
  return conversationId() && typeof args.archived === "boolean";
138
161
  case "renameConversation":
@@ -161,6 +184,145 @@ function nonempty(value) {
161
184
  return typeof value === "string" && value.length > 0;
162
185
  }
163
186
 
187
+ // packages/protocol/src/attachments.ts
188
+ var MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
189
+ var MAX_ATTACHMENTS_PER_MESSAGE = 10;
190
+ var ATTACHMENT_CHUNK_BYTES = 512 * 1024;
191
+ var MAX_PENDING_ATTACHMENTS = 20;
192
+ var MAX_PENDING_ATTACHMENT_BYTES = 200 * 1024 * 1024;
193
+ var ATTACHMENT_TTL_MS = 24 * 60 * 60 * 1000;
194
+ var MAX_ATTACHMENT_NAME_LENGTH = 255;
195
+ var ATTACHMENT_ID_PATTERN = /^[A-Za-z0-9_-]{22}$/;
196
+ function isAttachmentId(value) {
197
+ return ATTACHMENT_ID_PATTERN.test(value);
198
+ }
199
+ var ATTACHMENTS_OPEN = '<attachments v="1">';
200
+ var ATTACHMENTS_INSTRUCTION = "The user attached these files. They are read-only originals on this machine: read them with your tools when relevant, and copy one into the working directory before changing it.";
201
+ var ATTACHMENTS_CLOSE = "</attachments>";
202
+ var BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
203
+ function base64DecodedLength(data) {
204
+ if (data.length % 4 !== 0 || !BASE64.test(data))
205
+ return;
206
+ const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
207
+ return data.length * 3 / 4 - padding;
208
+ }
209
+ function attachmentBeginProblem(name, size) {
210
+ if (name.trim() === "")
211
+ return "Attachment name cannot be empty";
212
+ if (!Number.isSafeInteger(size) || size < 0)
213
+ return "Attachment size must be a non-negative integer";
214
+ if (size > MAX_ATTACHMENT_BYTES)
215
+ return `Attachment is larger than ${MAX_ATTACHMENT_BYTES / (1024 * 1024)} MB`;
216
+ return;
217
+ }
218
+ function attachmentChunkProblem(data) {
219
+ const size = base64DecodedLength(data);
220
+ if (size === undefined)
221
+ return "Attachment chunk is not valid base64";
222
+ if (size > ATTACHMENT_CHUNK_BYTES)
223
+ return `Attachment chunk is larger than ${ATTACHMENT_CHUNK_BYTES / 1024} KiB`;
224
+ return;
225
+ }
226
+ function xmlAttribute(value) {
227
+ let out = "";
228
+ for (let i = 0;i < value.length; i++) {
229
+ const unit = value.charCodeAt(i);
230
+ if (unit === 38)
231
+ out += "&amp;";
232
+ else if (unit === 60)
233
+ out += "&lt;";
234
+ else if (unit === 62)
235
+ out += "&gt;";
236
+ else if (unit === 34)
237
+ out += "&quot;";
238
+ else if (unit < 32 || unit === 127)
239
+ out += `&#x${unit.toString(16)};`;
240
+ else
241
+ out += value[i];
242
+ }
243
+ return out;
244
+ }
245
+ function attachmentBlock(files) {
246
+ const lines = [
247
+ ATTACHMENTS_OPEN,
248
+ ATTACHMENTS_INSTRUCTION,
249
+ ...files.map((file) => `<file id="${xmlAttribute(file.id)}" name="${xmlAttribute(file.name)}" path="${xmlAttribute(file.path)}" size="${file.size}"/>`),
250
+ ATTACHMENTS_CLOSE
251
+ ];
252
+ return lines.join(`
253
+ `);
254
+ }
255
+ var FILE_OPEN = '<file id="';
256
+ var FILE_NAME = '" name="';
257
+ var FILE_PATH = '" path="';
258
+ var FILE_SIZE = '" size="';
259
+ var FILE_END = '"/>';
260
+ var SIZE = /^(?:0|[1-9][0-9]*)$/;
261
+ function decodeXmlAttribute(value) {
262
+ return value.replace(/&(amp|lt|gt|quot|apos|#x[0-9A-Fa-f]+|#[0-9]+);/g, (entity, body) => {
263
+ if (body === "amp")
264
+ return "&";
265
+ if (body === "lt")
266
+ return "<";
267
+ if (body === "gt")
268
+ return ">";
269
+ if (body === "quot")
270
+ return '"';
271
+ if (body === "apos")
272
+ return "'";
273
+ const code = body.startsWith("#x") ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10);
274
+ return code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : entity;
275
+ });
276
+ }
277
+ function quoted(text, at, marker) {
278
+ if (!text.startsWith(marker, at))
279
+ return;
280
+ const start = at + marker.length;
281
+ const end = text.indexOf('"', start);
282
+ if (end === -1)
283
+ return;
284
+ return { value: text.slice(start, end), next: end };
285
+ }
286
+ function parseFileLine(line) {
287
+ const id = quoted(line, 0, FILE_OPEN);
288
+ if (id === undefined)
289
+ return;
290
+ const name = quoted(line, id.next, FILE_NAME);
291
+ if (name === undefined)
292
+ return;
293
+ const path = quoted(line, name.next, FILE_PATH);
294
+ if (path === undefined)
295
+ return;
296
+ const size = quoted(line, path.next, FILE_SIZE);
297
+ if (size === undefined || !line.startsWith(FILE_END, size.next) || size.next + FILE_END.length !== line.length) {
298
+ return;
299
+ }
300
+ if (!isAttachmentId(id.value) || !SIZE.test(size.value))
301
+ return;
302
+ const bytes = Number(size.value);
303
+ if (!Number.isSafeInteger(bytes))
304
+ return;
305
+ return { id: id.value, name: decodeXmlAttribute(name.value), size: bytes };
306
+ }
307
+ function parseAttachmentBlock(text) {
308
+ if (!text.startsWith(ATTACHMENTS_OPEN + `
309
+ `) || !text.endsWith(`
310
+ ` + ATTACHMENTS_CLOSE))
311
+ return;
312
+ const lines = text.slice(ATTACHMENTS_OPEN.length + 1, -(ATTACHMENTS_CLOSE.length + 1)).split(`
313
+ `);
314
+ if (lines[0] !== ATTACHMENTS_INSTRUCTION || lines.length < 2)
315
+ return;
316
+ const files = [];
317
+ for (const line of lines.slice(1)) {
318
+ const file = parseFileLine(line);
319
+ if (file === undefined)
320
+ return;
321
+ files.push(file);
322
+ }
323
+ return files;
324
+ }
325
+
164
326
  // packages/protocol/src/organization.ts
165
327
  function organize(index, summaries) {
166
328
  const summaryById = new Map(summaries.map((summary) => [summary.id, summary]));
@@ -254,4 +416,4 @@ function homeOf(index, summaries, id) {
254
416
  return;
255
417
  }
256
418
 
257
- export { HOST_STREAM_NAMES, hostStreams, conversationStream, docStream, UNAUTHORIZED_CLOSE_CODE, INVALID_FRAME_CLOSE_CODE, PROTOCOL, hostSupportsCall, hostSupportsFeature, parseClientFrame, organize, checkoutOf, homeOf };
419
+ export { HOST_STREAM_NAMES, hostStreams, conversationStream, docStream, UNAUTHORIZED_CLOSE_CODE, INVALID_FRAME_CLOSE_CODE, PROTOCOL, hostSupportsCall, hostSupportsFeature, parseClientFrame, MAX_ATTACHMENTS_PER_MESSAGE, MAX_PENDING_ATTACHMENTS, MAX_PENDING_ATTACHMENT_BYTES, ATTACHMENT_TTL_MS, MAX_ATTACHMENT_NAME_LENGTH, isAttachmentId, base64DecodedLength, attachmentBeginProblem, attachmentChunkProblem, attachmentBlock, parseAttachmentBlock, organize, checkoutOf, homeOf };
@@ -22,14 +22,14 @@ import {
22
22
  restartService,
23
23
  serviceManager,
24
24
  withoutRelayArgs
25
- } from "./chunk-d5ypmmr7.js";
25
+ } from "./chunk-12kaxggg.js";
26
26
  import"./chunk-kzbzacz9.js";
27
27
  import {
28
28
  commandHint,
29
29
  hostEntry
30
- } from "./chunk-c8nxym2z.js";
31
- import"./chunk-d93kvxvc.js";
32
- import"./chunk-n4tjhcb7.js";
30
+ } from "./chunk-rtwxjb90.js";
31
+ import"./chunk-sj7rnvh4.js";
32
+ import"./chunk-e4ahjbmz.js";
33
33
  import {
34
34
  cleanHostName,
35
35
  normalizeHostName
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  connectRemoteDurable
3
- } from "./chunk-d93kvxvc.js";
4
- import"./chunk-n4tjhcb7.js";
3
+ } from "./chunk-sj7rnvh4.js";
4
+ import"./chunk-e4ahjbmz.js";
5
5
  import"./chunk-znhn8af7.js";
6
6
  import {
7
7
  hostAddress
@@ -1,6 +1,6 @@
1
1
  // apps/host/src/distribution.ts
2
2
  import { fileURLToPath } from "node:url";
3
- var packagedVersion = "0.6.0";
3
+ var packagedVersion = "0.7.0";
4
4
  var webRoot = fileURLToPath(new URL(packagedVersion === undefined ? "../../web/dist" : "../web/", import.meta.url));
5
5
  var hostEntry = packagedVersion === undefined ? { script: fileURLToPath(new URL("./main.ts", import.meta.url)), args: [] } : { script: fileURLToPath(new URL("./pinomad.js", import.meta.url)), args: ["host"] };
6
6
  var commandHint = (name) => packagedVersion === undefined ? `bun run ${name}` : `pinomad ${name}`;