@getpaseo/cli 0.1.91-beta.1 → 0.1.91
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.
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
|
2
2
|
import { collectMultiple } from "../../utils/command-options.js";
|
|
3
3
|
import { agentRunSchema } from "./run.js";
|
|
4
|
-
const IMPORT_PROVIDER_LIST = ["claude", "codex", "opencode", "pi", "acp"];
|
|
5
|
-
const IMPORT_PROVIDERS = new Set(IMPORT_PROVIDER_LIST);
|
|
6
|
-
const IMPORT_PROVIDER_HELP = IMPORT_PROVIDER_LIST.join(", ");
|
|
7
4
|
export function addImportOptions(cmd) {
|
|
8
5
|
return cmd
|
|
9
6
|
.description("Import an existing provider session as a Paseo agent")
|
|
10
7
|
.argument("<id>", "Provider session/thread ID to import")
|
|
11
|
-
.requiredOption("--provider <provider>",
|
|
8
|
+
.requiredOption("--provider <provider>", "Agent provider id")
|
|
12
9
|
.option("--cwd <path>", "Working directory for providers that require it")
|
|
13
10
|
.option("--label <key=value>", "Add label(s) to the agent (can be used multiple times)", collectMultiple, []);
|
|
14
11
|
}
|
|
@@ -30,13 +27,6 @@ function parseImportProvider(provider) {
|
|
|
30
27
|
details: "Usage: paseo import --provider <provider> <id>",
|
|
31
28
|
};
|
|
32
29
|
}
|
|
33
|
-
if (!IMPORT_PROVIDERS.has(normalizedProvider)) {
|
|
34
|
-
throw {
|
|
35
|
-
code: "INVALID_PROVIDER",
|
|
36
|
-
message: `Unsupported provider: ${normalizedProvider}`,
|
|
37
|
-
details: `Supported providers: ${IMPORT_PROVIDER_HELP}`,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
30
|
return normalizedProvider;
|
|
41
31
|
}
|
|
42
32
|
function parseImportLabels(labelFlags) {
|
|
@@ -20,7 +20,7 @@ export type AgentLsResult = ListResult<AgentListItem>;
|
|
|
20
20
|
export interface AgentLsOptions extends CommandOptions {
|
|
21
21
|
/** -a: Include archived agents */
|
|
22
22
|
all?: boolean;
|
|
23
|
-
/**
|
|
23
|
+
/** -g: List agents across all directories */
|
|
24
24
|
global?: boolean;
|
|
25
25
|
/** Filter by specific status */
|
|
26
26
|
status?: string;
|
|
@@ -31,11 +31,13 @@ export interface AgentLsOptions extends CommandOptions {
|
|
|
31
31
|
/** Filter by thinking option ID */
|
|
32
32
|
thinking?: string;
|
|
33
33
|
}
|
|
34
|
-
export declare function buildAgentLsFetchOptions(options: Pick<AgentLsOptions, "all" | "label" | "thinking">): FetchAgentsOptions;
|
|
34
|
+
export declare function buildAgentLsFetchOptions(options: Pick<AgentLsOptions, "all" | "global" | "label" | "thinking">): FetchAgentsOptions;
|
|
35
35
|
/**
|
|
36
36
|
* Agent ls command semantics:
|
|
37
37
|
* - `paseo agent ls` → active non-archived agents
|
|
38
|
-
* - `paseo agent ls -
|
|
38
|
+
* - `paseo agent ls -g` → global non-archived agents
|
|
39
|
+
* - `paseo agent ls -a` → active agents, including archived
|
|
40
|
+
* - `paseo agent ls -ag` → global agents, including archived
|
|
39
41
|
*/
|
|
40
42
|
export declare function runLsCommand(options: AgentLsOptions, _command: Command): Promise<AgentLsResult>;
|
|
41
43
|
export {};
|
|
@@ -5,7 +5,7 @@ export function addLsOptions(cmd) {
|
|
|
5
5
|
return cmd
|
|
6
6
|
.description("List agents. By default excludes archived agents.")
|
|
7
7
|
.option("-a, --all", "Include archived agents")
|
|
8
|
-
.option("-g, --global", "
|
|
8
|
+
.option("-g, --global", "List agents across all directories")
|
|
9
9
|
.option("--label <key=value>", "Filter by label (can be used multiple times)", collectMultiple, [])
|
|
10
10
|
.option("--thinking <id>", "Filter by thinking option ID");
|
|
11
11
|
}
|
|
@@ -104,7 +104,7 @@ export function buildAgentLsFetchOptions(options) {
|
|
|
104
104
|
daemonFilter.thinkingOptionId = normalizedThinkingOptionId;
|
|
105
105
|
}
|
|
106
106
|
const fetchOptions = {};
|
|
107
|
-
if (!options.
|
|
107
|
+
if (!options.global) {
|
|
108
108
|
fetchOptions.scope = "active";
|
|
109
109
|
}
|
|
110
110
|
if (Object.keys(daemonFilter).length > 0) {
|
|
@@ -115,7 +115,9 @@ export function buildAgentLsFetchOptions(options) {
|
|
|
115
115
|
/**
|
|
116
116
|
* Agent ls command semantics:
|
|
117
117
|
* - `paseo agent ls` → active non-archived agents
|
|
118
|
-
* - `paseo agent ls -
|
|
118
|
+
* - `paseo agent ls -g` → global non-archived agents
|
|
119
|
+
* - `paseo agent ls -a` → active agents, including archived
|
|
120
|
+
* - `paseo agent ls -ag` → global agents, including archived
|
|
119
121
|
*/
|
|
120
122
|
export async function runLsCommand(options, _command) {
|
|
121
123
|
const host = getDaemonHost({ host: options.host });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { loadConfig, resolvePaseoHome, spawnProcess } from "@getpaseo/server";
|
|
@@ -239,6 +239,84 @@ async function waitForPidExit(pid, timeoutMs) {
|
|
|
239
239
|
}
|
|
240
240
|
return poll();
|
|
241
241
|
}
|
|
242
|
+
async function waitForDaemonUnreachable(state, timeoutMs) {
|
|
243
|
+
const host = resolveTcpHostFromListen(state.listen);
|
|
244
|
+
if (!host) {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
const reachableHost = host;
|
|
248
|
+
const deadline = Date.now() + timeoutMs;
|
|
249
|
+
async function poll() {
|
|
250
|
+
const client = await tryConnectToDaemon({ host: reachableHost, timeout: 500 });
|
|
251
|
+
if (!client) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
await client.close().catch(() => undefined);
|
|
255
|
+
if (Date.now() >= deadline) {
|
|
256
|
+
const finalClient = await tryConnectToDaemon({
|
|
257
|
+
host: reachableHost,
|
|
258
|
+
timeout: PID_POLL_INTERVAL_MS,
|
|
259
|
+
});
|
|
260
|
+
if (!finalClient) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
await finalClient.close().catch(() => undefined);
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
await sleep(PID_POLL_INTERVAL_MS);
|
|
267
|
+
return poll();
|
|
268
|
+
}
|
|
269
|
+
return poll();
|
|
270
|
+
}
|
|
271
|
+
function removeStalePidFile(state) {
|
|
272
|
+
if (!state.stalePidFile) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
unlinkSync(state.pidPath);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Best-effort cleanup only. The successful lifecycle stop is authoritative.
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function createNotRunningStopResult(state, pid, message) {
|
|
283
|
+
return {
|
|
284
|
+
action: "not_running",
|
|
285
|
+
home: state.home,
|
|
286
|
+
pid,
|
|
287
|
+
forced: false,
|
|
288
|
+
message,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function createStopTimeoutError(state, pid, timeoutMs) {
|
|
292
|
+
if (!state.running) {
|
|
293
|
+
const host = resolveTcpHostFromListen(state.listen);
|
|
294
|
+
return new Error(`Timed out waiting for daemon${host ? ` at ${host}` : ""} to stop after ${Math.ceil(timeoutMs / 1000)}s`);
|
|
295
|
+
}
|
|
296
|
+
return new Error(`Timed out waiting for daemon PID ${pid} to stop after ${Math.ceil(timeoutMs / 1000)}s`);
|
|
297
|
+
}
|
|
298
|
+
async function signalDaemonOwnerForStop(state, pid) {
|
|
299
|
+
if (pid === null) {
|
|
300
|
+
return createNotRunningStopResult(state, null, "Daemon is not running");
|
|
301
|
+
}
|
|
302
|
+
const signaled = await signalProcessTreeOrOwnerSafely(pid, "SIGTERM");
|
|
303
|
+
if (signaled) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
return createNotRunningStopResult(state, pid, "Daemon process was already stopped");
|
|
307
|
+
}
|
|
308
|
+
async function waitForStopAfterRequest(args) {
|
|
309
|
+
const { state, pid, timeoutMs, killTimeoutMs, force } = args;
|
|
310
|
+
let stopped = state.running && pid !== null
|
|
311
|
+
? await waitForPidExit(pid, timeoutMs)
|
|
312
|
+
: await waitForDaemonUnreachable(state, timeoutMs);
|
|
313
|
+
if (!stopped && force && state.running && pid !== null) {
|
|
314
|
+
await signalProcessTreeOrOwnerSafely(pid, "SIGKILL");
|
|
315
|
+
stopped = await waitForPidExit(pid, killTimeoutMs);
|
|
316
|
+
return { stopped, forced: true };
|
|
317
|
+
}
|
|
318
|
+
return { stopped, forced: false };
|
|
319
|
+
}
|
|
242
320
|
function getErrorMessage(error) {
|
|
243
321
|
return error instanceof Error ? error.message : String(error);
|
|
244
322
|
}
|
|
@@ -402,41 +480,31 @@ export async function stopLocalDaemon(options = {}) {
|
|
|
402
480
|
const timeoutMs = options.timeoutMs ?? DEFAULT_STOP_TIMEOUT_MS;
|
|
403
481
|
const killTimeoutMs = options.killTimeoutMs ?? DEFAULT_KILL_TIMEOUT_MS;
|
|
404
482
|
const state = resolveLocalDaemonState({ home: options.home });
|
|
405
|
-
if (!state.pidInfo || !state.running) {
|
|
406
|
-
const staleSuffix = state.stalePidFile && state.pidInfo ? ` (stale PID file for ${state.pidInfo.pid})` : "";
|
|
407
|
-
return {
|
|
408
|
-
action: "not_running",
|
|
409
|
-
home: state.home,
|
|
410
|
-
pid: state.pidInfo?.pid ?? null,
|
|
411
|
-
forced: false,
|
|
412
|
-
message: `Daemon is not running${staleSuffix}`,
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
const pid = state.pidInfo.pid;
|
|
416
483
|
const shutdownAttempt = await requestLifecycleShutdown(state, timeoutMs);
|
|
417
484
|
const lifecycleRequested = shutdownAttempt.requested;
|
|
485
|
+
if (!state.pidInfo || (!state.running && !lifecycleRequested)) {
|
|
486
|
+
const staleSuffix = state.stalePidFile && state.pidInfo ? ` (stale PID file for ${state.pidInfo.pid})` : "";
|
|
487
|
+
return createNotRunningStopResult(state, state.pidInfo?.pid ?? null, `Daemon is not running${staleSuffix}`);
|
|
488
|
+
}
|
|
489
|
+
const pid = state.pidInfo?.pid ?? null;
|
|
418
490
|
const fallbackMessage = shutdownAttempt.requested ? null : shutdownAttempt.reason;
|
|
419
|
-
let forced = false;
|
|
420
491
|
if (!lifecycleRequested) {
|
|
421
|
-
const
|
|
422
|
-
if (
|
|
423
|
-
return
|
|
424
|
-
action: "not_running",
|
|
425
|
-
home: state.home,
|
|
426
|
-
pid,
|
|
427
|
-
forced: false,
|
|
428
|
-
message: "Daemon process was already stopped",
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
let stopped = await waitForPidExit(pid, timeoutMs);
|
|
433
|
-
if (!stopped && options.force) {
|
|
434
|
-
forced = true;
|
|
435
|
-
await signalProcessTreeOrOwnerSafely(pid, "SIGKILL");
|
|
436
|
-
stopped = await waitForPidExit(pid, killTimeoutMs);
|
|
492
|
+
const notRunningResult = await signalDaemonOwnerForStop(state, pid);
|
|
493
|
+
if (notRunningResult)
|
|
494
|
+
return notRunningResult;
|
|
437
495
|
}
|
|
496
|
+
const { stopped, forced } = await waitForStopAfterRequest({
|
|
497
|
+
state,
|
|
498
|
+
pid,
|
|
499
|
+
timeoutMs,
|
|
500
|
+
killTimeoutMs,
|
|
501
|
+
force: options.force,
|
|
502
|
+
});
|
|
438
503
|
if (!stopped) {
|
|
439
|
-
throw
|
|
504
|
+
throw createStopTimeoutError(state, pid, timeoutMs);
|
|
505
|
+
}
|
|
506
|
+
if (lifecycleRequested) {
|
|
507
|
+
removeStalePidFile(state);
|
|
440
508
|
}
|
|
441
509
|
return {
|
|
442
510
|
action: "stopped",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { getOrCreateServerId, findExecutable, execCommand } from "@getpaseo/server";
|
|
3
|
-
import {
|
|
3
|
+
import { connectToDaemon } from "../../utils/client.js";
|
|
4
4
|
import { resolveLocalDaemonState, resolveTcpHostFromListen } from "./local-daemon.js";
|
|
5
5
|
import { resolveNodePathFromPid } from "./runtime-toolchain.js";
|
|
6
6
|
const require = createRequire(import.meta.url);
|
|
@@ -55,7 +55,7 @@ function createStatusSchema(status) {
|
|
|
55
55
|
if (item.key === "Connected Daemon") {
|
|
56
56
|
if (item.value === "reachable")
|
|
57
57
|
return "green";
|
|
58
|
-
if (item.value === "not_probed")
|
|
58
|
+
if (item.value === "not_probed" || item.value === "auth_required")
|
|
59
59
|
return "yellow";
|
|
60
60
|
return "red";
|
|
61
61
|
}
|
|
@@ -100,7 +100,7 @@ function toStatusRows(status) {
|
|
|
100
100
|
else {
|
|
101
101
|
rows.push({
|
|
102
102
|
key: "Agents",
|
|
103
|
-
value:
|
|
103
|
+
value: `Unavailable (${status.agentsUnavailableReason ?? "daemon API not reachable"})`,
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
106
|
if (status.note) {
|
|
@@ -164,10 +164,41 @@ function resolveOwnerLabel(uid, hostname) {
|
|
|
164
164
|
const hostPart = hostname ?? "unknown-host";
|
|
165
165
|
return `${uidPart}@${hostPart}`;
|
|
166
166
|
}
|
|
167
|
+
function classifyDaemonAuthProbeFailure(error) {
|
|
168
|
+
if (!(error instanceof Error))
|
|
169
|
+
return null;
|
|
170
|
+
if (error.message === "Password required")
|
|
171
|
+
return "auth_required";
|
|
172
|
+
if (error.message === "Incorrect password")
|
|
173
|
+
return "auth_failed";
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
function describeDaemonAuthProbeFailure(host, failure) {
|
|
177
|
+
if (failure === "auth_required") {
|
|
178
|
+
return `Daemon is reachable at ${host} but requires a password. Set PASEO_PASSWORD and retry.`;
|
|
179
|
+
}
|
|
180
|
+
return `Daemon is reachable at ${host} but the supplied password was rejected. Check PASEO_PASSWORD and retry.`;
|
|
181
|
+
}
|
|
182
|
+
function describeAgentsUnavailableReason(failure) {
|
|
183
|
+
if (failure === "auth_required")
|
|
184
|
+
return "password required";
|
|
185
|
+
return "incorrect password";
|
|
186
|
+
}
|
|
167
187
|
async function probeDaemonOverWebsocket(args) {
|
|
168
188
|
const { host, state } = args;
|
|
169
|
-
|
|
170
|
-
|
|
189
|
+
let client;
|
|
190
|
+
try {
|
|
191
|
+
client = await connectToDaemon({ host, timeout: 1500 });
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
const authFailure = classifyDaemonAuthProbeFailure(error);
|
|
195
|
+
if (authFailure) {
|
|
196
|
+
return {
|
|
197
|
+
connectedDaemon: authFailure,
|
|
198
|
+
agentsUnavailableReason: describeAgentsUnavailableReason(authFailure),
|
|
199
|
+
note: describeDaemonAuthProbeFailure(host, authFailure),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
171
202
|
if (state.running) {
|
|
172
203
|
return {
|
|
173
204
|
connectedDaemon: "unreachable",
|
|
@@ -247,6 +278,7 @@ function applyProbeToStatus(input) {
|
|
|
247
278
|
runningAgents: probe.runningAgents !== undefined ? probe.runningAgents : input.runningAgents,
|
|
248
279
|
idleAgents: probe.idleAgents !== undefined ? probe.idleAgents : input.idleAgents,
|
|
249
280
|
daemonProviders: probe.daemonProviders ?? input.daemonProviders,
|
|
281
|
+
agentsUnavailableReason: probe.agentsUnavailableReason ?? input.agentsUnavailableReason,
|
|
250
282
|
note: probe.note ? appendNote(input.note, probe.note) : input.note,
|
|
251
283
|
};
|
|
252
284
|
}
|
|
@@ -288,6 +320,7 @@ export async function runStatusCommand(options, _command) {
|
|
|
288
320
|
let idleAgents = null;
|
|
289
321
|
let daemonVersion = null;
|
|
290
322
|
let daemonProviders;
|
|
323
|
+
let agentsUnavailableReason;
|
|
291
324
|
let note;
|
|
292
325
|
if (!state.running && state.stalePidFile && state.pidInfo) {
|
|
293
326
|
localDaemon = "stale_pid";
|
|
@@ -303,6 +336,7 @@ export async function runStatusCommand(options, _command) {
|
|
|
303
336
|
runningAgents,
|
|
304
337
|
idleAgents,
|
|
305
338
|
daemonProviders,
|
|
339
|
+
agentsUnavailableReason,
|
|
306
340
|
note,
|
|
307
341
|
} = applyProbeToStatus({
|
|
308
342
|
probe,
|
|
@@ -313,6 +347,7 @@ export async function runStatusCommand(options, _command) {
|
|
|
313
347
|
runningAgents,
|
|
314
348
|
idleAgents,
|
|
315
349
|
daemonProviders,
|
|
350
|
+
agentsUnavailableReason,
|
|
316
351
|
note,
|
|
317
352
|
}));
|
|
318
353
|
}
|
|
@@ -346,6 +381,7 @@ export async function runStatusCommand(options, _command) {
|
|
|
346
381
|
daemonVersion,
|
|
347
382
|
desktopManaged: state.pidInfo?.desktopManaged === true,
|
|
348
383
|
providers,
|
|
384
|
+
agentsUnavailableReason,
|
|
349
385
|
note,
|
|
350
386
|
};
|
|
351
387
|
return {
|
|
@@ -5,7 +5,7 @@ const PROVIDERS = AGENT_PROVIDER_DEFINITIONS.map((def) => ({
|
|
|
5
5
|
provider: def.id,
|
|
6
6
|
label: def.label,
|
|
7
7
|
status: "available",
|
|
8
|
-
enabled: "Enabled",
|
|
8
|
+
enabled: def.enabledByDefault === false ? "Disabled" : "Enabled",
|
|
9
9
|
defaultMode: def.defaultModeId ?? "-",
|
|
10
10
|
modes: def.modes.length > 0 ? def.modes.map((m) => m.label).join(", ") : "-",
|
|
11
11
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getpaseo/cli",
|
|
3
|
-
"version": "0.1.91
|
|
3
|
+
"version": "0.1.91",
|
|
4
4
|
"description": "Paseo CLI - control your AI coding agents from the command line",
|
|
5
5
|
"bin": {
|
|
6
6
|
"paseo": "bin/paseo"
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@clack/prompts": "^1.0.0",
|
|
30
|
-
"@getpaseo/client": "0.1.91
|
|
31
|
-
"@getpaseo/protocol": "0.1.91
|
|
32
|
-
"@getpaseo/server": "0.1.91
|
|
30
|
+
"@getpaseo/client": "0.1.91",
|
|
31
|
+
"@getpaseo/protocol": "0.1.91",
|
|
32
|
+
"@getpaseo/server": "0.1.91",
|
|
33
33
|
"chalk": "^5.3.0",
|
|
34
34
|
"commander": "^12.0.0",
|
|
35
35
|
"mime-types": "^2.1.35",
|