fix: spawn list <cloud> now correctly filters by cloud instead of failing (#563)

Previously, `spawn list hetzner` always treated the bare positional
argument as an agent filter, returning 0 results since "hetzner" is a
cloud, not an agent. Now resolveListFilters auto-detects: when the
filter doesn't resolve as an agent but does resolve as a cloud, it
reclassifies to a cloud filter. This matches the help text which
promises "Filter history by agent or cloud name".

Agent: ux-engineer

Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
A 2026-02-11 17:54:19 -08:00 committed by GitHub
parent 746adb4f41
commit 477ce58367
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 73 additions and 3 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.2.58",
"version": "0.2.59",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

@ -308,6 +308,66 @@ describe("cmdList filter resolution via display names", () => {
});
});
// ── Bare positional arg: auto-detect cloud vs agent ───────────────────────
describe("bare positional arg reclassified as cloud filter when appropriate", () => {
it("should reclassify 'hetzner' from agentFilter to cloudFilter", async () => {
await setManifest(mockManifest);
writeHistory(sampleRecords);
// "hetzner" passed as agentFilter (bare positional), should be reclassified
await cmdList("hetzner");
const output = consoleOutput();
// Should find 2 records on hetzner (aider + claude), not 0
expect(output).toContain("2 of 4");
});
it("should reclassify 'sprite' from agentFilter to cloudFilter", async () => {
await setManifest(mockManifest);
writeHistory(sampleRecords);
await cmdList("sprite");
const output = consoleOutput();
// Should find 2 records on sprite
expect(output).toContain("2 of 4");
});
it("should reclassify cloud display name 'Hetzner Cloud' to cloudFilter", async () => {
await setManifest(mockManifest);
writeHistory(sampleRecords);
await cmdList("Hetzner Cloud");
const output = consoleOutput();
expect(output).toContain("2 of 4");
});
it("should NOT reclassify when agentFilter resolves to an agent", async () => {
await setManifest(mockManifest);
writeHistory(sampleRecords);
await cmdList("claude");
const output = consoleOutput();
// "claude" is a valid agent, should filter by agent
expect(output).toContain("2 of 4");
});
it("should NOT reclassify when explicit cloudFilter is already set", async () => {
await setManifest(mockManifest);
writeHistory(sampleRecords);
// When both are set, don't reclassify
await cmdList("unknown-thing", "hetzner");
const info = logInfoOutput();
// Should show "no spawns" since agent=unknown-thing finds nothing
expect(info).toContain("No spawns found matching");
});
});
// ── Key that matches directly vs display name ──────────────────────────────
describe("direct key match takes precedence over display name", () => {

View file

@ -933,7 +933,8 @@ function buildRecordHint(r: SpawnRecord): string {
return when;
}
/** Try to load manifest and resolve filter display names to keys */
/** Try to load manifest and resolve filter display names to keys.
* When a bare positional filter doesn't match an agent, try it as a cloud. */
async function resolveListFilters(
agentFilter?: string,
cloudFilter?: string
@ -947,7 +948,16 @@ async function resolveListFilters(
if (manifest && agentFilter) {
const resolved = resolveAgentKey(manifest, agentFilter);
if (resolved) agentFilter = resolved;
if (resolved) {
agentFilter = resolved;
} else if (!cloudFilter) {
// Bare positional arg didn't match an agent -- try as a cloud filter
const resolvedCloud = resolveCloudKey(manifest, agentFilter);
if (resolvedCloud) {
cloudFilter = resolvedCloud;
agentFilter = undefined;
}
}
}
if (manifest && cloudFilter) {
const resolved = resolveCloudKey(manifest, cloudFilter);