@dungle-scrubs/tallow 0.8.21 → 0.8.23
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.
- package/dist/cli.js +35 -4
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/interactive-mode-patch.d.ts +2 -0
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +82 -0
- package/dist/interactive-mode-patch.js.map +1 -1
- package/dist/sdk.d.ts +17 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +68 -1
- package/dist/sdk.js.map +1 -1
- package/dist/workspace-transition-relay.d.ts +40 -7
- package/dist/workspace-transition-relay.d.ts.map +1 -1
- package/dist/workspace-transition-relay.js +81 -16
- package/dist/workspace-transition-relay.js.map +1 -1
- package/extensions/__integration__/background-task-widget-ownership.test.ts +216 -0
- package/extensions/__integration__/claude-hooks-compat.test.ts +156 -0
- package/extensions/__integration__/slash-command-bridge.test.ts +169 -23
- package/extensions/_shared/atomic-write.ts +1 -1
- package/extensions/_shared/bordered-box.ts +102 -0
- package/extensions/_shared/interop-events.ts +5 -0
- package/extensions/_shared/pid-registry.ts +1 -1
- package/extensions/agent-commands-tool/index.ts +4 -1
- package/extensions/background-task-tool/__tests__/lifecycle.test.ts +50 -25
- package/extensions/background-task-tool/index.ts +139 -221
- package/extensions/bash-tool-enhanced/index.ts +1 -75
- package/extensions/cd-tool/index.ts +2 -2
- package/extensions/context-fork/spawn.ts +4 -1
- package/extensions/health/index.ts +6 -6
- package/extensions/hooks/__tests__/claude-compat.test.ts +35 -0
- package/extensions/hooks/__tests__/subprocess-hardening.test.ts +73 -0
- package/extensions/hooks/index.ts +27 -4
- package/extensions/loop/__tests__/loop.test.ts +168 -4
- package/extensions/loop/extension.json +6 -5
- package/extensions/loop/index.ts +242 -31
- package/extensions/plan-mode-tool/__tests__/agent-end-execution.test.ts +373 -0
- package/extensions/plan-mode-tool/index.ts +103 -41
- package/extensions/prompt-suggestions/__tests__/editor-compatibility.test.ts +42 -0
- package/extensions/prompt-suggestions/index.ts +41 -6
- package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +267 -671
- package/extensions/slash-command-bridge/extension.json +1 -1
- package/extensions/slash-command-bridge/index.ts +230 -116
- package/extensions/subagent-tool/index.ts +2 -2
- package/extensions/subagent-tool/process.ts +4 -5
- package/extensions/tasks/commands/register-tasks-extension.ts +41 -0
- package/extensions/teams-tool/__tests__/peer-messaging.test.ts +29 -24
- package/extensions/teams-tool/dashboard.ts +3 -5
- package/extensions/teams-tool/dispatch/auto-dispatch.ts +18 -1
- package/extensions/teams-tool/tools/teammate-tools.ts +9 -6
- package/extensions/wezterm-pane-control/__tests__/index.test.ts +88 -4
- package/extensions/wezterm-pane-control/index.ts +113 -8
- package/package.json +6 -4
- package/packages/tallow-tui/README.md +51 -0
- package/packages/tallow-tui/dist/autocomplete.d.ts +48 -0
- package/packages/tallow-tui/dist/autocomplete.d.ts.map +1 -0
- package/packages/tallow-tui/dist/autocomplete.js +564 -0
- package/packages/tallow-tui/dist/autocomplete.js.map +1 -0
- package/packages/tallow-tui/dist/border-styles.d.ts +32 -0
- package/packages/tallow-tui/dist/border-styles.d.ts.map +1 -0
- package/packages/tallow-tui/dist/border-styles.js +46 -0
- package/packages/tallow-tui/dist/border-styles.js.map +1 -0
- package/packages/tallow-tui/dist/components/bordered-box.d.ts +52 -0
- package/packages/tallow-tui/dist/components/bordered-box.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/bordered-box.js +89 -0
- package/packages/tallow-tui/dist/components/bordered-box.js.map +1 -0
- package/packages/tallow-tui/dist/components/box.d.ts +22 -0
- package/packages/tallow-tui/dist/components/box.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/box.js +104 -0
- package/packages/tallow-tui/dist/components/box.js.map +1 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.d.ts +22 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.js +35 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.js.map +1 -0
- package/packages/tallow-tui/dist/components/editor.d.ts +240 -0
- package/packages/tallow-tui/dist/components/editor.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/editor.js +1766 -0
- package/packages/tallow-tui/dist/components/editor.js.map +1 -0
- package/packages/tallow-tui/dist/components/image.d.ts +126 -0
- package/packages/tallow-tui/dist/components/image.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/image.js +245 -0
- package/packages/tallow-tui/dist/components/image.js.map +1 -0
- package/packages/tallow-tui/dist/components/input.d.ts +37 -0
- package/packages/tallow-tui/dist/components/input.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/input.js +439 -0
- package/packages/tallow-tui/dist/components/input.js.map +1 -0
- package/packages/tallow-tui/dist/components/loader.d.ts +88 -0
- package/packages/tallow-tui/dist/components/loader.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/loader.js +146 -0
- package/packages/tallow-tui/dist/components/loader.js.map +1 -0
- package/packages/tallow-tui/dist/components/markdown.d.ts +95 -0
- package/packages/tallow-tui/dist/components/markdown.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/markdown.js +633 -0
- package/packages/tallow-tui/dist/components/markdown.js.map +1 -0
- package/packages/tallow-tui/dist/components/select-list.d.ts +32 -0
- package/packages/tallow-tui/dist/components/select-list.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/select-list.js +156 -0
- package/packages/tallow-tui/dist/components/select-list.js.map +1 -0
- package/packages/tallow-tui/dist/components/settings-list.d.ts +50 -0
- package/packages/tallow-tui/dist/components/settings-list.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/settings-list.js +189 -0
- package/packages/tallow-tui/dist/components/settings-list.js.map +1 -0
- package/packages/tallow-tui/dist/components/spacer.d.ts +12 -0
- package/packages/tallow-tui/dist/components/spacer.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/spacer.js +23 -0
- package/packages/tallow-tui/dist/components/spacer.js.map +1 -0
- package/packages/tallow-tui/dist/components/text.d.ts +19 -0
- package/packages/tallow-tui/dist/components/text.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/text.js +91 -0
- package/packages/tallow-tui/dist/components/text.js.map +1 -0
- package/packages/tallow-tui/dist/components/truncated-text.d.ts +13 -0
- package/packages/tallow-tui/dist/components/truncated-text.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/truncated-text.js +51 -0
- package/packages/tallow-tui/dist/components/truncated-text.js.map +1 -0
- package/packages/tallow-tui/dist/editor-component.d.ts +50 -0
- package/packages/tallow-tui/dist/editor-component.d.ts.map +1 -0
- package/packages/tallow-tui/dist/editor-component.js +2 -0
- package/packages/tallow-tui/dist/editor-component.js.map +1 -0
- package/packages/tallow-tui/dist/fuzzy.d.ts +16 -0
- package/packages/tallow-tui/dist/fuzzy.d.ts.map +1 -0
- package/packages/tallow-tui/dist/fuzzy.js +107 -0
- package/packages/tallow-tui/dist/fuzzy.js.map +1 -0
- package/packages/tallow-tui/dist/index.d.ts +25 -0
- package/packages/tallow-tui/dist/index.d.ts.map +1 -0
- package/packages/tallow-tui/dist/index.js +35 -0
- package/packages/tallow-tui/dist/index.js.map +1 -0
- package/packages/tallow-tui/dist/keybindings.d.ts +39 -0
- package/packages/tallow-tui/dist/keybindings.d.ts.map +1 -0
- package/packages/tallow-tui/dist/keybindings.js +114 -0
- package/packages/tallow-tui/dist/keybindings.js.map +1 -0
- package/packages/tallow-tui/dist/keys.d.ts +168 -0
- package/packages/tallow-tui/dist/keys.d.ts.map +1 -0
- package/packages/tallow-tui/dist/keys.js +971 -0
- package/packages/tallow-tui/dist/keys.js.map +1 -0
- package/packages/tallow-tui/dist/kill-ring.d.ts +28 -0
- package/packages/tallow-tui/dist/kill-ring.d.ts.map +1 -0
- package/packages/tallow-tui/dist/kill-ring.js +44 -0
- package/packages/tallow-tui/dist/kill-ring.js.map +1 -0
- package/packages/tallow-tui/dist/stdin-buffer.d.ts +48 -0
- package/packages/tallow-tui/dist/stdin-buffer.d.ts.map +1 -0
- package/packages/tallow-tui/dist/stdin-buffer.js +317 -0
- package/packages/tallow-tui/dist/stdin-buffer.js.map +1 -0
- package/packages/tallow-tui/dist/terminal-image.d.ts +161 -0
- package/packages/tallow-tui/dist/terminal-image.d.ts.map +1 -0
- package/packages/tallow-tui/dist/terminal-image.js +460 -0
- package/packages/tallow-tui/dist/terminal-image.js.map +1 -0
- package/packages/tallow-tui/dist/terminal.d.ts +102 -0
- package/packages/tallow-tui/dist/terminal.d.ts.map +1 -0
- package/packages/tallow-tui/dist/terminal.js +263 -0
- package/packages/tallow-tui/dist/terminal.js.map +1 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.d.ts +14 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.d.ts.map +1 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.js +55 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.js.map +1 -0
- package/packages/tallow-tui/dist/tui.d.ts +239 -0
- package/packages/tallow-tui/dist/tui.d.ts.map +1 -0
- package/packages/tallow-tui/dist/tui.js +1058 -0
- package/packages/tallow-tui/dist/tui.js.map +1 -0
- package/packages/tallow-tui/dist/undo-stack.d.ts +17 -0
- package/packages/tallow-tui/dist/undo-stack.d.ts.map +1 -0
- package/packages/tallow-tui/dist/undo-stack.js +25 -0
- package/packages/tallow-tui/dist/undo-stack.js.map +1 -0
- package/packages/tallow-tui/dist/utils.d.ts +96 -0
- package/packages/tallow-tui/dist/utils.d.ts.map +1 -0
- package/packages/tallow-tui/dist/utils.js +843 -0
- package/packages/tallow-tui/dist/utils.js.map +1 -0
- package/packages/tallow-tui/package.json +24 -0
- package/packages/tallow-tui/src/__tests__/__snapshots__/render.test.ts.snap +121 -0
- package/packages/tallow-tui/src/__tests__/editor-border.test.ts +72 -0
- package/packages/tallow-tui/src/__tests__/editor-change-listener.test.ts +121 -0
- package/packages/tallow-tui/src/__tests__/editor-ghost-text.test.ts +112 -0
- package/packages/tallow-tui/src/__tests__/fuzzy.test.ts +91 -0
- package/packages/tallow-tui/src/__tests__/image-component.test.ts +113 -0
- package/packages/tallow-tui/src/__tests__/keys.test.ts +141 -0
- package/packages/tallow-tui/src/__tests__/render.test.ts +179 -0
- package/packages/tallow-tui/src/__tests__/stdin-buffer.test.ts +82 -0
- package/packages/tallow-tui/src/__tests__/terminal-image.test.ts +363 -0
- package/packages/tallow-tui/src/__tests__/tui-diff-regression.test.ts +454 -0
- package/packages/tallow-tui/src/__tests__/tui-render-scheduling.test.ts +256 -0
- package/packages/tallow-tui/src/__tests__/utils.test.ts +259 -0
- package/packages/tallow-tui/src/autocomplete.ts +716 -0
- package/packages/tallow-tui/src/border-styles.ts +60 -0
- package/packages/tallow-tui/src/components/bordered-box.ts +113 -0
- package/packages/tallow-tui/src/components/box.ts +137 -0
- package/packages/tallow-tui/src/components/cancellable-loader.ts +40 -0
- package/packages/tallow-tui/src/components/editor.ts +2143 -0
- package/packages/tallow-tui/src/components/image.ts +315 -0
- package/packages/tallow-tui/src/components/input.ts +522 -0
- package/packages/tallow-tui/src/components/loader.ts +187 -0
- package/packages/tallow-tui/src/components/markdown.ts +780 -0
- package/packages/tallow-tui/src/components/select-list.ts +197 -0
- package/packages/tallow-tui/src/components/settings-list.ts +264 -0
- package/packages/tallow-tui/src/components/spacer.ts +28 -0
- package/packages/tallow-tui/src/components/text.ts +113 -0
- package/packages/tallow-tui/src/components/truncated-text.ts +65 -0
- package/packages/tallow-tui/src/editor-component.ts +92 -0
- package/packages/tallow-tui/src/fuzzy.ts +133 -0
- package/packages/tallow-tui/src/index.ts +118 -0
- package/packages/tallow-tui/src/keybindings.ts +183 -0
- package/packages/tallow-tui/src/keys.ts +1189 -0
- package/packages/tallow-tui/src/kill-ring.ts +46 -0
- package/packages/tallow-tui/src/stdin-buffer.ts +386 -0
- package/packages/tallow-tui/src/terminal-image.ts +619 -0
- package/packages/tallow-tui/src/terminal.ts +350 -0
- package/packages/tallow-tui/src/test-utils/capability-env.ts +56 -0
- package/packages/tallow-tui/src/tui.ts +1336 -0
- package/packages/tallow-tui/src/undo-stack.ts +28 -0
- package/packages/tallow-tui/src/utils.ts +948 -0
- package/packages/tallow-tui/tsconfig.build.json +21 -0
- package/runtime/agent-runner.ts +20 -0
- package/runtime/atomic-write.ts +8 -0
- package/runtime/otel.ts +12 -0
- package/runtime/resolve-module.ts +23 -0
- package/runtime/runtime-path-provider.ts +12 -0
- package/runtime/runtime-provenance.ts +17 -0
- package/runtime/workspace-transition-relay.ts +21 -0
- package/runtime/workspace-transition.ts +29 -0
|
@@ -14,8 +14,9 @@ import type {
|
|
|
14
14
|
ExtensionContext,
|
|
15
15
|
Theme,
|
|
16
16
|
} from "@mariozechner/pi-coding-agent";
|
|
17
|
-
import {
|
|
18
|
-
import { resolveRuntimeProvenance } from "../../
|
|
17
|
+
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
18
|
+
import { resolveRuntimeProvenance } from "../../runtime/runtime-provenance.js";
|
|
19
|
+
import { renderBorderedBox } from "../_shared/bordered-box.js";
|
|
19
20
|
import { getTallowHomeDir } from "../_shared/tallow-paths.js";
|
|
20
21
|
|
|
21
22
|
// ── Types ────────────────────────────────────────────────────────────────────
|
|
@@ -662,13 +663,12 @@ export default function healthExtension(pi: ExtensionAPI): void {
|
|
|
662
663
|
render(width: number): string[] {
|
|
663
664
|
const innerWidth = width - 4; // 2 borders + 2 padding
|
|
664
665
|
const contentLines = renderHealth(details, theme, innerWidth);
|
|
665
|
-
|
|
666
|
-
borderStyle: ROUNDED,
|
|
667
|
-
title: "Health",
|
|
666
|
+
return renderBorderedBox(contentLines, width, {
|
|
668
667
|
borderColorFn: (s) => theme.fg("muted", s),
|
|
668
|
+
style: "rounded",
|
|
669
|
+
title: "Health",
|
|
669
670
|
titleColorFn: (s) => theme.fg("accent", s),
|
|
670
671
|
});
|
|
671
|
-
return box.render(width);
|
|
672
672
|
},
|
|
673
673
|
invalidate() {},
|
|
674
674
|
};
|
|
@@ -2,6 +2,7 @@ import { describe, expect, it } from "bun:test";
|
|
|
2
2
|
import {
|
|
3
3
|
adaptEventDataForHook,
|
|
4
4
|
CLAUDE_EVENT_MAP,
|
|
5
|
+
hasClaudeEventKeys,
|
|
5
6
|
shouldSkipClaudeToolResultHandler,
|
|
6
7
|
translateClaudeHooks,
|
|
7
8
|
translateClaudeOutput,
|
|
@@ -244,3 +245,37 @@ describe("shouldSkipClaudeToolResultHandler", () => {
|
|
|
244
245
|
);
|
|
245
246
|
});
|
|
246
247
|
});
|
|
248
|
+
|
|
249
|
+
describe("hasClaudeEventKeys", () => {
|
|
250
|
+
it("detects PreToolUse as a Claude event key", () => {
|
|
251
|
+
expect(hasClaudeEventKeys({ PreToolUse: [] })).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("detects UserPromptSubmit as a Claude event key", () => {
|
|
255
|
+
expect(hasClaudeEventKeys({ UserPromptSubmit: [] })).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("detects Stop as a Claude event key", () => {
|
|
259
|
+
expect(hasClaudeEventKeys({ Stop: [] })).toBe(true);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("detects multiple Claude event keys", () => {
|
|
263
|
+
expect(hasClaudeEventKeys({ PreToolUse: [], PostToolUse: [], Stop: [] })).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("returns false for native tallow event keys", () => {
|
|
267
|
+
expect(hasClaudeEventKeys({ tool_call: [], tool_result: [], agent_end: [] })).toBe(false);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("returns false for empty objects", () => {
|
|
271
|
+
expect(hasClaudeEventKeys({})).toBe(false);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("returns false for unrelated keys", () => {
|
|
275
|
+
expect(hasClaudeEventKeys({ name: "test", version: "1.0" })).toBe(false);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("detects Claude keys in a mixed config", () => {
|
|
279
|
+
expect(hasClaudeEventKeys({ tool_call: [], PreToolUse: [] })).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -119,3 +119,76 @@ describe("hook subprocess hardening", () => {
|
|
|
119
119
|
expect(elapsedMs).toBeLessThan(1000);
|
|
120
120
|
});
|
|
121
121
|
});
|
|
122
|
+
|
|
123
|
+
describe("Claude-sourced hook subprocess execution", () => {
|
|
124
|
+
/**
|
|
125
|
+
* Build a Claude-sourced command hook handler.
|
|
126
|
+
*
|
|
127
|
+
* @param command - Shell command executed by the hook
|
|
128
|
+
* @param claudeEventName - Original Claude event name
|
|
129
|
+
* @returns Claude-sourced command-type hook handler
|
|
130
|
+
*/
|
|
131
|
+
function createClaudeHandler(command: string, claudeEventName: string): HookHandler {
|
|
132
|
+
return {
|
|
133
|
+
command,
|
|
134
|
+
timeout: 5,
|
|
135
|
+
type: "command",
|
|
136
|
+
_claudeSource: true,
|
|
137
|
+
_claudeEventName: claudeEventName,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
test("Claude-sourced handler translates deny output to block result", async () => {
|
|
142
|
+
const handler = createClaudeHandler(
|
|
143
|
+
`node -e "process.stdout.write(JSON.stringify({hookSpecificOutput:{permissionDecision:'deny',permissionDecisionReason:'blocked by policy'}}))"`,
|
|
144
|
+
"PreToolUse"
|
|
145
|
+
);
|
|
146
|
+
const result = await runCommandHook(handler, { tool_name: "bash" }, process.cwd());
|
|
147
|
+
|
|
148
|
+
expect(result.ok).toBe(false);
|
|
149
|
+
expect(result.decision).toBe("block");
|
|
150
|
+
expect(result.reason).toBe("blocked by policy");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("Claude-sourced handler translates additionalContext output", async () => {
|
|
154
|
+
const handler = createClaudeHandler(
|
|
155
|
+
`node -e "process.stdout.write(JSON.stringify({hookSpecificOutput:{additionalContext:'remember this context'}}))"`,
|
|
156
|
+
"UserPromptSubmit"
|
|
157
|
+
);
|
|
158
|
+
const result = await runCommandHook(handler, { prompt: "hello" }, process.cwd());
|
|
159
|
+
|
|
160
|
+
expect(result.ok).toBe(true);
|
|
161
|
+
expect(result.additionalContext).toBe("remember this context");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("Claude-sourced handler sets CLAUDE_PROJECT_DIR env var", async () => {
|
|
165
|
+
const handler = createClaudeHandler(
|
|
166
|
+
`node -e "process.stdout.write(JSON.stringify({hookSpecificOutput:{additionalContext:process.env.CLAUDE_PROJECT_DIR}}))"`,
|
|
167
|
+
"PreToolUse"
|
|
168
|
+
);
|
|
169
|
+
const testCwd = process.cwd();
|
|
170
|
+
const result = await runCommandHook(handler, { tool_name: "bash" }, testCwd);
|
|
171
|
+
|
|
172
|
+
expect(result.ok).toBe(true);
|
|
173
|
+
expect(result.additionalContext).toBe(testCwd);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("Claude-sourced handler exit code 2 still blocks", async () => {
|
|
177
|
+
const handler = createClaudeHandler(
|
|
178
|
+
`node -e "process.stderr.write('BLOCKED: unsafe operation'); process.exit(2)"`,
|
|
179
|
+
"PreToolUse"
|
|
180
|
+
);
|
|
181
|
+
const result = await runCommandHook(handler, { tool_name: "bash" }, process.cwd());
|
|
182
|
+
|
|
183
|
+
expect(result.ok).toBe(false);
|
|
184
|
+
expect(result.decision).toBe("block");
|
|
185
|
+
expect(result.reason).toContain("BLOCKED: unsafe operation");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("Claude-sourced handler with no output succeeds silently", async () => {
|
|
189
|
+
const handler = createClaudeHandler("true", "PreToolUse");
|
|
190
|
+
const result = await runCommandHook(handler, { tool_name: "bash" }, process.cwd());
|
|
191
|
+
|
|
192
|
+
expect(result.ok).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
type AgentRunnerCandidate,
|
|
35
35
|
formatMissingAgentRunnerError,
|
|
36
36
|
resolveAgentRunnerCandidates,
|
|
37
|
-
} from "../../
|
|
37
|
+
} from "../../runtime/agent-runner.js";
|
|
38
38
|
import { isProjectTrusted } from "../_shared/project-trust.js";
|
|
39
39
|
import { evaluateCommand } from "../_shared/shell-policy.js";
|
|
40
40
|
import { getTallowHomeDir, getTallowPath } from "../_shared/tallow-paths.js";
|
|
@@ -568,15 +568,33 @@ function mergeHooks(target: HooksConfig, source: HooksConfig): void {
|
|
|
568
568
|
* Reads hooks from a JSON file (standalone hooks.json or settings.json with hooks key).
|
|
569
569
|
* Returns null if the file doesn't exist or can't be parsed.
|
|
570
570
|
*/
|
|
571
|
+
/**
|
|
572
|
+
* Check whether a parsed object contains any Claude Code event names as top-level keys.
|
|
573
|
+
*
|
|
574
|
+
* Used to detect hooks.json files that use Claude format (PreToolUse, UserPromptSubmit, etc.)
|
|
575
|
+
* instead of native tallow format (tool_call, input, etc.).
|
|
576
|
+
*
|
|
577
|
+
* @param obj - Parsed JSON object to inspect
|
|
578
|
+
* @returns True when at least one key is a known Claude event name
|
|
579
|
+
*/
|
|
580
|
+
export function hasClaudeEventKeys(obj: Record<string, unknown>): boolean {
|
|
581
|
+
return Object.keys(obj).some((key) => key in CLAUDE_EVENT_MAP);
|
|
582
|
+
}
|
|
583
|
+
|
|
571
584
|
function readHooksFile(filePath: string): HooksConfig | null {
|
|
572
585
|
try {
|
|
573
586
|
if (!fs.existsSync(filePath)) return null;
|
|
574
587
|
const content = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
575
588
|
// Standalone hooks.json has event keys at top level.
|
|
576
589
|
// settings.json wraps them under a "hooks" key.
|
|
590
|
+
// Also detect Claude event names (PreToolUse, etc.) as valid top-level keys.
|
|
577
591
|
return (
|
|
578
592
|
content.hooks ??
|
|
579
|
-
(content.tool_call || content.tool_result || content.agent_end
|
|
593
|
+
(content.tool_call || content.tool_result || content.agent_end
|
|
594
|
+
? content
|
|
595
|
+
: hasClaudeEventKeys(content)
|
|
596
|
+
? content
|
|
597
|
+
: null)
|
|
580
598
|
);
|
|
581
599
|
} catch {
|
|
582
600
|
return null;
|
|
@@ -596,7 +614,10 @@ function scanExtensionHooks(extensionsDir: string): HooksConfig {
|
|
|
596
614
|
const hooksPath = path.join(extensionsDir, entry, "hooks.json");
|
|
597
615
|
const hooks = readHooksFile(hooksPath);
|
|
598
616
|
if (hooks) {
|
|
599
|
-
mergeHooks(
|
|
617
|
+
mergeHooks(
|
|
618
|
+
merged,
|
|
619
|
+
hasClaudeEventKeys(hooks) ? translateClaudeHooks(hooks, hooksPath) : hooks
|
|
620
|
+
);
|
|
600
621
|
}
|
|
601
622
|
}
|
|
602
623
|
} catch {
|
|
@@ -655,7 +676,9 @@ function getPackageHooks(settingsPath: string): HooksConfig[] {
|
|
|
655
676
|
const hooksFile = path.join(resolved, "hooks.json");
|
|
656
677
|
const hooks = readHooksFile(hooksFile);
|
|
657
678
|
if (hooks) {
|
|
658
|
-
|
|
679
|
+
// Translate Claude event names if present, so packages can use
|
|
680
|
+
// either native tallow format or Claude Code format in hooks.json.
|
|
681
|
+
results.push(hasClaudeEventKeys(hooks) ? translateClaudeHooks(hooks, hooksFile) : hooks);
|
|
659
682
|
}
|
|
660
683
|
}
|
|
661
684
|
} catch {
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unit tests for the loop extension's pure helpers.
|
|
3
3
|
*
|
|
4
|
-
* Tests interval parsing, countdown formatting,
|
|
5
|
-
*
|
|
6
|
-
* `extensions/__integration__/loop.test.ts`.
|
|
4
|
+
* Tests interval parsing, countdown formatting, argument parsing,
|
|
5
|
+
* max iterations, and until-condition extraction.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import { describe, expect, test } from "bun:test";
|
|
10
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
extractUntilCondition,
|
|
11
|
+
formatCountdown,
|
|
12
|
+
parseInterval,
|
|
13
|
+
parseLoopArgs,
|
|
14
|
+
parseMaxIterations,
|
|
15
|
+
} from "../index.js";
|
|
11
16
|
|
|
12
17
|
describe("parseInterval", () => {
|
|
13
18
|
test("parses seconds", () => {
|
|
@@ -52,6 +57,69 @@ describe("parseInterval", () => {
|
|
|
52
57
|
});
|
|
53
58
|
});
|
|
54
59
|
|
|
60
|
+
describe("parseMaxIterations", () => {
|
|
61
|
+
test("parses x<N> format", () => {
|
|
62
|
+
expect(parseMaxIterations("x100")).toBe(100);
|
|
63
|
+
expect(parseMaxIterations("x1")).toBe(1);
|
|
64
|
+
expect(parseMaxIterations("x10")).toBe(10);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("rejects zero", () => {
|
|
68
|
+
expect(parseMaxIterations("x0")).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("rejects non-x formats", () => {
|
|
72
|
+
expect(parseMaxIterations("100")).toBeNull();
|
|
73
|
+
expect(parseMaxIterations("100x")).toBeNull();
|
|
74
|
+
expect(parseMaxIterations("abc")).toBeNull();
|
|
75
|
+
expect(parseMaxIterations("")).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("extractUntilCondition", () => {
|
|
80
|
+
test("extracts double-quoted condition", () => {
|
|
81
|
+
const result = extractUntilCondition(["until", '"build', "is", 'done"', "check", "status"]);
|
|
82
|
+
expect(result.condition).toBe("build is done");
|
|
83
|
+
expect(result.remaining).toEqual(["check", "status"]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("extracts single-quoted condition", () => {
|
|
87
|
+
const result = extractUntilCondition(["until", "'tests", "pass'", "run", "tests"]);
|
|
88
|
+
expect(result.condition).toBe("tests pass");
|
|
89
|
+
expect(result.remaining).toEqual(["run", "tests"]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("extracts single-word quoted condition", () => {
|
|
93
|
+
const result = extractUntilCondition(["until", '"done"', "check"]);
|
|
94
|
+
expect(result.condition).toBe("done");
|
|
95
|
+
expect(result.remaining).toEqual(["check"]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("extracts unquoted single-word condition", () => {
|
|
99
|
+
const result = extractUntilCondition(["until", "done", "check", "status"]);
|
|
100
|
+
expect(result.condition).toBe("done");
|
|
101
|
+
expect(result.remaining).toEqual(["check", "status"]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("returns null when no until keyword", () => {
|
|
105
|
+
const result = extractUntilCondition(["check", "deploy", "status"]);
|
|
106
|
+
expect(result.condition).toBeNull();
|
|
107
|
+
expect(result.remaining).toEqual(["check", "deploy", "status"]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("preserves tokens before until", () => {
|
|
111
|
+
const result = extractUntilCondition(["x10", "until", '"done"', "check"]);
|
|
112
|
+
expect(result.condition).toBe("done");
|
|
113
|
+
expect(result.remaining).toEqual(["x10", "check"]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("handles until at end with no condition", () => {
|
|
117
|
+
const result = extractUntilCondition(["check", "until"]);
|
|
118
|
+
expect(result.condition).toBeNull();
|
|
119
|
+
expect(result.remaining).toEqual(["check", "until"]);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
55
123
|
describe("formatCountdown", () => {
|
|
56
124
|
test("returns 'now' for zero or negative", () => {
|
|
57
125
|
expect(formatCountdown(0)).toBe("now");
|
|
@@ -111,6 +179,8 @@ describe("parseLoopArgs", () => {
|
|
|
111
179
|
intervalMs: 300_000,
|
|
112
180
|
intervalLabel: "5m",
|
|
113
181
|
prompt: "check deploy",
|
|
182
|
+
maxIterations: null,
|
|
183
|
+
untilCondition: null,
|
|
114
184
|
});
|
|
115
185
|
});
|
|
116
186
|
|
|
@@ -121,6 +191,8 @@ describe("parseLoopArgs", () => {
|
|
|
121
191
|
intervalMs: 30_000,
|
|
122
192
|
intervalLabel: "30s",
|
|
123
193
|
prompt: "/stats",
|
|
194
|
+
maxIterations: null,
|
|
195
|
+
untilCondition: null,
|
|
124
196
|
});
|
|
125
197
|
});
|
|
126
198
|
|
|
@@ -155,6 +227,98 @@ describe("parseLoopArgs", () => {
|
|
|
155
227
|
intervalMs: 3_600_000,
|
|
156
228
|
intervalLabel: "1h",
|
|
157
229
|
prompt: "summarize git log --oneline -20",
|
|
230
|
+
maxIterations: null,
|
|
231
|
+
untilCondition: null,
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// ── Max iterations ───────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
test("parses x<N> max iterations", () => {
|
|
238
|
+
const result = parseLoopArgs("1m x100 run tests");
|
|
239
|
+
expect(result).toEqual({
|
|
240
|
+
action: "start",
|
|
241
|
+
intervalMs: 60_000,
|
|
242
|
+
intervalLabel: "1m",
|
|
243
|
+
prompt: "run tests",
|
|
244
|
+
maxIterations: 100,
|
|
245
|
+
untilCondition: null,
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("x<N> works with single iteration", () => {
|
|
250
|
+
const result = parseLoopArgs("5s x1 check once");
|
|
251
|
+
expect(result).toEqual({
|
|
252
|
+
action: "start",
|
|
253
|
+
intervalMs: 5_000,
|
|
254
|
+
intervalLabel: "5s",
|
|
255
|
+
prompt: "check once",
|
|
256
|
+
maxIterations: 1,
|
|
257
|
+
untilCondition: null,
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ── Until condition ──────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
test("parses until condition with double quotes", () => {
|
|
264
|
+
const result = parseLoopArgs('2m until "build is done" check fuse index');
|
|
265
|
+
expect(result).toEqual({
|
|
266
|
+
action: "start",
|
|
267
|
+
intervalMs: 120_000,
|
|
268
|
+
intervalLabel: "2m",
|
|
269
|
+
prompt: "check fuse index",
|
|
270
|
+
maxIterations: null,
|
|
271
|
+
untilCondition: "build is done",
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("parses until condition with single quotes", () => {
|
|
276
|
+
const result = parseLoopArgs("1m until 'tests pass' run test suite");
|
|
277
|
+
expect(result).toEqual({
|
|
278
|
+
action: "start",
|
|
279
|
+
intervalMs: 60_000,
|
|
280
|
+
intervalLabel: "1m",
|
|
281
|
+
prompt: "run test suite",
|
|
282
|
+
maxIterations: null,
|
|
283
|
+
untilCondition: "tests pass",
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("parses unquoted until condition", () => {
|
|
288
|
+
const result = parseLoopArgs("5m until done check status");
|
|
289
|
+
expect(result).toEqual({
|
|
290
|
+
action: "start",
|
|
291
|
+
intervalMs: 300_000,
|
|
292
|
+
intervalLabel: "5m",
|
|
293
|
+
prompt: "check status",
|
|
294
|
+
maxIterations: null,
|
|
295
|
+
untilCondition: "done",
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// ── Combined x<N> + until ────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
test("parses both x<N> and until condition", () => {
|
|
302
|
+
const result = parseLoopArgs('1m x50 until "tests pass" run the test suite');
|
|
303
|
+
expect(result).toEqual({
|
|
304
|
+
action: "start",
|
|
305
|
+
intervalMs: 60_000,
|
|
306
|
+
intervalLabel: "1m",
|
|
307
|
+
prompt: "run the test suite",
|
|
308
|
+
maxIterations: 50,
|
|
309
|
+
untilCondition: "tests pass",
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("until before x<N> also works", () => {
|
|
314
|
+
const result = parseLoopArgs('1m until "deployed" x10 check deploy status');
|
|
315
|
+
expect(result).toEqual({
|
|
316
|
+
action: "start",
|
|
317
|
+
intervalMs: 60_000,
|
|
318
|
+
intervalLabel: "1m",
|
|
319
|
+
prompt: "check deploy status",
|
|
320
|
+
maxIterations: 10,
|
|
321
|
+
untilCondition: "deployed",
|
|
158
322
|
});
|
|
159
323
|
});
|
|
160
324
|
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loop",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Run a prompt on a recurring interval (/loop 5m check deploy)",
|
|
5
|
-
"whenToUse": "Use /loop to run a prompt or slash command repeatedly at a fixed interval.",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Run a prompt on a recurring interval with optional limits and stop conditions (/loop 5m check deploy)",
|
|
5
|
+
"whenToUse": "Use /loop to run a prompt or slash command repeatedly at a fixed interval. Supports max iterations (x<N>) and model-evaluated stop conditions (until \"condition\").",
|
|
6
6
|
"capabilities": {
|
|
7
|
-
"commands": ["loop"]
|
|
7
|
+
"commands": ["loop"],
|
|
8
|
+
"tools": ["loop_stop"]
|
|
8
9
|
},
|
|
9
10
|
"permissionSurface": {
|
|
10
11
|
"filesystem": "none",
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
"subprocess": false
|
|
14
15
|
},
|
|
15
16
|
"category": "automation",
|
|
16
|
-
"tags": ["loop", "recurring", "interval", "automation"],
|
|
17
|
+
"tags": ["loop", "recurring", "interval", "automation", "until", "condition"],
|
|
17
18
|
"files": ["index.ts"],
|
|
18
19
|
"relationships": [],
|
|
19
20
|
"npmDependencies": {}
|