@hyperdrive.bot/paseo-cli 0.3.45 → 0.3.47

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.
@@ -0,0 +1,68 @@
1
+ /**
2
+ * `paseo agent ls` filtering, over the SAME predicate the app and the MCP tool
3
+ * use (`@hyperdrive.bot/paseo-protocol/agent-filter`).
4
+ *
5
+ * Two things this file exists to avoid. First, a third hand-rolled copy of the
6
+ * semantics: the app has drifted from itself three times this way, and a CLI
7
+ * that disagreed with the UI about what "running" means would be worse, because
8
+ * nobody is looking at a list while it lies. Second, the `--status`/`--cwd`
9
+ * situation this replaces -- both were declared on the options interface and
10
+ * both were applied by `runLsCommand`, but neither was ever registered as a
11
+ * Commander option, so the filtering code was live and completely unreachable.
12
+ */
13
+ import { type AgentArchivedMode, type AgentFilterCriteria, type AgentTimeField, type AgentTimeSelection } from "@hyperdrive.bot/paseo-protocol/agent-filter";
14
+ import { type WorkspaceStateBucket } from "@hyperdrive.bot/paseo-protocol/agent-state-bucket";
15
+ import type { AgentSnapshotPayload } from "@hyperdrive.bot/paseo-protocol/messages";
16
+ /** The filter flags `agent ls` accepts, mirroring the UI's dimensions. */
17
+ export interface AgentLsFilterOptions {
18
+ archived?: string;
19
+ all?: boolean;
20
+ state?: string[];
21
+ origin?: string[];
22
+ tag?: string[];
23
+ notTag?: string[];
24
+ kanban?: string[];
25
+ notKanban?: string[];
26
+ provider?: string[];
27
+ notProvider?: string[];
28
+ timeField?: string;
29
+ window?: string;
30
+ from?: string;
31
+ to?: string;
32
+ text?: string;
33
+ }
34
+ /**
35
+ * `--archived` wins when given; otherwise `-a` means "all" and the default is
36
+ * `hide`, which is the behaviour `agent ls` has always had.
37
+ */
38
+ export declare function resolveLsArchivedMode(options: AgentLsFilterOptions): AgentArchivedMode;
39
+ export declare function resolveLsTimeField(options: AgentLsFilterOptions): AgentTimeField;
40
+ /**
41
+ * A window and an exact range are the same dimension, so the more specific
42
+ * request (the range) wins rather than intersecting into an unexplainable
43
+ * empty list.
44
+ */
45
+ export declare function resolveLsTimeSelection(options: AgentLsFilterOptions): AgentTimeSelection | null;
46
+ /**
47
+ * Builds the criteria.
48
+ *
49
+ * `state` and `origin` are closed sets, so they carry no exclude half: "except
50
+ * running" is the other five buckets, and listing them is not the burden that
51
+ * enumerating an open set of tags would be.
52
+ */
53
+ export declare function buildAgentLsCriteria(options: AgentLsFilterOptions): AgentFilterCriteria;
54
+ type FilterableAgent = Pick<AgentSnapshotPayload, "id" | "status" | "cwd" | "createdAt" | "updatedAt" | "archivedAt" | "labels" | "provider"> & Partial<Pick<AgentSnapshotPayload, "title" | "lastUserMessageAt" | "requiresAttention" | "attentionReason" | "pendingPermissions" | "activeBackgroundTaskCount">>;
55
+ /**
56
+ * `parentAgentId -> live child count`, computed over the corpus BEFORE any
57
+ * narrowing so a parent does not lose its `pending` bucket because its only
58
+ * live child was filtered out.
59
+ */
60
+ export declare function countActiveChildrenByParent(agents: readonly FilterableAgent[]): Map<string, number>;
61
+ /** The derived bucket, matching what the app shows for the same agent. */
62
+ export declare function deriveLsStateBucket(agent: FilterableAgent, activeChildCount: number): WorkspaceStateBucket;
63
+ /** Narrows `agents` with the shared predicate. `now` is injected so tests are deterministic. */
64
+ export declare function applyAgentLsFilter<T extends FilterableAgent>(agents: readonly T[], criteria: AgentFilterCriteria, timeField: AgentTimeField, now: Date): T[];
65
+ /** `agentId -> derived bucket`, so the table can SHOW what `--state` filtered on. */
66
+ export declare function stateBucketsById(agents: readonly FilterableAgent[]): Map<string, WorkspaceStateBucket>;
67
+ export {};
68
+ //# sourceMappingURL=ls-filter.d.ts.map
@@ -0,0 +1,139 @@
1
+ /**
2
+ * `paseo agent ls` filtering, over the SAME predicate the app and the MCP tool
3
+ * use (`@hyperdrive.bot/paseo-protocol/agent-filter`).
4
+ *
5
+ * Two things this file exists to avoid. First, a third hand-rolled copy of the
6
+ * semantics: the app has drifted from itself three times this way, and a CLI
7
+ * that disagreed with the UI about what "running" means would be worse, because
8
+ * nobody is looking at a list while it lies. Second, the `--status`/`--cwd`
9
+ * situation this replaces -- both were declared on the options interface and
10
+ * both were applied by `runLsCommand`, but neither was ever registered as a
11
+ * Commander option, so the filtering code was live and completely unreachable.
12
+ */
13
+ import { agentPassesFilter, deriveAgentOrigin, emptyAgentFilterCriteria, filterSelectionFrom, isAgentDay, } from "@hyperdrive.bot/paseo-protocol/agent-filter";
14
+ import { PARENT_AGENT_ID_LABEL } from "@hyperdrive.bot/paseo-protocol/agent-labels";
15
+ import { deriveAgentStateBucket, } from "@hyperdrive.bot/paseo-protocol/agent-state-bucket";
16
+ const TAG_LABEL_PREFIX = "tag:";
17
+ const KANBAN_LABEL_KEY = "kanban";
18
+ function isTimeWindow(value) {
19
+ return (value === "today" || value === "3d" || value === "7d" || value === "30d" || value === "stale");
20
+ }
21
+ /**
22
+ * `--archived` wins when given; otherwise `-a` means "all" and the default is
23
+ * `hide`, which is the behaviour `agent ls` has always had.
24
+ */
25
+ export function resolveLsArchivedMode(options) {
26
+ const explicit = options.archived?.trim();
27
+ if (explicit === "all" || explicit === "only" || explicit === "hide")
28
+ return explicit;
29
+ return options.all ? "all" : "hide";
30
+ }
31
+ export function resolveLsTimeField(options) {
32
+ return options.timeField === "created" ? "created" : "active";
33
+ }
34
+ /**
35
+ * A window and an exact range are the same dimension, so the more specific
36
+ * request (the range) wins rather than intersecting into an unexplainable
37
+ * empty list.
38
+ */
39
+ export function resolveLsTimeSelection(options) {
40
+ const { from, to, window } = options;
41
+ if (isAgentDay(from) && isAgentDay(to))
42
+ return { kind: "range", range: { from, to } };
43
+ if (isTimeWindow(window))
44
+ return { kind: "window", window };
45
+ return null;
46
+ }
47
+ /**
48
+ * Builds the criteria.
49
+ *
50
+ * `state` and `origin` are closed sets, so they carry no exclude half: "except
51
+ * running" is the other five buckets, and listing them is not the burden that
52
+ * enumerating an open set of tags would be.
53
+ */
54
+ export function buildAgentLsCriteria(options) {
55
+ return {
56
+ ...emptyAgentFilterCriteria(),
57
+ stateSelection: filterSelectionFrom(options.state, undefined),
58
+ originSelection: filterSelectionFrom(options.origin, undefined),
59
+ tagSelection: filterSelectionFrom(options.tag, options.notTag),
60
+ kanbanSelection: filterSelectionFrom(options.kanban, options.notKanban),
61
+ providerSelection: filterSelectionFrom(options.provider, options.notProvider),
62
+ archivedMode: resolveLsArchivedMode(options),
63
+ timeSelection: resolveLsTimeSelection(options),
64
+ textQuery: (options.text ?? "").trim().toLowerCase(),
65
+ };
66
+ }
67
+ function isAliveChild(agent) {
68
+ if (agent.archivedAt)
69
+ return false;
70
+ return agent.status === "running" || agent.status === "initializing";
71
+ }
72
+ /**
73
+ * `parentAgentId -> live child count`, computed over the corpus BEFORE any
74
+ * narrowing so a parent does not lose its `pending` bucket because its only
75
+ * live child was filtered out.
76
+ */
77
+ export function countActiveChildrenByParent(agents) {
78
+ const counts = new Map();
79
+ for (const agent of agents) {
80
+ const parentId = agent.labels?.[PARENT_AGENT_ID_LABEL]?.trim();
81
+ if (!parentId || !isAliveChild(agent))
82
+ continue;
83
+ counts.set(parentId, (counts.get(parentId) ?? 0) + 1);
84
+ }
85
+ return counts;
86
+ }
87
+ /** The derived bucket, matching what the app shows for the same agent. */
88
+ export function deriveLsStateBucket(agent, activeChildCount) {
89
+ return deriveAgentStateBucket({
90
+ status: agent.status,
91
+ pendingPermissionCount: agent.pendingPermissions?.length ?? 0,
92
+ requiresAttention: agent.requiresAttention ?? false,
93
+ attentionReason: agent.attentionReason ?? null,
94
+ activeChildCount,
95
+ });
96
+ }
97
+ function timeValueFor(agent, field) {
98
+ const raw = field === "created" ? agent.createdAt : (agent.lastUserMessageAt ?? agent.updatedAt);
99
+ if (!raw)
100
+ return null;
101
+ const date = new Date(raw);
102
+ return Number.isFinite(date.getTime()) ? date : null;
103
+ }
104
+ function tagsFromLabels(labels) {
105
+ if (!labels)
106
+ return [];
107
+ return Object.keys(labels)
108
+ .filter((key) => key.startsWith(TAG_LABEL_PREFIX) && key.length > TAG_LABEL_PREFIX.length)
109
+ .map((key) => key.slice(TAG_LABEL_PREFIX.length))
110
+ .sort();
111
+ }
112
+ /** Narrows `agents` with the shared predicate. `now` is injected so tests are deterministic. */
113
+ export function applyAgentLsFilter(agents, criteria, timeField, now) {
114
+ const childrenByParent = countActiveChildrenByParent(agents);
115
+ return agents.filter((agent) => {
116
+ const activeChildCount = (childrenByParent.get(agent.id) ?? 0) + (agent.activeBackgroundTaskCount ?? 0);
117
+ return agentPassesFilter({
118
+ stateBucket: deriveLsStateBucket(agent, activeChildCount),
119
+ tags: tagsFromLabels(agent.labels),
120
+ kanbanBucket: agent.labels?.[KANBAN_LABEL_KEY]?.trim() || null,
121
+ isArchived: Boolean(agent.archivedAt),
122
+ timeValue: timeValueFor(agent, timeField),
123
+ provider: agent.provider,
124
+ origin: deriveAgentOrigin(agent.labels),
125
+ searchableText: `${agent.title ?? ""} ${agent.cwd}`.toLowerCase(),
126
+ }, criteria, now);
127
+ });
128
+ }
129
+ /** `agentId -> derived bucket`, so the table can SHOW what `--state` filtered on. */
130
+ export function stateBucketsById(agents) {
131
+ const childrenByParent = countActiveChildrenByParent(agents);
132
+ const buckets = new Map();
133
+ for (const agent of agents) {
134
+ const activeChildCount = (childrenByParent.get(agent.id) ?? 0) + (agent.activeBackgroundTaskCount ?? 0);
135
+ buckets.set(agent.id, deriveLsStateBucket(agent, activeChildCount));
136
+ }
137
+ return buckets;
138
+ }
139
+ //# sourceMappingURL=ls-filter.js.map
@@ -11,6 +11,12 @@ export interface AgentListItem {
11
11
  provider: string;
12
12
  thinking: string;
13
13
  status: string;
14
+ /**
15
+ * The DERIVED bucket the app groups by. Shown next to the raw lifecycle
16
+ * because `--state pending` on a row whose STATUS reads `idle` is otherwise
17
+ * unreadable: they are different questions and both answers matter.
18
+ */
19
+ state: string;
14
20
  cwd: string;
15
21
  created: string;
16
22
  }
@@ -30,8 +36,23 @@ export interface AgentLsOptions extends CommandOptions {
30
36
  label?: string[];
31
37
  /** Filter by thinking option ID */
32
38
  thinking?: string;
39
+ /** Derived-state bucket(s), repeatable. */
40
+ state?: string[];
41
+ origin?: string[];
42
+ tag?: string[];
43
+ notTag?: string[];
44
+ kanban?: string[];
45
+ notKanban?: string[];
46
+ provider?: string[];
47
+ notProvider?: string[];
48
+ archived?: string;
49
+ timeField?: string;
50
+ window?: string;
51
+ from?: string;
52
+ to?: string;
53
+ text?: string;
33
54
  }
34
- export declare function buildAgentLsFetchOptions(options: Pick<AgentLsOptions, "all" | "global" | "label" | "thinking">): FetchAgentsOptions;
55
+ export declare function buildAgentLsFetchOptions(options: Pick<AgentLsOptions, "all" | "global" | "label" | "thinking" | "archived">): FetchAgentsOptions;
35
56
  /**
36
57
  * Agent ls command semantics:
37
58
  * - `paseo agent ls` → active non-archived agents
@@ -1,13 +1,33 @@
1
1
  import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
2
2
  import { collectMultiple } from "../../utils/command-options.js";
3
3
  import { isSameOrDescendantPath } from "../../utils/paths.js";
4
+ import { applyAgentLsFilter, buildAgentLsCriteria, resolveLsTimeField, stateBucketsById, } from "./ls-filter.js";
4
5
  export function addLsOptions(cmd) {
5
- return cmd
6
+ return (cmd
6
7
  .description("List agents. By default excludes archived agents.")
7
8
  .option("-a, --all", "Include archived agents")
8
9
  .option("-g, --global", "List agents across all directories")
9
10
  .option("--label <key=value>", "Filter by label (can be used multiple times)", collectMultiple, [])
10
- .option("--thinking <id>", "Filter by thinking option ID");
11
+ .option("--thinking <id>", "Filter by thinking option ID")
12
+ // The dimensions the paseo UI filters by. `--status` and `--cwd` are not new
13
+ // behaviour: `runLsCommand` has always applied them, but neither was ever
14
+ // registered here, so the code was unreachable from the command line.
15
+ .option("--status <status>", "Filter by raw lifecycle status (initializing|idle|running|error|closed)")
16
+ .option("--cwd <path>", "Filter to this directory and its descendants")
17
+ .option("--state <bucket>", "Filter by derived state bucket (needs_input|failed|running|pending|attention|done). Repeatable.", collectMultiple, [])
18
+ .option("--origin <origin>", "Filter by origin (root|delegated). Repeatable.", collectMultiple, [])
19
+ .option("--tag <tag>", "Keep agents carrying this tag. Repeatable.", collectMultiple, [])
20
+ .option("--not-tag <tag>", "Drop agents carrying this tag. Repeatable.", collectMultiple, [])
21
+ .option("--kanban <bucket>", "Keep agents in this kanban bucket. Repeatable.", collectMultiple, [])
22
+ .option("--not-kanban <bucket>", "Drop agents in this kanban bucket. Repeatable.", collectMultiple, [])
23
+ .option("--provider <id>", "Keep agents on this provider. Repeatable.", collectMultiple, [])
24
+ .option("--not-provider <id>", "Drop agents on this provider. Repeatable.", collectMultiple, [])
25
+ .option("--archived <mode>", "Archived handling: all|only|hide (default hide; -a means all)")
26
+ .option("--time-field <field>", "Which timestamp dates measure: created|active (default active)")
27
+ .option("--window <window>", "Rolling window: today|3d|7d|30d|stale")
28
+ .option("--from <YYYY-MM-DD>", "Exact range start (requires --to)")
29
+ .option("--to <YYYY-MM-DD>", "Exact range end, inclusive (requires --from)")
30
+ .option("--text <query>", "Case-insensitive substring over title and cwd"));
11
31
  }
12
32
  /** Helper to get relative time string */
13
33
  function relativeTime(date) {
@@ -60,12 +80,26 @@ export const agentLsSchema = {
60
80
  return undefined;
61
81
  },
62
82
  },
83
+ {
84
+ header: "STATE",
85
+ field: "state",
86
+ width: 12,
87
+ color: (value) => {
88
+ if (value === "running")
89
+ return "green";
90
+ if (value === "needs_input" || value === "failed")
91
+ return "red";
92
+ if (value === "pending" || value === "attention")
93
+ return "yellow";
94
+ return undefined;
95
+ },
96
+ },
63
97
  { header: "CWD", field: "cwd", width: 30 },
64
98
  { header: "CREATED", field: "created", width: 15 },
65
99
  ],
66
100
  };
67
101
  /** Transform agent snapshot to AgentListItem */
68
- function toListItem(agent) {
102
+ function toListItem(agent, state) {
69
103
  const model = normalizeModelId(agent.runtimeInfo?.model) ?? normalizeModelId(agent.model);
70
104
  return {
71
105
  id: agent.id,
@@ -74,6 +108,7 @@ function toListItem(agent) {
74
108
  provider: model ? `${agent.provider}/${model}` : agent.provider,
75
109
  thinking: agent.effectiveThinkingOptionId ?? "auto",
76
110
  status: agent.status,
111
+ state,
77
112
  cwd: shortenPath(agent.cwd),
78
113
  created: relativeTime(agent.createdAt),
79
114
  };
@@ -94,7 +129,10 @@ export function buildAgentLsFetchOptions(options) {
94
129
  const labelFilters = parseLabelFilters(options.label);
95
130
  const normalizedThinkingOptionId = options.thinking?.trim();
96
131
  const daemonFilter = {};
97
- if (options.all) {
132
+ // `only` needs archived records on the wire just as much as `all` does; asking
133
+ // the daemon to hide them and then filtering FOR them client-side returns an
134
+ // always-empty list, which reads as "nothing is archived".
135
+ if (options.all || options.archived === "all" || options.archived === "only") {
98
136
  daemonFilter.includeArchived = true;
99
137
  }
100
138
  if (Object.keys(labelFilters).length > 0) {
@@ -146,15 +184,11 @@ export async function runLsCommand(options, _command) {
146
184
  const labelFilters = parseLabelFilters(options.label);
147
185
  const fetchPayload = await client.fetchAgents(buildAgentLsFetchOptions(options));
148
186
  let agents = fetchPayload.entries.map((entry) => entry.agent);
149
- // By default, exclude archived agents. `-a` includes them.
150
- if (!options.all) {
151
- agents = agents.filter((a) => !a.archivedAt);
152
- }
153
- // If explicit status filter is provided, apply it.
187
+ // Raw lifecycle status and cwd stay separate from the shared predicate: they
188
+ // are properties of the record rather than filter dimensions the UI models.
154
189
  if (options.status) {
155
190
  agents = agents.filter((a) => a.status === options.status);
156
191
  }
157
- // Optional cwd filter.
158
192
  if (options.cwd) {
159
193
  agents = agents.filter((a) => isSameOrDescendantPath(options.cwd, a.cwd));
160
194
  }
@@ -171,6 +205,11 @@ export async function runLsCommand(options, _command) {
171
205
  });
172
206
  }
173
207
  await client.close();
208
+ // Every UI dimension, through the one shared predicate. The state buckets are
209
+ // computed BEFORE narrowing so a parent keeps its `pending` bucket even when
210
+ // its only live child is filtered out of the result.
211
+ const buckets = stateBucketsById(agents);
212
+ agents = applyAgentLsFilter(agents, buildAgentLsCriteria(options), resolveLsTimeField(options), new Date());
174
213
  // Sort agents: running first, then idle, then others; within each group, most recent first
175
214
  const statusOrder = { running: 0, idle: 1 };
176
215
  agents.sort((a, b) => {
@@ -184,7 +223,7 @@ export async function runLsCommand(options, _command) {
184
223
  const bTime = new Date(b.createdAt).getTime();
185
224
  return bTime - aTime;
186
225
  });
187
- const items = agents.map(toListItem);
226
+ const items = agents.map((agent) => toListItem(agent, buckets.get(agent.id) ?? "done"));
188
227
  return {
189
228
  type: "list",
190
229
  data: items,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperdrive.bot/paseo-cli",
3
- "version": "0.3.45",
3
+ "version": "0.3.47",
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
- "@hyperdrive.bot/paseo-client": "0.3.45",
31
- "@hyperdrive.bot/paseo-protocol": "0.3.45",
32
- "@hyperdrive.bot/paseo-server": "0.3.45",
30
+ "@hyperdrive.bot/paseo-client": "0.3.47",
31
+ "@hyperdrive.bot/paseo-protocol": "0.3.47",
32
+ "@hyperdrive.bot/paseo-server": "0.3.47",
33
33
  "chalk": "^5.3.0",
34
34
  "commander": "^12.0.0",
35
35
  "mime-types": "^2.1.35",