Compare commits
3 Commits
main
...
jverdi/hoo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dcedca323 | ||
|
|
25cc12e590 | ||
|
|
d826d96e2e |
@ -7,6 +7,7 @@
|
|||||||
- Agent runtime: update pi-mono dependencies to 0.31.1 (agent-core split).
|
- Agent runtime: update pi-mono dependencies to 0.31.1 (agent-core split).
|
||||||
- Dependencies: bump to latest compatible versions (TypeBox, grammY, Zod, Rolldown, oxlint-tsgolint).
|
- Dependencies: bump to latest compatible versions (TypeBox, grammY, Zod, Rolldown, oxlint-tsgolint).
|
||||||
- Tests: cover read tool image metadata + text output.
|
- Tests: cover read tool image metadata + text output.
|
||||||
|
- Hooks: treat null transforms as handled skips (204) and let explicit mappings override presets (#117) — thanks @jverdi.
|
||||||
|
|
||||||
### Breaking
|
### Breaking
|
||||||
- Skills config schema moved under `skills.*`:
|
- Skills config schema moved under `skills.*`:
|
||||||
@ -53,6 +54,7 @@
|
|||||||
- Skills: add tmux-first coding-agent skill + `requires.anyBins` gate for multi-CLI setup (thanks @sreekaransrinath).
|
- Skills: add tmux-first coding-agent skill + `requires.anyBins` gate for multi-CLI setup (thanks @sreekaransrinath).
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
- Gog calendar: format date ranges as RFC 3339 with timezone to satisfy Google Calendar API (thanks @jayhickey).
|
||||||
- Chat UI: keep the chat scrolled to the latest message after switching sessions.
|
- Chat UI: keep the chat scrolled to the latest message after switching sessions.
|
||||||
- Auto-reply: stream completed reply blocks as soon as they finish (configurable default + break); skip empty tool-only blocks unless verbose.
|
- Auto-reply: stream completed reply blocks as soon as they finish (configurable default + break); skip empty tool-only blocks unless verbose.
|
||||||
- Providers: make outbound text chunk limits configurable via `*.textChunkLimit` (defaults remain 4000/Discord 2000).
|
- Providers: make outbound text chunk limits configurable via `*.textChunkLimit` (defaults remain 4000/Discord 2000).
|
||||||
|
|||||||
@ -701,6 +701,7 @@ Endpoints:
|
|||||||
Mapping notes:
|
Mapping notes:
|
||||||
- `match.path` matches the sub-path after `/hooks` (e.g. `/hooks/gmail` → `gmail`).
|
- `match.path` matches the sub-path after `/hooks` (e.g. `/hooks/gmail` → `gmail`).
|
||||||
- `match.source` matches a payload field (e.g. `{ source: "gmail" }`) so you can use a generic `/hooks/ingest` path.
|
- `match.source` matches a payload field (e.g. `{ source: "gmail" }`) so you can use a generic `/hooks/ingest` path.
|
||||||
|
- `hooks.mappings` are evaluated before presets; first match wins.
|
||||||
- Templates like `{{messages[0].subject}}` read from the payload.
|
- Templates like `{{messages[0].subject}}` read from the payload.
|
||||||
- `transform` can point to a JS/TS module that returns a hook action.
|
- `transform` can point to a JS/TS module that returns a hook action.
|
||||||
|
|
||||||
|
|||||||
@ -89,6 +89,7 @@ code transforms.
|
|||||||
Mapping options (summary):
|
Mapping options (summary):
|
||||||
- `hooks.presets: ["gmail"]` enables the built-in Gmail mapping.
|
- `hooks.presets: ["gmail"]` enables the built-in Gmail mapping.
|
||||||
- `hooks.mappings` lets you define `match`, `action`, and templates in config.
|
- `hooks.mappings` lets you define `match`, `action`, and templates in config.
|
||||||
|
- `hooks.mappings` are evaluated before presets; first match wins.
|
||||||
- `hooks.transformsDir` + `transform.module` loads a JS/TS module for custom logic.
|
- `hooks.transformsDir` + `transform.module` loads a JS/TS module for custom logic.
|
||||||
- Use `match.source` to keep a generic ingest endpoint (payload-driven routing).
|
- Use `match.source` to keep a generic ingest endpoint (payload-driven routing).
|
||||||
- TS transforms require a TS loader (e.g. `tsx`) or precompiled `.js` at runtime.
|
- TS transforms require a TS loader (e.g. `tsx`) or precompiled `.js` at runtime.
|
||||||
|
|||||||
@ -74,6 +74,61 @@ describe("hooks mapping", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("treats null transform as a handled skip", async () => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdis-hooks-skip-"));
|
||||||
|
const modPath = path.join(dir, "transform.mjs");
|
||||||
|
fs.writeFileSync(modPath, "export default () => null;");
|
||||||
|
|
||||||
|
const mappings = resolveHookMappings({
|
||||||
|
transformsDir: dir,
|
||||||
|
mappings: [
|
||||||
|
{
|
||||||
|
match: { path: "skip" },
|
||||||
|
action: "agent",
|
||||||
|
transform: { module: "transform.mjs" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await applyHookMappings(mappings, {
|
||||||
|
payload: {},
|
||||||
|
headers: {},
|
||||||
|
url: new URL("http://127.0.0.1:18789/hooks/skip"),
|
||||||
|
path: "skip",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result?.ok).toBe(true);
|
||||||
|
if (result?.ok) {
|
||||||
|
expect(result.action).toBeNull();
|
||||||
|
expect("skipped" in result).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers explicit mappings over presets", async () => {
|
||||||
|
const mappings = resolveHookMappings({
|
||||||
|
presets: ["gmail"],
|
||||||
|
mappings: [
|
||||||
|
{
|
||||||
|
id: "override",
|
||||||
|
match: { path: "gmail" },
|
||||||
|
action: "agent",
|
||||||
|
messageTemplate: "Override subject: {{messages[0].subject}}",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const result = await applyHookMappings(mappings, {
|
||||||
|
payload: { messages: [{ subject: "Hello" }] },
|
||||||
|
headers: {},
|
||||||
|
url: baseUrl,
|
||||||
|
path: "gmail",
|
||||||
|
});
|
||||||
|
expect(result?.ok).toBe(true);
|
||||||
|
if (result?.ok) {
|
||||||
|
expect(result.action.kind).toBe("agent");
|
||||||
|
expect(result.action.message).toBe("Override subject: Hello");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects missing message", async () => {
|
it("rejects missing message", async () => {
|
||||||
const mappings = resolveHookMappings({
|
const mappings = resolveHookMappings({
|
||||||
mappings: [{ match: { path: "noop" }, action: "agent" }],
|
mappings: [{ match: { path: "noop" }, action: "agent" }],
|
||||||
|
|||||||
@ -70,6 +70,7 @@ export type HookAction =
|
|||||||
|
|
||||||
export type HookMappingResult =
|
export type HookMappingResult =
|
||||||
| { ok: true; action: HookAction }
|
| { ok: true; action: HookAction }
|
||||||
|
| { ok: true; action: null; skipped: true }
|
||||||
| { ok: false; error: string };
|
| { ok: false; error: string };
|
||||||
|
|
||||||
const hookPresetMappings: Record<string, HookMappingConfig[]> = {
|
const hookPresetMappings: Record<string, HookMappingConfig[]> = {
|
||||||
@ -113,11 +114,11 @@ export function resolveHookMappings(
|
|||||||
): HookMappingResolved[] {
|
): HookMappingResolved[] {
|
||||||
const presets = hooks?.presets ?? [];
|
const presets = hooks?.presets ?? [];
|
||||||
const mappings: HookMappingConfig[] = [];
|
const mappings: HookMappingConfig[] = [];
|
||||||
|
if (hooks?.mappings) mappings.push(...hooks.mappings);
|
||||||
for (const preset of presets) {
|
for (const preset of presets) {
|
||||||
const presetMappings = hookPresetMappings[preset];
|
const presetMappings = hookPresetMappings[preset];
|
||||||
if (presetMappings) mappings.push(...presetMappings);
|
if (presetMappings) mappings.push(...presetMappings);
|
||||||
}
|
}
|
||||||
if (hooks?.mappings) mappings.push(...hooks.mappings);
|
|
||||||
if (mappings.length === 0) return [];
|
if (mappings.length === 0) return [];
|
||||||
|
|
||||||
const configDir = path.dirname(CONFIG_PATH_CLAWDIS);
|
const configDir = path.dirname(CONFIG_PATH_CLAWDIS);
|
||||||
@ -145,7 +146,9 @@ export async function applyHookMappings(
|
|||||||
if (mapping.transform) {
|
if (mapping.transform) {
|
||||||
const transform = await loadTransform(mapping.transform);
|
const transform = await loadTransform(mapping.transform);
|
||||||
override = await transform(ctx);
|
override = await transform(ctx);
|
||||||
if (override === null) return null;
|
if (override === null) {
|
||||||
|
return { ok: true, action: null, skipped: true };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const merged = mergeAction(base.action, override, mapping.action);
|
const merged = mergeAction(base.action, override, mapping.action);
|
||||||
|
|||||||
@ -1706,6 +1706,11 @@ export async function startGatewayServer(
|
|||||||
sendJson(res, 400, { ok: false, error: mapped.error });
|
sendJson(res, 400, { ok: false, error: mapped.error });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (mapped.action === null) {
|
||||||
|
res.statusCode = 204;
|
||||||
|
res.end();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (mapped.action.kind === "wake") {
|
if (mapped.action.kind === "wake") {
|
||||||
dispatchWakeHook({
|
dispatchWakeHook({
|
||||||
text: mapped.action.text,
|
text: mapped.action.text,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user