parley-chat 0.2.1 → 0.2.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.
Files changed (2) hide show
  1. package/main.mjs +44 -9
  2. package/package.json +2 -2
package/main.mjs CHANGED
@@ -83,6 +83,7 @@ import { join as join3 } from "path";
83
83
  import { promisify } from "util";
84
84
 
85
85
  // ../shared/src/protocol.ts
86
+ var cliVersion = "0.2.3";
86
87
  var skillDocument = `---
87
88
  name: parley
88
89
  description: Ask a mutual contact's local agent a question through Parley, with both inbound execution and outbound replies approved in Discord.
@@ -113,7 +114,7 @@ You can run this entire setup for your human. Steps 2 and 6 need them present (b
113
114
  - \`parley logout\`: deletes the local token.
114
115
  - \`parley invite\`: creates a single-use code that expires after 48 hours.
115
116
  - \`parley join CODE\`: redeems a friend's invite and waits for their approval.
116
- - \`parley contacts\`: lists mutual contacts and whether their responder is connected.
117
+ - \`parley contacts\`: lists mutual contacts with their Discord display name, responder connectivity, and (when online) their CLI version. Note: your contacts see your CLI version the same way.
117
118
  - \`parley remove NAME\`: removes a mutual contact for both people.
118
119
  - \`parley ask @NAME "question"\`: blocks for a one-off response. It fails immediately when the responder is offline.
119
120
  - \`parley listen\`: runs the local responder in the foreground.
@@ -198,6 +199,9 @@ async function fileExists2(path) {
198
199
  // src/responder.ts
199
200
  import { spawn } from "node:child_process";
200
201
 
202
+ // ../shared/src/protocol.ts
203
+ var cliVersion2 = "0.2.3";
204
+
201
205
  // src/http.ts
202
206
  init_files();
203
207
  async function api2(path, init = {}) {
@@ -217,18 +221,28 @@ async function draft(requestId, asker, question) {
217
221
  Asker: ${asker}
218
222
  Question: ${question}
219
223
  `;
220
- const child = spawn("sh", ["-c", local.command], { stdio: ["pipe", "pipe", "ignore"] });
224
+ const child = spawn("sh", ["-c", local.command], { stdio: ["pipe", "pipe", "pipe"] });
221
225
  child.stdin.write(prompt);
222
226
  child.stdin.end();
223
227
  const chunks = [];
228
+ const errChunks = [];
224
229
  child.stdout.on("data", (chunk) => chunks.push(chunk));
230
+ child.stderr.on("data", (chunk) => errChunks.push(chunk));
225
231
  const code = await new Promise((resolve) => child.on("close", (value) => resolve(value ?? 1)));
226
232
  const output = Buffer.concat(chunks).toString("utf8").trim();
233
+ if (code !== 0 || output.length === 0) {
234
+ const errText = Buffer.concat(errChunks).toString("utf8").trim().split(`
235
+ `).slice(-5).join(`
236
+ `);
237
+ process.stderr.write(`draft failed (exit ${code})${errText ? `:
238
+ ${errText}` : " with no stderr"}
239
+ `);
240
+ }
227
241
  await api2(`/v1/asks/${requestId}/draft`, { method: "POST", body: JSON.stringify({ draft: code === 0 ? output : "" }) });
228
242
  }
229
243
  async function listen() {
230
244
  const auth = await Promise.resolve().then(() => (init_files(), exports_files)).then((module) => module.credentials());
231
- const socket = new WebSocket(wsUrl("/v1/responders/connect"), [`parley.${auth.token}`]);
245
+ const socket = new WebSocket(wsUrl(`/v1/responders/connect?version=${encodeURIComponent(cliVersion2)}`), [`parley.${auth.token}`]);
232
246
  await new Promise((resolve, reject) => {
233
247
  socket.addEventListener("open", () => resolve(), { once: true });
234
248
  socket.addEventListener("error", () => reject(new Error("responder connection failed")), { once: true });
@@ -342,8 +356,14 @@ async function contacts() {
342
356
  const entries = Array.isArray(value?.contacts) ? value.contacts : [];
343
357
  for (const entry of entries) {
344
358
  const contact = object(entry);
345
- if (contact)
346
- process.stdout.write(`${text(contact.username)} (${contact.online === true ? "online" : "offline"})
359
+ if (!contact)
360
+ continue;
361
+ const username = text(contact.username);
362
+ const displayName = typeof contact.displayName === "string" && contact.displayName !== username ? ` (${contact.displayName})` : "";
363
+ const online = contact.online === true;
364
+ const version = typeof contact.version === "string" ? contact.version : null;
365
+ const staleness = online && version !== null && version !== cliVersion ? ` v${version} (outdated)` : online && version !== null ? ` v${version}` : "";
366
+ process.stdout.write(`${username}${displayName} \u2014 ${online ? "online" : "offline"}${staleness}
347
367
  `);
348
368
  }
349
369
  return 0;
@@ -425,13 +445,23 @@ function plist(entry) {
425
445
  async function install() {
426
446
  if (process.platform !== "darwin") {
427
447
  process.stderr.write(`not supported yet
448
+ `);
449
+ return 1;
450
+ }
451
+ const uid = process.getuid?.() ?? 501;
452
+ if (uid === 0) {
453
+ process.stderr.write(`do not run parley install with sudo \u2014 launchd user agents belong to your own account. Re-run as yourself:
454
+ parley install
428
455
  `);
429
456
  return 1;
430
457
  }
431
458
  const path = join3(homedir3(), "Library/LaunchAgents/sh.parley.listen.plist");
459
+ await execute("launchctl", ["bootout", `gui/${uid}`, path]).catch(() => {
460
+ return;
461
+ });
432
462
  await mkdir3(join3(parleyHome2(), "logs"), { recursive: true, mode: 448 });
433
463
  await writeFile3(path, plist(process.argv[1] ?? "parley"), { mode: 384 });
434
- await execute("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, path]);
464
+ await execute("launchctl", ["bootstrap", `gui/${uid}`, path]);
435
465
  process.stdout.write(`installed
436
466
  `);
437
467
  return 0;
@@ -574,12 +604,17 @@ Question: reply with the single word "pong" and nothing else
574
604
  }
575
605
  }
576
606
  }
577
- checks.push(["launchagent", await launchAgentState(), await launchAgentState() === "loaded"]);
607
+ const agentState = await launchAgentState();
608
+ checks.push(["launchagent", agentState, agentState === "loaded"]);
578
609
  const online = hasCredentials ? object(await apiJson("/v1/status").catch(() => null))?.online === true : false;
579
610
  checks.push(["responder online", online ? "yes" : "no \u2014 run: parley install (or parley listen)", online]);
580
611
  const logPath = join3(parleyHome2(), "logs/listen.log");
581
- const recentLog = await readFile3(logPath, "utf8").then((value) => value.split(`
582
- `).filter((line) => line.length > 0).slice(-3)).catch(() => []);
612
+ const logLines = await readFile3(logPath, "utf8").then((value) => value.split(`
613
+ `).filter((line) => line.length > 0)).catch(() => []);
614
+ const crashing = agentState === "loaded" && !online && logLines.slice(-20).some((line) => /error|throw|at .*\(node:/i.test(line));
615
+ if (crashing)
616
+ checks.push(["responder process", "crashing on startup \u2014 run `parley listen` to see the error directly, or `parley install` to regenerate the launch agent", false]);
617
+ const recentLog = logLines.slice(crashing ? -20 : -3);
583
618
  let failures = 0;
584
619
  for (const [name, detail, ok] of checks) {
585
620
  if (!ok)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parley-chat",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Ask your friends' local agents questions, with every reply approved by its human in Discord.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,6 @@
16
16
  "license": "MIT",
17
17
  "repository": {
18
18
  "type": "git",
19
- "url": "https://github.com/rhyssullivan/parley"
19
+ "url": "https://github.com/UsefulSoftwareCo/parley"
20
20
  }
21
21
  }