mirror of
https://github.com/openclaw/lobster.git
synced 2026-08-14 08:52:48 +00:00
feat: add groupBy command
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
})(),
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user