@elizaos/autonomous 2.0.0-alpha.10
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/LICENSE +21 -0
- package/package.json +270 -0
- package/src/actions/emote.ts +101 -0
- package/src/actions/restart.ts +101 -0
- package/src/actions/send-message.ts +168 -0
- package/src/actions/stream-control.ts +439 -0
- package/src/actions/switch-stream-source.ts +126 -0
- package/src/actions/terminal.ts +186 -0
- package/src/api/agent-admin-routes.ts +178 -0
- package/src/api/agent-lifecycle-routes.ts +129 -0
- package/src/api/agent-model.ts +143 -0
- package/src/api/agent-transfer-routes.ts +211 -0
- package/src/api/apps-routes.ts +210 -0
- package/src/api/auth-routes.ts +90 -0
- package/src/api/bsc-trade.ts +736 -0
- package/src/api/bug-report-routes.ts +161 -0
- package/src/api/character-routes.ts +421 -0
- package/src/api/cloud-billing-routes.ts +598 -0
- package/src/api/cloud-compat-routes.ts +192 -0
- package/src/api/cloud-routes.ts +529 -0
- package/src/api/cloud-status-routes.ts +234 -0
- package/src/api/compat-utils.ts +154 -0
- package/src/api/connector-health.ts +135 -0
- package/src/api/coordinator-wiring.ts +179 -0
- package/src/api/credit-detection.ts +47 -0
- package/src/api/database.ts +1357 -0
- package/src/api/diagnostics-routes.ts +389 -0
- package/src/api/drop-service.ts +205 -0
- package/src/api/early-logs.ts +111 -0
- package/src/api/http-helpers.ts +252 -0
- package/src/api/index.ts +85 -0
- package/src/api/knowledge-routes.ts +1189 -0
- package/src/api/knowledge-service-loader.ts +92 -0
- package/src/api/memory-bounds.ts +121 -0
- package/src/api/memory-routes.ts +349 -0
- package/src/api/merkle-tree.ts +239 -0
- package/src/api/models-routes.ts +72 -0
- package/src/api/nfa-routes.ts +169 -0
- package/src/api/nft-verify.ts +188 -0
- package/src/api/og-tracker.ts +72 -0
- package/src/api/parse-action-block.ts +145 -0
- package/src/api/permissions-routes.ts +222 -0
- package/src/api/plugin-validation.ts +355 -0
- package/src/api/provider-switch-config.ts +455 -0
- package/src/api/registry-routes.ts +165 -0
- package/src/api/registry-service.ts +292 -0
- package/src/api/route-helpers.ts +21 -0
- package/src/api/sandbox-routes.ts +1480 -0
- package/src/api/server.ts +17674 -0
- package/src/api/signal-routes.ts +265 -0
- package/src/api/stream-persistence.ts +297 -0
- package/src/api/stream-route-state.ts +48 -0
- package/src/api/stream-routes.ts +1046 -0
- package/src/api/stream-voice-routes.ts +208 -0
- package/src/api/streaming-text.ts +129 -0
- package/src/api/streaming-types.ts +23 -0
- package/src/api/subscription-routes.ts +283 -0
- package/src/api/terminal-run-limits.ts +31 -0
- package/src/api/training-backend-check.ts +40 -0
- package/src/api/training-routes.ts +314 -0
- package/src/api/training-service-like.ts +46 -0
- package/src/api/trajectory-routes.ts +714 -0
- package/src/api/trigger-routes.ts +438 -0
- package/src/api/twitter-verify.ts +226 -0
- package/src/api/tx-service.ts +193 -0
- package/src/api/wallet-dex-prices.ts +206 -0
- package/src/api/wallet-evm-balance.ts +989 -0
- package/src/api/wallet-routes.ts +505 -0
- package/src/api/wallet-rpc.ts +523 -0
- package/src/api/wallet-trading-profile.ts +694 -0
- package/src/api/wallet.ts +745 -0
- package/src/api/whatsapp-routes.ts +282 -0
- package/src/api/zip-utils.ts +130 -0
- package/src/auth/anthropic.ts +63 -0
- package/src/auth/apply-stealth.ts +38 -0
- package/src/auth/claude-code-stealth.ts +141 -0
- package/src/auth/credentials.ts +226 -0
- package/src/auth/index.ts +18 -0
- package/src/auth/openai-codex.ts +94 -0
- package/src/auth/types.ts +24 -0
- package/src/awareness/registry.ts +220 -0
- package/src/bin.ts +10 -0
- package/src/cli/index.ts +36 -0
- package/src/cli/parse-duration.ts +43 -0
- package/src/cloud/auth.test.ts +370 -0
- package/src/cloud/auth.ts +176 -0
- package/src/cloud/backup.test.ts +150 -0
- package/src/cloud/backup.ts +50 -0
- package/src/cloud/base-url.ts +45 -0
- package/src/cloud/bridge-client.test.ts +481 -0
- package/src/cloud/bridge-client.ts +307 -0
- package/src/cloud/cloud-manager.test.ts +223 -0
- package/src/cloud/cloud-manager.ts +151 -0
- package/src/cloud/cloud-proxy.test.ts +122 -0
- package/src/cloud/cloud-proxy.ts +52 -0
- package/src/cloud/index.ts +23 -0
- package/src/cloud/reconnect.test.ts +178 -0
- package/src/cloud/reconnect.ts +108 -0
- package/src/cloud/validate-url.test.ts +147 -0
- package/src/cloud/validate-url.ts +176 -0
- package/src/config/character-schema.ts +44 -0
- package/src/config/config.ts +149 -0
- package/src/config/env-vars.ts +86 -0
- package/src/config/includes.ts +196 -0
- package/src/config/index.ts +15 -0
- package/src/config/object-utils.ts +10 -0
- package/src/config/paths.ts +92 -0
- package/src/config/plugin-auto-enable.ts +520 -0
- package/src/config/schema.ts +1342 -0
- package/src/config/telegram-custom-commands.ts +99 -0
- package/src/config/types.agent-defaults.ts +342 -0
- package/src/config/types.agents.ts +112 -0
- package/src/config/types.gateway.ts +243 -0
- package/src/config/types.hooks.ts +124 -0
- package/src/config/types.messages.ts +201 -0
- package/src/config/types.milady.ts +791 -0
- package/src/config/types.tools.ts +416 -0
- package/src/config/types.ts +7 -0
- package/src/config/zod-schema.agent-runtime.ts +777 -0
- package/src/config/zod-schema.core.ts +778 -0
- package/src/config/zod-schema.hooks.ts +139 -0
- package/src/config/zod-schema.providers-core.ts +1126 -0
- package/src/config/zod-schema.session.ts +98 -0
- package/src/config/zod-schema.ts +865 -0
- package/src/contracts/apps.ts +46 -0
- package/src/contracts/awareness.ts +56 -0
- package/src/contracts/config.ts +172 -0
- package/src/contracts/drop.ts +21 -0
- package/src/contracts/index.ts +8 -0
- package/src/contracts/onboarding.ts +592 -0
- package/src/contracts/permissions.ts +52 -0
- package/src/contracts/verification.ts +9 -0
- package/src/contracts/wallet.ts +503 -0
- package/src/diagnostics/integration-observability.ts +132 -0
- package/src/emotes/catalog.ts +655 -0
- package/src/external-modules.d.ts +7 -0
- package/src/hooks/discovery.test.ts +357 -0
- package/src/hooks/discovery.ts +231 -0
- package/src/hooks/eligibility.ts +146 -0
- package/src/hooks/hooks.test.ts +320 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/loader.test.ts +418 -0
- package/src/hooks/loader.ts +256 -0
- package/src/hooks/registry.test.ts +168 -0
- package/src/hooks/registry.ts +74 -0
- package/src/hooks/types.ts +121 -0
- package/src/index.ts +19 -0
- package/src/onboarding-presets.ts +828 -0
- package/src/plugins/custom-rtmp/index.ts +40 -0
- package/src/providers/admin-trust.ts +76 -0
- package/src/providers/session-bridge.ts +143 -0
- package/src/providers/session-utils.ts +42 -0
- package/src/providers/simple-mode.ts +113 -0
- package/src/providers/ui-catalog.ts +135 -0
- package/src/providers/workspace-provider.ts +213 -0
- package/src/providers/workspace.ts +497 -0
- package/src/runtime/agent-event-service.ts +57 -0
- package/src/runtime/cloud-onboarding.test.ts +489 -0
- package/src/runtime/cloud-onboarding.ts +408 -0
- package/src/runtime/core-plugins.ts +53 -0
- package/src/runtime/custom-actions.ts +605 -0
- package/src/runtime/eliza.ts +4941 -0
- package/src/runtime/embedding-presets.ts +73 -0
- package/src/runtime/index.ts +8 -0
- package/src/runtime/milady-plugin.ts +180 -0
- package/src/runtime/onboarding-names.ts +76 -0
- package/src/runtime/release-plugin-policy.ts +119 -0
- package/src/runtime/restart.ts +59 -0
- package/src/runtime/trajectory-persistence.ts +2584 -0
- package/src/runtime/version.ts +6 -0
- package/src/security/audit-log.ts +222 -0
- package/src/security/network-policy.ts +91 -0
- package/src/server/index.ts +6 -0
- package/src/services/agent-export.ts +976 -0
- package/src/services/app-manager.ts +755 -0
- package/src/services/browser-capture.ts +215 -0
- package/src/services/coding-agent-context.ts +355 -0
- package/src/services/fallback-training-service.ts +196 -0
- package/src/services/index.ts +17 -0
- package/src/services/mcp-marketplace.ts +327 -0
- package/src/services/plugin-manager-types.ts +185 -0
- package/src/services/privy-wallets.ts +352 -0
- package/src/services/registry-client-app-meta.ts +201 -0
- package/src/services/registry-client-endpoints.ts +253 -0
- package/src/services/registry-client-local.ts +485 -0
- package/src/services/registry-client-network.ts +173 -0
- package/src/services/registry-client-queries.ts +176 -0
- package/src/services/registry-client-types.ts +104 -0
- package/src/services/registry-client.ts +366 -0
- package/src/services/remote-signing-service.ts +261 -0
- package/src/services/sandbox-engine.ts +753 -0
- package/src/services/sandbox-manager.ts +503 -0
- package/src/services/self-updater.ts +213 -0
- package/src/services/signal-pairing.ts +189 -0
- package/src/services/signing-policy.ts +230 -0
- package/src/services/skill-catalog-client.ts +195 -0
- package/src/services/skill-marketplace.ts +909 -0
- package/src/services/stream-manager.ts +707 -0
- package/src/services/tts-stream-bridge.ts +465 -0
- package/src/services/update-checker.ts +163 -0
- package/src/services/version-compat.ts +367 -0
- package/src/services/whatsapp-pairing.ts +279 -0
- package/src/shared/ui-catalog-prompt.ts +1158 -0
- package/src/test-support/process-helpers.ts +35 -0
- package/src/test-support/route-test-helpers.ts +113 -0
- package/src/test-support/test-helpers.ts +304 -0
- package/src/testing/index.ts +3 -0
- package/src/triggers/action.ts +342 -0
- package/src/triggers/runtime.ts +432 -0
- package/src/triggers/scheduling.ts +472 -0
- package/src/triggers/types.ts +133 -0
- package/src/types/app-hyperscape-routes-shim.d.ts +29 -0
- package/src/types/external-modules.d.ts +7 -0
- package/src/utils/exec-safety.ts +23 -0
- package/src/utils/number-parsing.ts +112 -0
- package/src/utils/spoken-text.ts +65 -0
- package/src/version-resolver.ts +60 -0
- package/test/api/agent-admin-routes.test.ts +160 -0
- package/test/api/agent-lifecycle-routes.test.ts +164 -0
- package/test/api/agent-transfer-routes.test.ts +136 -0
- package/test/api/apps-routes.test.ts +140 -0
- package/test/api/auth-routes.test.ts +160 -0
- package/test/api/bug-report-routes.test.ts +88 -0
- package/test/api/knowledge-routes.test.ts +73 -0
- package/test/api/lifecycle.test.ts +342 -0
- package/test/api/memory-routes.test.ts +74 -0
- package/test/api/models-routes.test.ts +112 -0
- package/test/api/nfa-routes.test.ts +78 -0
- package/test/api/permissions-routes.test.ts +185 -0
- package/test/api/registry-routes.test.ts +157 -0
- package/test/api/signal-routes.test.ts +113 -0
- package/test/api/subscription-routes.test.ts +90 -0
- package/test/api/trigger-routes.test.ts +87 -0
- package/test/api/wallet-routes.observability.test.ts +191 -0
- package/test/api/wallet-routes.test.ts +502 -0
- package/test/diagnostics/integration-observability.test.ts +135 -0
- package/test/security/audit-log.test.ts +229 -0
- package/test/security/network-policy.test.ts +143 -0
- package/test/services/version-compat.test.ts +127 -0
- package/tsconfig.build.json +21 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Discovery — Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for:
|
|
5
|
+
* - Frontmatter parsing (valid, metadata JSON, missing delimiters, empty name, invalid JSON)
|
|
6
|
+
* - Handler resolution (handler.ts, fallback chain, no handler)
|
|
7
|
+
* - Discovery precedence (workspace > managed > bundled > extra)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { basename, join } from "node:path";
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
14
|
+
import { discoverHooks } from "./discovery";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// mocks
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
vi.mock("@elizaos/core", () => ({
|
|
21
|
+
logger: { info: vi.fn(), warn: vi.fn(), debug: vi.fn(), error: vi.fn() },
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
// Stub the managed dir (~/.milady/hooks) to avoid picking up real hooks
|
|
25
|
+
vi.mock("node:os", async (importOriginal) => {
|
|
26
|
+
const actual = (await importOriginal()) as typeof import("node:os");
|
|
27
|
+
return {
|
|
28
|
+
...actual,
|
|
29
|
+
// Point homedir to a temp location so ~/.milady/hooks doesn't exist
|
|
30
|
+
homedir: () => join(tmpdir(), "__discovery_test_fake_home__"),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// helpers
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
let tempRoot: string;
|
|
39
|
+
|
|
40
|
+
async function createHookDir(
|
|
41
|
+
base: string,
|
|
42
|
+
name: string,
|
|
43
|
+
opts: {
|
|
44
|
+
hookMd?: string;
|
|
45
|
+
handlerFile?: string; // which handler filename to create
|
|
46
|
+
handlerContent?: string;
|
|
47
|
+
} = {},
|
|
48
|
+
): Promise<string> {
|
|
49
|
+
const dir = join(base, name);
|
|
50
|
+
await mkdir(dir, { recursive: true });
|
|
51
|
+
|
|
52
|
+
if (opts.hookMd !== undefined) {
|
|
53
|
+
await writeFile(join(dir, "HOOK.md"), opts.hookMd, "utf-8");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const handlerFile = opts.handlerFile ?? "handler.ts";
|
|
57
|
+
const handlerContent = opts.handlerContent ?? "export default () => {};";
|
|
58
|
+
await writeFile(join(dir, handlerFile), handlerContent, "utf-8");
|
|
59
|
+
|
|
60
|
+
return dir;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// lifecycle
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
beforeEach(async () => {
|
|
68
|
+
tempRoot = join(
|
|
69
|
+
tmpdir(),
|
|
70
|
+
`hooks-discovery-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
71
|
+
);
|
|
72
|
+
await mkdir(tempRoot, { recursive: true });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(async () => {
|
|
76
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// 1. Frontmatter parsing (tested via discoverHooks)
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
describe("frontmatter parsing", () => {
|
|
84
|
+
it("parses valid frontmatter with name and description", async () => {
|
|
85
|
+
const bundled = join(tempRoot, "bundled");
|
|
86
|
+
await createHookDir(bundled, "my-hook", {
|
|
87
|
+
hookMd: [
|
|
88
|
+
"---",
|
|
89
|
+
"name: my-hook",
|
|
90
|
+
"description: A test hook",
|
|
91
|
+
"---",
|
|
92
|
+
"",
|
|
93
|
+
"# My Hook",
|
|
94
|
+
].join("\n"),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const entries = await discoverHooks({ bundledDir: bundled });
|
|
98
|
+
|
|
99
|
+
expect(entries).toHaveLength(1);
|
|
100
|
+
expect(entries[0].hook.name).toBe("my-hook");
|
|
101
|
+
expect(entries[0].frontmatter.description).toBe("A test hook");
|
|
102
|
+
expect(entries[0].hook.source).toBe("milady-bundled");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("extracts milady metadata from frontmatter JSON", async () => {
|
|
106
|
+
const bundled = join(tempRoot, "bundled");
|
|
107
|
+
await createHookDir(bundled, "meta-hook", {
|
|
108
|
+
hookMd: [
|
|
109
|
+
"---",
|
|
110
|
+
"name: meta-hook",
|
|
111
|
+
"description: Hook with metadata",
|
|
112
|
+
'metadata: { "milady": { "emoji": "🔥", "events": ["command:new"], "hookKey": "custom-key" } }',
|
|
113
|
+
"---",
|
|
114
|
+
].join("\n"),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const entries = await discoverHooks({ bundledDir: bundled });
|
|
118
|
+
|
|
119
|
+
expect(entries).toHaveLength(1);
|
|
120
|
+
expect(entries[0].metadata).toBeDefined();
|
|
121
|
+
expect(entries[0].metadata?.emoji).toBe("🔥");
|
|
122
|
+
expect(entries[0].metadata?.events).toEqual(["command:new"]);
|
|
123
|
+
expect(entries[0].metadata?.hookKey).toBe("custom-key");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("skips hook when frontmatter delimiters are missing", async () => {
|
|
127
|
+
const bundled = join(tempRoot, "bundled");
|
|
128
|
+
await createHookDir(bundled, "bad-fm", {
|
|
129
|
+
hookMd: "name: no-delimiters\ndescription: missing ---\n",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const entries = await discoverHooks({ bundledDir: bundled });
|
|
133
|
+
|
|
134
|
+
expect(entries).toHaveLength(0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("skips hook when name is empty", async () => {
|
|
138
|
+
const bundled = join(tempRoot, "bundled");
|
|
139
|
+
await createHookDir(bundled, "empty-name", {
|
|
140
|
+
hookMd: [
|
|
141
|
+
"---",
|
|
142
|
+
"name: ",
|
|
143
|
+
"description: Has description but no name",
|
|
144
|
+
"---",
|
|
145
|
+
].join("\n"),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const entries = await discoverHooks({ bundledDir: bundled });
|
|
149
|
+
|
|
150
|
+
expect(entries).toHaveLength(0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("warns but still parses name/description when metadata JSON is invalid", async () => {
|
|
154
|
+
const { logger } = await import("@elizaos/core");
|
|
155
|
+
const bundled = join(tempRoot, "bundled");
|
|
156
|
+
await createHookDir(bundled, "bad-meta", {
|
|
157
|
+
hookMd: [
|
|
158
|
+
"---",
|
|
159
|
+
"name: bad-meta",
|
|
160
|
+
"description: Invalid metadata JSON",
|
|
161
|
+
"metadata: {not valid json!!!}",
|
|
162
|
+
"---",
|
|
163
|
+
].join("\n"),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const entries = await discoverHooks({ bundledDir: bundled });
|
|
167
|
+
|
|
168
|
+
expect(entries).toHaveLength(1);
|
|
169
|
+
expect(entries[0].hook.name).toBe("bad-meta");
|
|
170
|
+
expect(entries[0].frontmatter.description).toBe("Invalid metadata JSON");
|
|
171
|
+
expect(entries[0].metadata).toBeUndefined();
|
|
172
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
173
|
+
expect.stringContaining("Failed to parse metadata"),
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("parses homepage from frontmatter", async () => {
|
|
178
|
+
const bundled = join(tempRoot, "bundled");
|
|
179
|
+
await createHookDir(bundled, "hp-hook", {
|
|
180
|
+
hookMd: [
|
|
181
|
+
"---",
|
|
182
|
+
"name: hp-hook",
|
|
183
|
+
"description: Hook with homepage",
|
|
184
|
+
"homepage: https://example.com",
|
|
185
|
+
"---",
|
|
186
|
+
].join("\n"),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const entries = await discoverHooks({ bundledDir: bundled });
|
|
190
|
+
|
|
191
|
+
expect(entries).toHaveLength(1);
|
|
192
|
+
expect(entries[0].frontmatter.homepage).toBe("https://example.com");
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// 2. Handler resolution
|
|
198
|
+
// ============================================================================
|
|
199
|
+
|
|
200
|
+
describe("handler resolution", () => {
|
|
201
|
+
it("uses handler.ts when found", async () => {
|
|
202
|
+
const bundled = join(tempRoot, "bundled");
|
|
203
|
+
await createHookDir(bundled, "ts-handler", {
|
|
204
|
+
hookMd: "---\nname: ts-handler\ndescription: uses handler.ts\n---",
|
|
205
|
+
handlerFile: "handler.ts",
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const entries = await discoverHooks({ bundledDir: bundled });
|
|
209
|
+
|
|
210
|
+
expect(entries).toHaveLength(1);
|
|
211
|
+
expect(entries[0].hook.handlerPath).toContain("handler.ts");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("falls back to index.ts when handler.ts is missing", async () => {
|
|
215
|
+
const bundled = join(tempRoot, "bundled");
|
|
216
|
+
const dir = join(bundled, "idx-hook");
|
|
217
|
+
await mkdir(dir, { recursive: true });
|
|
218
|
+
await writeFile(
|
|
219
|
+
join(dir, "HOOK.md"),
|
|
220
|
+
"---\nname: idx-hook\ndescription: uses index.ts\n---",
|
|
221
|
+
"utf-8",
|
|
222
|
+
);
|
|
223
|
+
// Only create index.ts, no handler.ts or handler
|
|
224
|
+
await writeFile(join(dir, "index.ts"), "export default () => {};", "utf-8");
|
|
225
|
+
|
|
226
|
+
const entries = await discoverHooks({ bundledDir: bundled });
|
|
227
|
+
|
|
228
|
+
expect(entries).toHaveLength(1);
|
|
229
|
+
expect(entries[0].hook.handlerPath).toContain("index.ts");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("falls back to handler (no ext) when handler.ts is missing", async () => {
|
|
233
|
+
const bundled = join(tempRoot, "bundled");
|
|
234
|
+
const dir = join(bundled, "noext-hook");
|
|
235
|
+
await mkdir(dir, { recursive: true });
|
|
236
|
+
await writeFile(
|
|
237
|
+
join(dir, "HOOK.md"),
|
|
238
|
+
"---\nname: noext-hook\ndescription: uses handler (no ext)\n---",
|
|
239
|
+
"utf-8",
|
|
240
|
+
);
|
|
241
|
+
await writeFile(join(dir, "handler"), "export default () => {};", "utf-8");
|
|
242
|
+
|
|
243
|
+
const entries = await discoverHooks({ bundledDir: bundled });
|
|
244
|
+
|
|
245
|
+
expect(entries).toHaveLength(1);
|
|
246
|
+
expect(basename(entries[0].hook.handlerPath)).toBe("handler");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("skips hook when no handler file exists", async () => {
|
|
250
|
+
const { logger } = await import("@elizaos/core");
|
|
251
|
+
const bundled = join(tempRoot, "bundled");
|
|
252
|
+
const dir = join(bundled, "no-handler");
|
|
253
|
+
await mkdir(dir, { recursive: true });
|
|
254
|
+
await writeFile(
|
|
255
|
+
join(dir, "HOOK.md"),
|
|
256
|
+
"---\nname: no-handler\ndescription: missing handler\n---",
|
|
257
|
+
"utf-8",
|
|
258
|
+
);
|
|
259
|
+
// No handler file at all
|
|
260
|
+
|
|
261
|
+
const entries = await discoverHooks({ bundledDir: bundled });
|
|
262
|
+
|
|
263
|
+
expect(entries).toHaveLength(0);
|
|
264
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
265
|
+
expect.stringContaining("no handler"),
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// 3. Discovery precedence
|
|
272
|
+
// ============================================================================
|
|
273
|
+
|
|
274
|
+
describe("discovery precedence", () => {
|
|
275
|
+
it("workspace hook overrides bundled hook with the same name", async () => {
|
|
276
|
+
const bundled = join(tempRoot, "bundled");
|
|
277
|
+
const workspace = join(tempRoot, "workspace");
|
|
278
|
+
|
|
279
|
+
await createHookDir(bundled, "shared-hook", {
|
|
280
|
+
hookMd: "---\nname: shared-hook\ndescription: bundled version\n---",
|
|
281
|
+
});
|
|
282
|
+
// workspace hooks live under <workspacePath>/hooks/
|
|
283
|
+
const wsHooks = join(workspace, "hooks");
|
|
284
|
+
await createHookDir(wsHooks, "shared-hook", {
|
|
285
|
+
hookMd: "---\nname: shared-hook\ndescription: workspace version\n---",
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const entries = await discoverHooks({
|
|
289
|
+
bundledDir: bundled,
|
|
290
|
+
workspacePath: workspace,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
expect(entries).toHaveLength(1);
|
|
294
|
+
expect(entries[0].frontmatter.description).toBe("workspace version");
|
|
295
|
+
expect(entries[0].hook.source).toBe("milady-workspace");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("collects hooks from all sources when names are unique", async () => {
|
|
299
|
+
const bundled = join(tempRoot, "bundled");
|
|
300
|
+
const extra = join(tempRoot, "extra");
|
|
301
|
+
|
|
302
|
+
await createHookDir(bundled, "hook-a", {
|
|
303
|
+
hookMd: "---\nname: hook-a\ndescription: bundled\n---",
|
|
304
|
+
});
|
|
305
|
+
await createHookDir(extra, "hook-b", {
|
|
306
|
+
hookMd: "---\nname: hook-b\ndescription: extra\n---",
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const entries = await discoverHooks({
|
|
310
|
+
bundledDir: bundled,
|
|
311
|
+
extraDirs: [extra],
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const names = entries.map((e) => e.hook.name).sort();
|
|
315
|
+
expect(names).toEqual(["hook-a", "hook-b"]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("returns empty array for nonexistent directory", async () => {
|
|
319
|
+
const entries = await discoverHooks({
|
|
320
|
+
bundledDir: join(tempRoot, "does-not-exist"),
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
expect(entries).toEqual([]);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("skips non-directory entries inside hooks dir", async () => {
|
|
327
|
+
const bundled = join(tempRoot, "bundled");
|
|
328
|
+
await mkdir(bundled, { recursive: true });
|
|
329
|
+
// Create a file (not directory) inside the hooks dir
|
|
330
|
+
await writeFile(join(bundled, "not-a-dir.txt"), "just a file", "utf-8");
|
|
331
|
+
|
|
332
|
+
await createHookDir(bundled, "real-hook", {
|
|
333
|
+
hookMd: "---\nname: real-hook\ndescription: a real hook\n---",
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const entries = await discoverHooks({ bundledDir: bundled });
|
|
337
|
+
|
|
338
|
+
expect(entries).toHaveLength(1);
|
|
339
|
+
expect(entries[0].hook.name).toBe("real-hook");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("skips directories without HOOK.md", async () => {
|
|
343
|
+
const bundled = join(tempRoot, "bundled");
|
|
344
|
+
const dir = join(bundled, "no-hookmd");
|
|
345
|
+
await mkdir(dir, { recursive: true });
|
|
346
|
+
await writeFile(
|
|
347
|
+
join(dir, "handler.ts"),
|
|
348
|
+
"export default () => {};",
|
|
349
|
+
"utf-8",
|
|
350
|
+
);
|
|
351
|
+
// No HOOK.md
|
|
352
|
+
|
|
353
|
+
const entries = await discoverHooks({ bundledDir: bundled });
|
|
354
|
+
|
|
355
|
+
expect(entries).toEqual([]);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover hooks from workspace, managed (~/.milady/hooks/), and bundled dirs.
|
|
3
|
+
* Later sources win on name conflicts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
import { logger } from "@elizaos/core";
|
|
10
|
+
import type {
|
|
11
|
+
Hook,
|
|
12
|
+
HookEntry,
|
|
13
|
+
HookSource,
|
|
14
|
+
MiladyHookMetadata,
|
|
15
|
+
ParsedHookFrontmatter,
|
|
16
|
+
} from "./types";
|
|
17
|
+
|
|
18
|
+
const HOOK_MD = "HOOK.md";
|
|
19
|
+
const HANDLER_NAMES = ["handler.ts", "handler", "index.ts", "index"];
|
|
20
|
+
|
|
21
|
+
function parseFrontmatter(content: string): ParsedHookFrontmatter | null {
|
|
22
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
23
|
+
if (!fmMatch) return null;
|
|
24
|
+
|
|
25
|
+
const fmBlock = fmMatch[1];
|
|
26
|
+
const result: ParsedHookFrontmatter = { name: "", description: "" };
|
|
27
|
+
|
|
28
|
+
for (const line of fmBlock.split("\n")) {
|
|
29
|
+
const kvMatch = line.match(/^(\w+):\s*(.+)/);
|
|
30
|
+
if (!kvMatch) continue;
|
|
31
|
+
const [, key, rawValue] = kvMatch;
|
|
32
|
+
const value = rawValue.replace(/^["']|["']$/g, "").trim();
|
|
33
|
+
|
|
34
|
+
switch (key) {
|
|
35
|
+
case "name":
|
|
36
|
+
result.name = value;
|
|
37
|
+
break;
|
|
38
|
+
case "description":
|
|
39
|
+
result.description = value;
|
|
40
|
+
break;
|
|
41
|
+
case "homepage":
|
|
42
|
+
result.homepage = value;
|
|
43
|
+
break;
|
|
44
|
+
case "metadata":
|
|
45
|
+
try {
|
|
46
|
+
const metaStart = fmBlock.indexOf("metadata:");
|
|
47
|
+
if (metaStart !== -1) {
|
|
48
|
+
const metaRest = fmBlock
|
|
49
|
+
.slice(metaStart + "metadata:".length)
|
|
50
|
+
.trim();
|
|
51
|
+
const jsonMatch = metaRest.match(/\{[\s\S]*\}/);
|
|
52
|
+
if (jsonMatch) {
|
|
53
|
+
result.metadata = JSON.parse(jsonMatch[0]);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
try {
|
|
58
|
+
result.metadata = JSON.parse(value);
|
|
59
|
+
} catch {
|
|
60
|
+
logger.warn(`[hooks] Failed to parse metadata in HOOK.md`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result.name ? result : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function extractMetadata(
|
|
71
|
+
frontmatter: ParsedHookFrontmatter,
|
|
72
|
+
): MiladyHookMetadata | undefined {
|
|
73
|
+
const milady = frontmatter.metadata?.milady;
|
|
74
|
+
if (!milady) return undefined;
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
always: milady.always,
|
|
78
|
+
hookKey: milady.hookKey,
|
|
79
|
+
emoji: milady.emoji,
|
|
80
|
+
homepage: milady.homepage ?? frontmatter.homepage,
|
|
81
|
+
events: Array.isArray(milady.events) ? milady.events : [],
|
|
82
|
+
export: milady.export,
|
|
83
|
+
os: milady.os,
|
|
84
|
+
requires: milady.requires,
|
|
85
|
+
install: milady.install,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function isDirectory(path: string): Promise<boolean> {
|
|
90
|
+
try {
|
|
91
|
+
return (await stat(path)).isDirectory();
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
98
|
+
try {
|
|
99
|
+
return (await stat(path)).isFile();
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function findHandlerPath(dir: string): Promise<string | null> {
|
|
106
|
+
for (const name of HANDLER_NAMES) {
|
|
107
|
+
const p = join(dir, name);
|
|
108
|
+
if (await fileExists(p)) return p;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function loadHookFromDir(
|
|
114
|
+
dir: string,
|
|
115
|
+
source: HookSource,
|
|
116
|
+
pluginId?: string,
|
|
117
|
+
): Promise<HookEntry | null> {
|
|
118
|
+
const hookMdPath = join(dir, HOOK_MD);
|
|
119
|
+
|
|
120
|
+
if (!(await fileExists(hookMdPath))) return null;
|
|
121
|
+
|
|
122
|
+
const handlerPath = await findHandlerPath(dir);
|
|
123
|
+
if (!handlerPath) {
|
|
124
|
+
logger.warn(`[hooks] Hook at ${dir} has HOOK.md but no handler`);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const content = await readFile(hookMdPath, "utf-8");
|
|
130
|
+
const frontmatter = parseFrontmatter(content);
|
|
131
|
+
if (!frontmatter) {
|
|
132
|
+
logger.warn(`[hooks] Invalid frontmatter in ${hookMdPath}`);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const metadata = extractMetadata(frontmatter);
|
|
137
|
+
|
|
138
|
+
const hook: Hook = {
|
|
139
|
+
name: frontmatter.name,
|
|
140
|
+
description: frontmatter.description,
|
|
141
|
+
source,
|
|
142
|
+
pluginId,
|
|
143
|
+
filePath: hookMdPath,
|
|
144
|
+
baseDir: dir,
|
|
145
|
+
handlerPath,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return { hook, frontmatter, metadata };
|
|
149
|
+
} catch (err) {
|
|
150
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
151
|
+
logger.warn(`[hooks] Error loading hook from ${dir}: ${msg}`);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function scanHooksDir(
|
|
157
|
+
dir: string,
|
|
158
|
+
source: HookSource,
|
|
159
|
+
): Promise<HookEntry[]> {
|
|
160
|
+
if (!(await isDirectory(dir))) return [];
|
|
161
|
+
|
|
162
|
+
const entries: HookEntry[] = [];
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const items = await readdir(dir);
|
|
166
|
+
for (const item of items) {
|
|
167
|
+
const itemPath = join(dir, item);
|
|
168
|
+
if (!(await isDirectory(itemPath))) continue;
|
|
169
|
+
|
|
170
|
+
const entry = await loadHookFromDir(itemPath, source);
|
|
171
|
+
if (entry) {
|
|
172
|
+
entries.push(entry);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
177
|
+
logger.warn(`[hooks] Error scanning ${dir}: ${msg}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return entries;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface DiscoveryOptions {
|
|
184
|
+
workspacePath?: string;
|
|
185
|
+
bundledDir?: string;
|
|
186
|
+
extraDirs?: string[];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Precedence: extra (lowest) -> bundled -> managed -> workspace (highest). */
|
|
190
|
+
export async function discoverHooks(
|
|
191
|
+
options: DiscoveryOptions = {},
|
|
192
|
+
): Promise<HookEntry[]> {
|
|
193
|
+
const seen = new Map<string, HookEntry>();
|
|
194
|
+
|
|
195
|
+
if (options.extraDirs) {
|
|
196
|
+
for (const dir of options.extraDirs) {
|
|
197
|
+
const resolved = resolve(dir.replace(/^~/, homedir()));
|
|
198
|
+
for (const entry of await scanHooksDir(resolved, "milady-managed")) {
|
|
199
|
+
seen.set(entry.hook.name, entry);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (options.bundledDir) {
|
|
205
|
+
for (const entry of await scanHooksDir(
|
|
206
|
+
options.bundledDir,
|
|
207
|
+
"milady-bundled",
|
|
208
|
+
)) {
|
|
209
|
+
seen.set(entry.hook.name, entry);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const managedDir = join(homedir(), ".milady", "hooks");
|
|
214
|
+
for (const entry of await scanHooksDir(managedDir, "milady-managed")) {
|
|
215
|
+
seen.set(entry.hook.name, entry);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (options.workspacePath) {
|
|
219
|
+
const wsHooksDir = join(
|
|
220
|
+
options.workspacePath.replace(/^~/, homedir()),
|
|
221
|
+
"hooks",
|
|
222
|
+
);
|
|
223
|
+
for (const entry of await scanHooksDir(wsHooksDir, "milady-workspace")) {
|
|
224
|
+
seen.set(entry.hook.name, entry);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const all = Array.from(seen.values());
|
|
229
|
+
logger.info(`[hooks] Discovered ${all.length} hooks`);
|
|
230
|
+
return all;
|
|
231
|
+
}
|