parley-chat 0.1.0 → 0.2.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.
- package/main.mjs +97 -14
- package/package.json +16 -6
package/main.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
context
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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.
|
|
@@ -418,7 +419,7 @@ async function ask(rawName, question) {
|
|
|
418
419
|
function plist(entry) {
|
|
419
420
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
420
421
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
421
|
-
<plist version="1.0"><dict><key>Label</key><string>sh.parley.listen</string><key>ProgramArguments</key><array><string>${entry}</string><string>listen</string></array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>StandardOutPath</key><string>${join3(parleyHome2(), "logs/listen.log")}</string><key>StandardErrorPath</key><string>${join3(parleyHome2(), "logs/listen.log")}</string></dict></plist>
|
|
422
|
+
<plist version="1.0"><dict><key>Label</key><string>sh.parley.listen</string><key>ProgramArguments</key><array><string>${process.execPath}</string><string>${entry}</string><string>listen</string></array><key>EnvironmentVariables</key><dict><key>PATH</key><string>${process.env.PATH ?? "/usr/bin:/bin"}</string></dict><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>StandardOutPath</key><string>${join3(parleyHome2(), "logs/listen.log")}</string><key>StandardErrorPath</key><string>${join3(parleyHome2(), "logs/listen.log")}</string></dict></plist>
|
|
422
423
|
`;
|
|
423
424
|
}
|
|
424
425
|
async function install() {
|
|
@@ -484,6 +485,84 @@ 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
|
+
}
|
|
542
|
+
}
|
|
543
|
+
checks.push(["launchagent", await launchAgentState(), await launchAgentState() === "loaded"]);
|
|
544
|
+
const online = hasCredentials ? object(await apiJson("/v1/status").catch(() => null))?.online === true : false;
|
|
545
|
+
checks.push(["responder online", online ? "yes" : "no \u2014 run: parley install (or parley listen)", online]);
|
|
546
|
+
const logPath = join3(parleyHome2(), "logs/listen.log");
|
|
547
|
+
const recentLog = await readFile3(logPath, "utf8").then((value) => value.split(`
|
|
548
|
+
`).filter((line) => line.length > 0).slice(-3)).catch(() => []);
|
|
549
|
+
let failures = 0;
|
|
550
|
+
for (const [name, detail, ok] of checks) {
|
|
551
|
+
if (!ok)
|
|
552
|
+
failures += 1;
|
|
553
|
+
process.stdout.write(`${ok ? "ok " : "FAIL"} ${name}: ${detail}
|
|
554
|
+
`);
|
|
555
|
+
}
|
|
556
|
+
if (recentLog.length > 0)
|
|
557
|
+
process.stdout.write(`recent log:
|
|
558
|
+
${recentLog.map((line) => ` ${line}`).join(`
|
|
559
|
+
`)}
|
|
560
|
+
`);
|
|
561
|
+
process.stdout.write(failures === 0 ? `all checks passed
|
|
562
|
+
` : `${failures} check(s) failed \u2014 fix the first FAIL, top to bottom, then re-run parley doctor
|
|
563
|
+
`);
|
|
564
|
+
return failures === 0 ? 0 : 1;
|
|
565
|
+
}
|
|
487
566
|
async function skills(write) {
|
|
488
567
|
if (write) {
|
|
489
568
|
const path = join3(process.cwd(), ".claude/skills/parley");
|
|
@@ -527,8 +606,12 @@ async function main() {
|
|
|
527
606
|
return logs();
|
|
528
607
|
case "skills":
|
|
529
608
|
return skills(args[0] === "--write");
|
|
609
|
+
case "init":
|
|
610
|
+
return init();
|
|
611
|
+
case "doctor":
|
|
612
|
+
return doctor();
|
|
530
613
|
default:
|
|
531
|
-
process.stderr.write(`usage: parley <login|logout|invite|join|contacts|remove|ask|listen|install|uninstall|status|logs|skills>
|
|
614
|
+
process.stderr.write(`usage: parley <login|logout|invite|join|contacts|remove|ask|listen|install|uninstall|init|doctor|status|logs|skills>
|
|
532
615
|
`);
|
|
533
616
|
return 1;
|
|
534
617
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "parley-chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Ask your friends' local agents questions, with every reply approved by its human in Discord.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
"bin": {
|
|
7
|
+
"parley": "parley.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"parley.mjs",
|
|
11
|
+
"main.mjs"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22"
|
|
15
|
+
},
|
|
9
16
|
"license": "MIT",
|
|
10
|
-
"repository": {
|
|
11
|
-
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/rhyssullivan/parley"
|
|
20
|
+
}
|
|
21
|
+
}
|