feat: add groupBy command

This commit is contained in:
Vignesh Natarajan
2026-01-23 16:33:25 -08:00
parent 6cbc089fd8
commit 9350297770
3 changed files with 111 additions and 0 deletions
+2
View File
@@ -8,6 +8,7 @@ import { sortCommand } from "./stdlib/sort.js";
import { dedupeCommand } from "./stdlib/dedupe.js";
import { templateCommand } from "./stdlib/template.js";
import { mapCommand } from "./stdlib/map.js";
import { groupByCommand } from "./stdlib/group_by.js";
import { approveCommand } from "./stdlib/approve.js";
import { clawdInvokeCommand } from "./stdlib/clawd_invoke.js";
import { stateGetCommand, stateSetCommand } from "./stdlib/state.js";
@@ -33,6 +34,7 @@ export function createDefaultRegistry() {
dedupeCommand,
templateCommand,
mapCommand,
groupByCommand,
approveCommand,
clawdInvokeCommand,
stateGetCommand,
+62
View File
@@ -0,0 +1,62 @@
function getByPath(obj: any, path: string): any {
const parts = path.split('.').filter(Boolean);
let cur: any = obj;
for (const p of parts) {
if (cur == null) return undefined;
cur = cur[p];
}
return cur;
}
export const groupByCommand = {
name: 'groupBy',
meta: {
description: 'Group items by a key (stable group order)',
argsSchema: {
type: 'object',
properties: {
key: { type: 'string', description: 'Dot-path key to group by (required)' },
_: { type: 'array', items: { type: 'string' } },
},
required: ['key'],
},
sideEffects: [],
},
help() {
return (
`groupBy — group items by a key\n\n` +
`Usage:\n` +
` ... | groupBy --key from\n\n` +
`Output:\n` +
` Stream of { key, items, count } objects\n\n` +
`Notes:\n` +
` - Group order is stable (order of first appearance).\n`
);
},
async run({ input, args }: any) {
const keyPath = String(args.key ?? '').trim();
if (!keyPath) throw new Error('groupBy requires --key');
const groups = new Map<string, { key: any; items: any[] }>();
const order: string[] = [];
for await (const item of input) {
const keyVal = getByPath(item, keyPath);
const k = JSON.stringify(keyVal);
if (!groups.has(k)) {
groups.set(k, { key: keyVal, items: [] });
order.push(k);
}
groups.get(k)!.items.push(item);
}
return {
output: (async function* () {
for (const k of order) {
const g = groups.get(k)!;
yield { key: g.key, items: g.items, count: g.items.length };
}
})(),
};
},
};
+47
View File
@@ -0,0 +1,47 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { runPipeline } from '../src/runtime.js';
import { createDefaultRegistry } from '../src/commands/registry.js';
import { parsePipeline } from '../src/parser.js';
async function run(pipelineText: string, input: any[]) {
const pipeline = parsePipeline(pipelineText);
const registry = createDefaultRegistry();
const res = await runPipeline({
pipeline,
registry,
stdin: process.stdin,
stdout: process.stdout,
stderr: process.stderr,
env: process.env,
mode: 'tool',
input: (async function* () { for (const x of input) yield x; })(),
});
return res.items;
}
test('groupBy groups items by key and preserves group order', async () => {
const input = [
{ from: 'a', id: 1 },
{ from: 'b', id: 2 },
{ from: 'a', id: 3 },
];
const out = await run('groupBy --key from', input);
assert.equal(out.length, 2);
assert.deepEqual(out[0].key, 'a');
assert.deepEqual(out[0].items.map((x: any) => x.id), [1, 3]);
assert.equal(out[0].count, 2);
assert.deepEqual(out[1].key, 'b');
});
test('groupBy supports nested key paths', async () => {
const input = [
{ user: { id: 'u1' } },
{ user: { id: 'u2' } },
{ user: { id: 'u1' } },
];
const out = await run('groupBy --key user.id', input);
assert.deepEqual(out.map((g: any) => g.key), ['u1', 'u2']);
assert.equal(out[0].count, 2);
});