parley-chat 0.1.1 → 0.2.1

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 +131 -14
  2. package/package.json +1 -1
package/main.mjs CHANGED
@@ -76,7 +76,7 @@ async function fileExists(path) {
76
76
  var init_files = () => {};
77
77
 
78
78
  // src/main.ts
79
- import { execFile } from "child_process";
79
+ import { execFile, spawn as spawn2 } from "child_process";
80
80
  import { mkdir as mkdir3, readFile as readFile3, rm as rm3, stat as stat3, writeFile as writeFile3 } from "fs/promises";
81
81
  import { homedir as homedir3 } from "os";
82
82
  import { join as join3 } from "path";
@@ -92,21 +92,20 @@ description: Ask a mutual contact's local agent a question through Parley, with
92
92
 
93
93
  Parley lets your human exchange one-off questions with a friend's local agent. The relay is ephemeral: it keeps accounts, contacts, unexpired invites, and only active requests. Every agent-written reply requires the owner's Discord approval before delivery.
94
94
 
95
- ## Setup
95
+ ## Setup (in order; verify with \`parley doctor\` after each step)
96
96
 
97
- 1. Install the CLI and authenticate: \`parley login\`.
98
- 2. Create \`~/.parley/config.toml\` with a command that reads stdin and writes a draft to stdout:
97
+ You can run this entire setup for your human. Steps 2 and 6 need them present (browser login, Discord taps); everything else is yours to do.
99
98
 
100
- \`\`\`toml
101
- command = "claude -p"
102
- context = """
103
- You are answering questions asked by my friends through Parley.
104
- Use only the local paths my human permits. Never reveal secrets or tokens.
105
- """
106
- \`\`\`
99
+ 1. Install: \`npm install -g parley-chat\` (requires node >= 22; the background responder is macOS-only). If \`parley\` is missing afterwards, npm's global bin directory is not on PATH — \`npm bin -g\` shows where it is.
100
+ 2. \`parley login\` — opens Discord OAuth in the browser; your human completes it. This also installs the Parley Discord app on their account so approval DMs can reach them.
101
+ 3. \`parley init\` — scaffolds \`~/.parley/config.toml\` with a safe default handler (\`claude -p\`). Then tailor it WITH your human: \`command\` is any shell command that reads a prompt on stdin and writes the reply to stdout; \`context\` should say who asks, what paths/topics are fair game, and what is off-limits. Ask your human what they want to share before writing it.
102
+ 4. \`parley install\` registers the always-on responder (launchd). \`parley listen\` is the foreground equivalent for testing.
103
+ 5. \`parley doctor\` — every line should read \`ok\`. Fix the first \`FAIL\` (each names its own fix) and re-run until clean. Do not continue until doctor passes.
104
+ 6. Connect a friend: \`parley invite\` prints a code + link for your human to send. The friend completes steps 1-5, runs \`parley join CODE\`, and the inviter taps **Connect** in the Discord DM. \`parley contacts\` then lists the friend on both sides.
107
105
 
108
- 3. Start the responder with \`parley listen\`, or install the macOS launchd agent once with \`parley install\`.
109
- 4. Add a friend: \`parley invite\`, send the six-character code, then have them run \`parley join CODE\`. Their redemption stays pending until you tap **Connect** in the Discord DM.
106
+ ## Troubleshooting
107
+
108
+ \`parley doctor\` is the first move for any problem — it checks node version, relay reachability, credentials, token validity, config parsing, handler availability, the launchd agent, and responder connectivity, in dependency order. \`parley status\` is the short version; \`parley logs\` tails the responder log. An ask failing with "offline" means the RECIPIENT's responder is down — their doctor run, not yours.
110
109
 
111
110
  ## Commands
112
111
 
@@ -119,6 +118,8 @@ Use only the local paths my human permits. Never reveal secrets or tokens.
119
118
  - \`parley ask @NAME "question"\`: blocks for a one-off response. It fails immediately when the responder is offline.
120
119
  - \`parley listen\`: runs the local responder in the foreground.
121
120
  - \`parley install\` / \`parley uninstall\`: manages the macOS launchd responder.
121
+ - \`parley init\`: scaffolds \`~/.parley/config.toml\` with a safe default (no-op if it exists).
122
+ - \`parley doctor\`: full diagnostic — checks every setup dependency in order and names the fix for each failure.
122
123
  - \`parley status\`: reports local responder and configuration status.
123
124
  - \`parley logs\`: tails the launchd responder log.
124
125
  - \`parley skills --write\`: writes this document to \`.claude/skills/parley/SKILL.md\` in the current directory.
@@ -484,6 +485,118 @@ async function logs() {
484
485
  `));
485
486
  return 0;
486
487
  }
488
+ var defaultConfig = `command = "claude -p"
489
+ context = """
490
+ You are answering questions asked by my friends through Parley.
491
+ Answer helpfully from my machine's context, but follow these rules:
492
+ - Read-only: do not modify files or run anything with side effects.
493
+ - Never reveal secrets, tokens, credentials, or private messages.
494
+ - If a question needs something private, say I should answer directly.
495
+ """
496
+ `;
497
+ async function init() {
498
+ const path = join3(parleyHome2(), "config.toml");
499
+ if (await fileExists2(path)) {
500
+ process.stdout.write(`config already exists at ${path}
501
+ `);
502
+ return 0;
503
+ }
504
+ await mkdir3(parleyHome2(), { recursive: true, mode: 448 });
505
+ await writeFile3(path, defaultConfig, { mode: 384 });
506
+ process.stdout.write(`wrote ${path}
507
+ edit the command and context to fit your setup, then run: parley install
508
+ `);
509
+ return 0;
510
+ }
511
+ async function doctor() {
512
+ const checks = [];
513
+ const nodeVersion = process.versions.node;
514
+ checks.push(["node", `${nodeVersion} (need >= 22)`, Number(nodeVersion.split(".")[0]) >= 22]);
515
+ checks.push(["platform", process.platform, process.platform === "darwin"]);
516
+ checks.push(["relay url", relayUrl2(), true]);
517
+ const reachable = await fetch(relayUrl2()).then((response) => response.ok).catch(() => false);
518
+ checks.push(["relay reachable", reachable ? "yes" : "no \u2014 check network", reachable]);
519
+ const hasCredentials = await fileExists2(join3(parleyHome2(), "credentials.json"));
520
+ checks.push(["credentials", hasCredentials ? "present" : "missing \u2014 run: parley login", hasCredentials]);
521
+ if (hasCredentials) {
522
+ const account = await credentials2().catch(() => null);
523
+ const authed = account ? await apiJson("/v1/contacts").then(() => true).catch(() => false) : false;
524
+ checks.push(["logged in as", account?.account.username ?? "unreadable", account !== null]);
525
+ checks.push(["token valid", authed ? "yes" : "no \u2014 run: parley login", authed]);
526
+ }
527
+ const hasConfig = await fileExists2(join3(parleyHome2(), "config.toml"));
528
+ checks.push(["config", hasConfig ? join3(parleyHome2(), "config.toml") : "missing \u2014 run: parley init", hasConfig]);
529
+ if (hasConfig) {
530
+ const local = await config2().catch(() => null);
531
+ checks.push(["config parses", local ? `command: ${local.command}` : "no \u2014 invalid toml, run: parley init after moving it aside", local !== null]);
532
+ if (local) {
533
+ const handler = await new Promise((resolve) => {
534
+ const child = execFile("sh", ["-c", `command -v ${JSON.stringify(local.command.split(" ")[0] === "cd" ? local.command.split("&&").at(-1)?.trim().split(" ")[0] ?? "" : local.command.split(" ")[0])}`], (error) => resolve(error === null));
535
+ setTimeout(() => {
536
+ child.kill();
537
+ resolve(false);
538
+ }, 5000);
539
+ });
540
+ checks.push(["handler command found", handler ? "yes" : "no \u2014 the command in config.toml is not on PATH", handler]);
541
+ if (handler) {
542
+ process.stdout.write(`running a test draft through your handler (this is what answers your friends; may take a minute)\u2026
543
+ `);
544
+ const probe = await new Promise((resolve) => {
545
+ const child = spawn2("sh", ["-c", local.command], { stdio: ["pipe", "pipe", "pipe"] });
546
+ const out = [];
547
+ const err = [];
548
+ child.stdout.on("data", (chunk) => out.push(chunk));
549
+ child.stderr.on("data", (chunk) => err.push(chunk));
550
+ const timer = setTimeout(() => {
551
+ child.kill();
552
+ resolve({ ok: false, detail: "timed out after 120s \u2014 the handler hung instead of answering" });
553
+ }, 120000);
554
+ child.on("close", (code) => {
555
+ clearTimeout(timer);
556
+ const text2 = Buffer.concat(out).toString("utf8").trim();
557
+ const errText = Buffer.concat(err).toString("utf8").trim().split(`
558
+ `).at(-1) ?? "";
559
+ if (code !== 0)
560
+ resolve({ ok: false, detail: `exited ${code}${errText ? ` \u2014 ${errText}` : ""}` });
561
+ else if (text2.length === 0)
562
+ resolve({ ok: false, detail: "produced no output \u2014 a real ask would auto-deny" });
563
+ else
564
+ resolve({ ok: true, detail: `replied: ${text2.slice(0, 60)}${text2.length > 60 ? "\u2026" : ""}` });
565
+ });
566
+ child.stdin.write(`${local.context}
567
+
568
+ Asker: parley doctor
569
+ Question: reply with the single word "pong" and nothing else
570
+ `);
571
+ child.stdin.end();
572
+ });
573
+ checks.push(["handler test draft", probe.detail, probe.ok]);
574
+ }
575
+ }
576
+ }
577
+ checks.push(["launchagent", await launchAgentState(), await launchAgentState() === "loaded"]);
578
+ const online = hasCredentials ? object(await apiJson("/v1/status").catch(() => null))?.online === true : false;
579
+ checks.push(["responder online", online ? "yes" : "no \u2014 run: parley install (or parley listen)", online]);
580
+ 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(() => []);
583
+ let failures = 0;
584
+ for (const [name, detail, ok] of checks) {
585
+ if (!ok)
586
+ failures += 1;
587
+ process.stdout.write(`${ok ? "ok " : "FAIL"} ${name}: ${detail}
588
+ `);
589
+ }
590
+ if (recentLog.length > 0)
591
+ process.stdout.write(`recent log:
592
+ ${recentLog.map((line) => ` ${line}`).join(`
593
+ `)}
594
+ `);
595
+ process.stdout.write(failures === 0 ? `all checks passed
596
+ ` : `${failures} check(s) failed \u2014 fix the first FAIL, top to bottom, then re-run parley doctor
597
+ `);
598
+ return failures === 0 ? 0 : 1;
599
+ }
487
600
  async function skills(write) {
488
601
  if (write) {
489
602
  const path = join3(process.cwd(), ".claude/skills/parley");
@@ -527,8 +640,12 @@ async function main() {
527
640
  return logs();
528
641
  case "skills":
529
642
  return skills(args[0] === "--write");
643
+ case "init":
644
+ return init();
645
+ case "doctor":
646
+ return doctor();
530
647
  default:
531
- process.stderr.write(`usage: parley <login|logout|invite|join|contacts|remove|ask|listen|install|uninstall|status|logs|skills>
648
+ process.stderr.write(`usage: parley <login|logout|invite|join|contacts|remove|ask|listen|install|uninstall|init|doctor|status|logs|skills>
532
649
  `);
533
650
  return 1;
534
651
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parley-chat",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
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": {