@parley-im/parley 0.1.1 → 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/README.md +23 -17
- package/bin/parley +7 -0
- package/dist/access.js +57 -0
- package/dist/account.js +5 -1
- package/dist/agents/claude.js +22 -10
- package/dist/agents/codex.js +17 -6
- package/dist/agents/process.js +35 -4
- package/dist/api.js +22 -11
- package/dist/autostart.js +86 -0
- package/dist/charter.js +5 -4
- package/dist/cli.js +64 -17
- package/dist/config.js +53 -12
- package/dist/control.js +16 -10
- package/dist/daemon.js +34 -10
- package/dist/db.js +287 -53
- package/dist/mcp.js +20 -20
- package/dist/models.js +8 -2
- package/dist/relay.js +32 -14
- package/dist/runner.js +15 -13
- package/dist/seal.js +7 -5
- package/dist/server.js +36 -21
- package/dist/state.js +7 -4
- package/dist/transcript.js +24 -11
- package/dist/version.js +15 -0
- package/package.json +2 -2
- package/web/app.css +21 -13
- package/web/chat.html +1 -1
- package/web/chat.js +120 -54
- package/web/dashboard.js +41 -0
- package/web/ended.html +1 -1
- package/web/missing.html +1 -1
- package/web/panel.html +4 -4
- package/web/panel.js +6 -6
package/dist/cli.js
CHANGED
|
@@ -6,17 +6,19 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { loadConfig, paths, cleanEnv } from "./config.js";
|
|
7
7
|
import { Store } from "./db.js";
|
|
8
8
|
import { ensureServer, stopServer } from "./daemon.js";
|
|
9
|
+
import { enableAutostart, disableAutostart } from "./autostart.js";
|
|
10
|
+
import { VERSION } from "./version.js";
|
|
9
11
|
import { readState, linkFor, dashboardUrl, isServerUp } from "./state.js";
|
|
10
12
|
import { transcript, summaryLine } from "./transcript.js";
|
|
11
13
|
import { startSession, endSession, deleteSession, setReplyLimit } from "./control.js";
|
|
12
14
|
import { login, readCredentials, clearCredentials, relay as registry } from "./account.js";
|
|
13
15
|
const HELP = `parley: someone chats with a read-only Claude or Codex running in this project, on this
|
|
14
|
-
computer, and you get everything they said. Good for feedback from
|
|
16
|
+
computer, and you get everything they said. Good for feedback from guests, or any interview.
|
|
15
17
|
|
|
16
18
|
If you're an agent and the user asks for "a parley" (or to get someone's take on this project):
|
|
17
19
|
|
|
18
20
|
1. From the project folder, make one and give the user the printed link to send:
|
|
19
|
-
parley new --
|
|
21
|
+
parley new --guest "Sam" --brief-file - <<'BRIEF'
|
|
20
22
|
What this is and how they'll try it, what the user most wants to learn, known issues to
|
|
21
23
|
skip. Written to the interviewer, in the terms a user sees (no file paths).
|
|
22
24
|
BRIEF
|
|
@@ -28,7 +30,7 @@ With the parley MCP tools (create_session, list_sessions, get_transcript, end_se
|
|
|
28
30
|
use those instead; they do the same.
|
|
29
31
|
|
|
30
32
|
Commands
|
|
31
|
-
parley new --
|
|
33
|
+
parley new --guest NAME (--brief TEXT | --brief-file FILE|-) Make a session here; prints its link
|
|
32
34
|
[--dir PATH] [--greeting TEXT] [--agent claude|codex] [--model MODEL] [--replies N]
|
|
33
35
|
parley list [--ended | --all] [--dir PATH] Sessions, with new-message counts and replies used
|
|
34
36
|
parley show ID [--new] Transcript as Markdown; --new for what's unread
|
|
@@ -38,10 +40,13 @@ Commands
|
|
|
38
40
|
parley login | logout | whoami Link this computer to parley.im, or unlink it
|
|
39
41
|
parley start | stop | status The background server (holds the parley.im connection)
|
|
40
42
|
parley panel Open the control panel (localhost)
|
|
41
|
-
parley install Add
|
|
43
|
+
parley install Add parley's MCP tools to Claude Code and Codex; start at login
|
|
44
|
+
parley uninstall Undo install (your conversations stay)
|
|
42
45
|
parley serve | mcp Run the server in the foreground | MCP over stdio
|
|
46
|
+
parley --version
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
Conversations and screenshots live in each project's .parley folder (kept out of git), with a
|
|
49
|
+
readable transcript.md per session. Settings and your parley.im login live in ${paths.home}.`;
|
|
45
50
|
const CLI_PATH = fileURLToPath(import.meta.url);
|
|
46
51
|
async function main() {
|
|
47
52
|
const [command, ...rest] = process.argv.slice(2);
|
|
@@ -92,7 +97,7 @@ async function main() {
|
|
|
92
97
|
return;
|
|
93
98
|
}
|
|
94
99
|
case "stop": {
|
|
95
|
-
console.log((await stopServer()) ? "Stopped." : "Not running.");
|
|
100
|
+
console.log((await stopServer(config)) ? "Stopped." : "Not running.");
|
|
96
101
|
return;
|
|
97
102
|
}
|
|
98
103
|
case "status": {
|
|
@@ -110,7 +115,7 @@ async function main() {
|
|
|
110
115
|
args: rest,
|
|
111
116
|
options: {
|
|
112
117
|
dir: { type: "string" },
|
|
113
|
-
|
|
118
|
+
guest: { type: "string" },
|
|
114
119
|
brief: { type: "string" },
|
|
115
120
|
"brief-file": { type: "string" },
|
|
116
121
|
greeting: { type: "string" },
|
|
@@ -122,22 +127,25 @@ async function main() {
|
|
|
122
127
|
// "-" reads the brief from stdin, so a long one can come in a heredoc.
|
|
123
128
|
const file = values["brief-file"];
|
|
124
129
|
const brief = file ? readFileSync(file === "-" ? 0 : file, "utf8") : values.brief;
|
|
125
|
-
if (!values.
|
|
126
|
-
fail("Need --
|
|
130
|
+
if (!values.guest || !brief)
|
|
131
|
+
fail("Need --guest and --brief (or --brief-file).");
|
|
127
132
|
if (values.agent && values.agent !== "claude" && values.agent !== "codex")
|
|
128
133
|
fail("--agent is claude or codex.");
|
|
134
|
+
const replies = values.replies === undefined ? null : Number(values.replies);
|
|
135
|
+
if (replies !== null && !(Number.isInteger(replies) && replies > 0))
|
|
136
|
+
fail("--replies is a whole number above zero.");
|
|
129
137
|
const store = new Store();
|
|
130
138
|
const made = await startSession(store, config, {
|
|
131
139
|
projectDir: resolve(values.dir ?? process.cwd()),
|
|
132
|
-
|
|
140
|
+
guest: values.guest,
|
|
133
141
|
brief,
|
|
134
142
|
greeting: values.greeting,
|
|
135
|
-
agent: values.agent ??
|
|
143
|
+
agent: values.agent ?? null,
|
|
136
144
|
model: values.model,
|
|
137
|
-
replyLimit:
|
|
145
|
+
replyLimit: replies,
|
|
138
146
|
}).catch((error) => fail(error.message));
|
|
139
147
|
const { session } = made;
|
|
140
|
-
console.log(`Session ${session.id} for ${session.
|
|
148
|
+
console.log(`Session ${session.id} for ${session.guest} in ${session.project_dir}`);
|
|
141
149
|
console.log(`Link: ${linkFor(session, config)}`);
|
|
142
150
|
console.log(`Replies allowed: ${session.reply_limit}`);
|
|
143
151
|
console.log(`Watch: ${dashboardUrl(config, session)}`);
|
|
@@ -196,11 +204,12 @@ async function main() {
|
|
|
196
204
|
if (session.status !== "open")
|
|
197
205
|
return console.log(`${session.id} had already ended.`);
|
|
198
206
|
await endSession(config, store, session.id);
|
|
199
|
-
console.log(`Ended ${session.id} (${session.
|
|
207
|
+
console.log(`Ended ${session.id} (${session.guest} on ${session.project_name}). Its link no longer works.`);
|
|
200
208
|
}
|
|
201
209
|
else {
|
|
202
210
|
await deleteSession(config, store, session.id);
|
|
203
|
-
console.log(`Deleted ${session.id} (${session.
|
|
211
|
+
console.log(`Deleted ${session.id} (${session.guest} on ${session.project_name}) and its screenshots.`);
|
|
212
|
+
console.log(`${session.agent === "codex" ? "Codex" : "Claude Code"} keeps its own log of the conversation (in ${session.agent === "codex" ? "~/.codex/sessions" : "~/.claude/projects"}); delete it there too if you want it gone everywhere.`);
|
|
204
213
|
}
|
|
205
214
|
return;
|
|
206
215
|
}
|
|
@@ -211,6 +220,13 @@ async function main() {
|
|
|
211
220
|
}
|
|
212
221
|
case "install":
|
|
213
222
|
return install(config);
|
|
223
|
+
case "uninstall":
|
|
224
|
+
return uninstall(config);
|
|
225
|
+
case "version":
|
|
226
|
+
case "--version":
|
|
227
|
+
case "-v":
|
|
228
|
+
console.log(VERSION);
|
|
229
|
+
return;
|
|
214
230
|
case undefined:
|
|
215
231
|
case "help":
|
|
216
232
|
case "--help":
|
|
@@ -221,7 +237,7 @@ async function main() {
|
|
|
221
237
|
fail(`Unknown command: ${command}\n\n${HELP}`);
|
|
222
238
|
}
|
|
223
239
|
}
|
|
224
|
-
function install(config) {
|
|
240
|
+
async function install(config) {
|
|
225
241
|
const launch = [process.execPath, "--no-warnings", CLI_PATH, "mcp"];
|
|
226
242
|
const env = cleanEnv();
|
|
227
243
|
const run = (bin, args) => {
|
|
@@ -247,8 +263,34 @@ function install(config) {
|
|
|
247
263
|
}
|
|
248
264
|
else
|
|
249
265
|
console.log("Codex: not found, skipped.");
|
|
266
|
+
// The server holds your links; it should come back after a restart.
|
|
267
|
+
await stopServer(config);
|
|
268
|
+
console.log(`Server: ${enableAutostart(CLI_PATH)}`);
|
|
269
|
+
if (!(await ensureServer(config)))
|
|
270
|
+
console.log(`The server didn't come up. See ${paths.log}`);
|
|
250
271
|
console.log(`\nStart a new Claude or Codex session in any project and ask it to make a parley for someone.`);
|
|
251
272
|
}
|
|
273
|
+
// Undoes `parley install`. Your conversations stay in each project's .parley
|
|
274
|
+
// folder, and settings and login in ~/.parley.
|
|
275
|
+
async function uninstall(config) {
|
|
276
|
+
const run = (bin, args) => {
|
|
277
|
+
try {
|
|
278
|
+
execFileSync(bin, args, { env: cleanEnv(), stdio: "pipe" });
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
if (config.claude.bin.startsWith("/") && run(config.claude.bin, ["mcp", "remove", "--scope", "user", "parley"]))
|
|
286
|
+
console.log("Claude Code: removed the parley MCP server.");
|
|
287
|
+
if (config.codex.bin.startsWith("/") && run(config.codex.bin, ["mcp", "remove", "parley"]))
|
|
288
|
+
console.log("Codex: removed the parley MCP server.");
|
|
289
|
+
console.log(disableAutostart() ?? "Wasn't set to start at login.");
|
|
290
|
+
if (await stopServer(config))
|
|
291
|
+
console.log("Stopped the server.");
|
|
292
|
+
console.log(`\nYour conversations are still in each project's .parley folder, and settings and login in ${paths.home}.`);
|
|
293
|
+
}
|
|
252
294
|
async function printRelay(config) {
|
|
253
295
|
const creds = readCredentials(config);
|
|
254
296
|
if (!creds)
|
|
@@ -269,4 +311,9 @@ function fail(message) {
|
|
|
269
311
|
console.error(message);
|
|
270
312
|
process.exit(1);
|
|
271
313
|
}
|
|
272
|
-
|
|
314
|
+
// Mistakes in how parley was called get a sentence, not a stack trace.
|
|
315
|
+
main().catch((error) => {
|
|
316
|
+
if (typeof error?.code === "string" && error.code.startsWith("ERR_PARSE_ARGS"))
|
|
317
|
+
fail(`${error.message}\nRun parley on its own for help.`);
|
|
318
|
+
fail(process.env.PARLEY_DEBUG ? String(error?.stack ?? error) : String(error?.message ?? error));
|
|
319
|
+
});
|
package/dist/config.js
CHANGED
|
@@ -1,17 +1,53 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, accessSync, constants } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { join, delimiter } from "node:path";
|
|
3
|
+
import { join, delimiter, basename } from "node:path";
|
|
4
4
|
export const HOME = process.env.PARLEY_HOME ?? join(homedir(), ".parley");
|
|
5
|
+
// Settings, your parley.im login and the server's state. Conversations live
|
|
6
|
+
// in each project's own .parley folder (see db.ts); projects.json lists them.
|
|
5
7
|
export const paths = {
|
|
6
8
|
home: HOME,
|
|
7
9
|
config: join(HOME, "config.json"),
|
|
8
|
-
|
|
10
|
+
projects: join(HOME, "projects.json"),
|
|
9
11
|
state: join(HOME, "server.json"),
|
|
10
12
|
log: join(HOME, "server.log"),
|
|
11
|
-
uploads: join(HOME, "uploads"),
|
|
12
13
|
// Your computer's link to a parley.im account, from `parley login`.
|
|
13
14
|
credentials: join(HOME, "credentials.json"),
|
|
14
15
|
};
|
|
16
|
+
export const BLOCKED_BY_DEFAULT = [
|
|
17
|
+
// Environment files and other places secrets usually live.
|
|
18
|
+
"**/.env",
|
|
19
|
+
"**/.env.*",
|
|
20
|
+
"**/.dev.vars",
|
|
21
|
+
"**/secrets/**",
|
|
22
|
+
"**/secrets.json",
|
|
23
|
+
"**/secrets.yml",
|
|
24
|
+
"**/secrets.yaml",
|
|
25
|
+
"**/credentials.json",
|
|
26
|
+
"**/*.tfstate",
|
|
27
|
+
"**/*.tfvars",
|
|
28
|
+
// Keys and certificates.
|
|
29
|
+
"**/*.pem",
|
|
30
|
+
"**/*.key",
|
|
31
|
+
"**/*.p12",
|
|
32
|
+
"**/*.pfx",
|
|
33
|
+
"**/*.jks",
|
|
34
|
+
"**/*.keystore",
|
|
35
|
+
"**/id_rsa*",
|
|
36
|
+
"**/id_dsa*",
|
|
37
|
+
"**/id_ecdsa*",
|
|
38
|
+
"**/id_ed25519*",
|
|
39
|
+
// Tool logins.
|
|
40
|
+
"**/.ssh/**",
|
|
41
|
+
"**/.aws/**",
|
|
42
|
+
"**/.gnupg/**",
|
|
43
|
+
"**/.kube/**",
|
|
44
|
+
"**/.docker/**",
|
|
45
|
+
"**/.npmrc",
|
|
46
|
+
"**/.pypirc",
|
|
47
|
+
"**/.netrc",
|
|
48
|
+
"**/.git-credentials",
|
|
49
|
+
"**/.git/config",
|
|
50
|
+
];
|
|
15
51
|
const defaults = {
|
|
16
52
|
relayUrl: "https://parley.im",
|
|
17
53
|
port: 4747,
|
|
@@ -20,9 +56,10 @@ const defaults = {
|
|
|
20
56
|
replyLimit: 100,
|
|
21
57
|
claude: { bin: "claude", model: null, effort: null },
|
|
22
58
|
codex: { bin: "codex", model: null, effort: null },
|
|
59
|
+
interviewer: { readConversations: false, blocked: BLOCKED_BY_DEFAULT, allowHomeFolder: false },
|
|
23
60
|
};
|
|
24
61
|
export function ensureHome() {
|
|
25
|
-
mkdirSync(paths.
|
|
62
|
+
mkdirSync(paths.home, { recursive: true });
|
|
26
63
|
}
|
|
27
64
|
export function loadConfig() {
|
|
28
65
|
ensureHome();
|
|
@@ -39,18 +76,20 @@ export function loadConfig() {
|
|
|
39
76
|
replyLimit: pick("replyLimit"),
|
|
40
77
|
claude: { ...defaults.claude, ...stored.claude },
|
|
41
78
|
codex: { ...defaults.codex, ...stored.codex },
|
|
79
|
+
interviewer: { ...defaults.interviewer, ...stored.interviewer },
|
|
42
80
|
};
|
|
43
81
|
// The server is often started from inside an MCP server with a thin PATH,
|
|
44
|
-
// so binaries are pinned to absolute paths the first time they are found
|
|
45
|
-
|
|
82
|
+
// so binaries are pinned to absolute paths the first time they are found,
|
|
83
|
+
// and found again if they move (say, from npm to a native installer).
|
|
84
|
+
let changed = !existsSync(paths.config) || Object.keys(stored).some((key) => !(key in defaults)) || !stored.interviewer;
|
|
46
85
|
for (const [get, set] of [
|
|
47
86
|
[() => config.claude.bin, (v) => (config.claude.bin = v)],
|
|
48
87
|
[() => config.codex.bin, (v) => (config.codex.bin = v)],
|
|
49
88
|
]) {
|
|
50
89
|
const current = get();
|
|
51
|
-
if (current.startsWith("/"))
|
|
90
|
+
if (current.startsWith("/") && existsSync(current))
|
|
52
91
|
continue;
|
|
53
|
-
const found = findBin(current);
|
|
92
|
+
const found = findBin(basename(current));
|
|
54
93
|
if (found) {
|
|
55
94
|
set(found);
|
|
56
95
|
changed = true;
|
|
@@ -83,13 +122,15 @@ export function findBin(name) {
|
|
|
83
122
|
}
|
|
84
123
|
return null;
|
|
85
124
|
}
|
|
86
|
-
// Agents run as you, on your own subscription.
|
|
87
|
-
//
|
|
88
|
-
// CLIs fall back to your normal login.
|
|
125
|
+
// Agents run as you, on your own subscription. What a host session (Claude
|
|
126
|
+
// Code itself, an MCP client) put in the environment is dropped, API keys
|
|
127
|
+
// included, so the CLIs fall back to your normal login. Settings that choose
|
|
128
|
+
// where that login lives (a config folder, Bedrock, Vertex, a proxy) stay.
|
|
129
|
+
const KEEP = new Set(["CLAUDE_CONFIG_DIR", "CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "ANTHROPIC_BASE_URL", "ANTHROPIC_VERTEX_PROJECT_ID"]);
|
|
89
130
|
export function cleanEnv() {
|
|
90
131
|
const env = {};
|
|
91
132
|
for (const [key, value] of Object.entries(process.env)) {
|
|
92
|
-
if (/^(CLAUDE|ANTHROPIC_|MCP_)/.test(key))
|
|
133
|
+
if (/^(CLAUDE|ANTHROPIC_|MCP_)/.test(key) && !KEEP.has(key))
|
|
93
134
|
continue;
|
|
94
135
|
env[key] = value;
|
|
95
136
|
}
|
package/dist/control.js
CHANGED
|
@@ -1,28 +1,34 @@
|
|
|
1
|
-
import { existsSync, statSync } from "node:fs";
|
|
2
|
-
import { isAbsolute
|
|
1
|
+
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
3
|
import { isServerUp } from "./state.js";
|
|
4
4
|
import { readCredentials, relay as registry } from "./account.js";
|
|
5
5
|
import { ensureServer } from "./daemon.js";
|
|
6
|
+
import { agentProblem, checkProjectFolder, defaultAgent } from "./access.js";
|
|
6
7
|
// The one way sessions get made, from the CLI, MCP tools or your control panel.
|
|
7
8
|
export function createSession(store, config, input) {
|
|
8
9
|
if (!isAbsolute(input.projectDir))
|
|
9
10
|
throw new Error(`Use the full path of the project folder, not ${input.projectDir}.`);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
if (!existsSync(input.projectDir) || !statSync(input.projectDir).isDirectory())
|
|
12
|
+
throw new Error(`Not a folder: ${input.projectDir}`);
|
|
13
|
+
const projectDir = realpathSync(input.projectDir);
|
|
14
|
+
checkProjectFolder(projectDir, config);
|
|
15
|
+
const guest = input.guest.trim();
|
|
14
16
|
const brief = input.brief.trim();
|
|
15
|
-
if (!
|
|
17
|
+
if (!guest)
|
|
16
18
|
throw new Error("Say who the session is for.");
|
|
17
19
|
if (!brief)
|
|
18
20
|
throw new Error("Write a brief for the interviewer.");
|
|
19
21
|
const replyLimit = input.replyLimit ?? config.replyLimit;
|
|
20
22
|
if (!Number.isInteger(replyLimit) || replyLimit < 1)
|
|
21
23
|
throw new Error("The reply limit has to be a whole number above zero.");
|
|
22
|
-
|
|
24
|
+
const agent = input.agent ?? defaultAgent(config);
|
|
25
|
+
const problem = agentProblem(config, agent);
|
|
26
|
+
if (problem)
|
|
27
|
+
throw new Error(problem);
|
|
28
|
+
return store.createSession({ ...input, projectDir, guest, brief, replyLimit, agent });
|
|
23
29
|
}
|
|
24
30
|
// Changes go through the server when it's running, so a reply in progress
|
|
25
|
-
// stops, any open
|
|
31
|
+
// stops, any open guest page updates at once, and parley.im hears about it.
|
|
26
32
|
async function viaServer(config, path, body) {
|
|
27
33
|
const res = await fetch(`http://localhost:${config.dashboardPort}${path}`, {
|
|
28
34
|
method: "POST",
|
|
@@ -39,7 +45,7 @@ async function viaServer(config, path, body) {
|
|
|
39
45
|
export async function startSession(store, config, input) {
|
|
40
46
|
if (await ensureServer(config)) {
|
|
41
47
|
const made = await viaServer(config, "/api/sessions", input);
|
|
42
|
-
return { session: store.session(made.id), shared: made.shared, serverUp: true };
|
|
48
|
+
return { session: made.session ?? store.session(made.id), shared: made.shared, serverUp: true };
|
|
43
49
|
}
|
|
44
50
|
const session = createSession(store, config, input);
|
|
45
51
|
return { session, shared: false, serverUp: false };
|
package/dist/daemon.js
CHANGED
|
@@ -1,15 +1,30 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { openSync } from "node:fs";
|
|
2
|
+
import { openSync, rmSync } from "node:fs";
|
|
3
3
|
import { join, dirname } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { paths, cleanEnv } from "./config.js";
|
|
6
|
-
import { readState,
|
|
6
|
+
import { readState, serverHealth } from "./state.js";
|
|
7
|
+
import { VERSION, compareVersions } from "./version.js";
|
|
7
8
|
// cli.ts in a checkout, cli.js once built for npm.
|
|
8
9
|
const CLI = join(dirname(fileURLToPath(import.meta.url)), import.meta.url.endsWith(".ts") ? "cli.ts" : "cli.js");
|
|
9
10
|
// Starts the server in the background if it isn't running, and waits until
|
|
10
|
-
// it answers. It holds this computer's connection to parley.im.
|
|
11
|
+
// it answers. It holds this computer's connection to parley.im. A server from
|
|
12
|
+
// an older parley is replaced; one newer than this process means this process
|
|
13
|
+
// is the stale one (an MCP server started before an update).
|
|
11
14
|
export async function ensureServer(config, waitMs = 20_000) {
|
|
12
|
-
|
|
15
|
+
const running = await serverHealth(config);
|
|
16
|
+
if (running?.version && running.version !== VERSION) {
|
|
17
|
+
if (compareVersions(running.version, VERSION) > 0) {
|
|
18
|
+
throw new Error(`parley was updated to ${running.version}, but this process is still ${VERSION}. Restart this Claude Code or Codex session (or the parley command) to use the new version.`);
|
|
19
|
+
}
|
|
20
|
+
await stopServer(config);
|
|
21
|
+
await waitUntilDown(config);
|
|
22
|
+
}
|
|
23
|
+
else if (running && !running.version) {
|
|
24
|
+
await stopServer(config);
|
|
25
|
+
await waitUntilDown(config);
|
|
26
|
+
}
|
|
27
|
+
if (!(await serverHealth(config))) {
|
|
13
28
|
const log = openSync(paths.log, "a");
|
|
14
29
|
const child = spawn(process.execPath, ["--no-warnings", CLI, "serve"], {
|
|
15
30
|
detached: true,
|
|
@@ -22,18 +37,27 @@ export async function ensureServer(config, waitMs = 20_000) {
|
|
|
22
37
|
const deadline = Date.now() + waitMs;
|
|
23
38
|
while (Date.now() < deadline) {
|
|
24
39
|
const state = readState();
|
|
25
|
-
if (state && (await
|
|
40
|
+
if (state && (await serverHealth(config)))
|
|
26
41
|
return state;
|
|
27
42
|
await new Promise((r) => setTimeout(r, 400));
|
|
28
43
|
}
|
|
29
|
-
return (await
|
|
44
|
+
return (await serverHealth(config)) ? readState() : null;
|
|
30
45
|
}
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
46
|
+
async function waitUntilDown(config, waitMs = 10_000) {
|
|
47
|
+
const deadline = Date.now() + waitMs;
|
|
48
|
+
while (Date.now() < deadline && (await serverHealth(config)))
|
|
49
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
50
|
+
}
|
|
51
|
+
// Stops the server, but only a process that answers as parley's server: the
|
|
52
|
+
// pid in server.json may be stale and belong to something else by now.
|
|
53
|
+
export async function stopServer(config) {
|
|
54
|
+
const running = await serverHealth(config);
|
|
55
|
+
if (!running) {
|
|
56
|
+
rmSync(paths.state, { force: true });
|
|
34
57
|
return false;
|
|
58
|
+
}
|
|
35
59
|
try {
|
|
36
|
-
process.kill(
|
|
60
|
+
process.kill(running.pid, "SIGTERM");
|
|
37
61
|
return true;
|
|
38
62
|
}
|
|
39
63
|
catch {
|