@inneranimalmedia/agentsam-sdk 2.6.9 → 2.6.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.
Files changed (176) hide show
  1. package/README.md +18 -0
  2. package/apps/cad-creator/shared/cad/package.json +1 -1
  3. package/apps/client-cms-editor/backend/package.json +3 -3
  4. package/apps/client-cms-editor/frontend/package.json +4 -4
  5. package/apps/client-cms-editor/package-lock.json +8 -8
  6. package/apps/client-cms-editor/package.json +1 -1
  7. package/apps/client-cms-editor/shared/cms/package.json +2 -2
  8. package/docs/architecture/HOOKS-MODELS-SELF-HOSTING-PLAN.md +199 -0
  9. package/examples/hyperspace-demo/index.html +16 -0
  10. package/examples/hyperspace-demo/package.json +21 -0
  11. package/examples/hyperspace-demo/src/main.tsx +160 -0
  12. package/examples/hyperspace-demo/tsconfig.json +9 -0
  13. package/examples/hyperspace-demo/vite.config.ts +24 -0
  14. package/migrations/runtime/0005_agentsam_hooks.sql +87 -0
  15. package/package.json +23 -7
  16. package/packages/agentsam-brand/package.json +1 -1
  17. package/packages/agentsam-brand/src/core/v2/compile.js +1 -1
  18. package/packages/agentsam-brand/src/core/v2/schema.js +1 -1
  19. package/packages/agentsam-contracts/package.json +1 -1
  20. package/packages/agentsam-contracts/src/errors.ts +19 -10
  21. package/packages/agentsam-errors/package.json +11 -3
  22. package/packages/agentsam-errors/protocol/agentsam.error.v2.schema.json +821 -0
  23. package/packages/agentsam-errors/protocol/error-catalog.json +3512 -0
  24. package/packages/agentsam-errors/src/envelope.js +63 -11
  25. package/packages/agentsam-errors/src/error.js +6 -0
  26. package/packages/agentsam-errors/src/fingerprint.js +9 -1
  27. package/packages/agentsam-errors/src/normalize.js +51 -6
  28. package/packages/agentsam-errors/src/recovery.js +9 -35
  29. package/packages/agentsam-errors/src/transport.js +6 -1
  30. package/packages/agentsam-errors/src/vocabulary.js +515 -126
  31. package/packages/agentsam-errors/test/catalog-v2.test.mjs +106 -0
  32. package/packages/agentsam-errors/types/index.d.ts +157 -0
  33. package/packages/agentsam-goap/package.json +2 -2
  34. package/packages/agentsam-hooks/README.md +206 -0
  35. package/packages/agentsam-hooks/bin/agentsam-hooks.mjs +64 -0
  36. package/packages/agentsam-hooks/bindings/go/command.go +55 -0
  37. package/packages/agentsam-hooks/bindings/go/errors_generated.go +402 -0
  38. package/packages/agentsam-hooks/bindings/go/go.mod +3 -0
  39. package/packages/agentsam-hooks/bindings/go/hooks.go +285 -0
  40. package/packages/agentsam-hooks/bindings/go/hooks_test.go +48 -0
  41. package/packages/agentsam-hooks/bindings/rust/Cargo.toml +11 -0
  42. package/packages/agentsam-hooks/bindings/rust/src/errors_generated.rs +396 -0
  43. package/packages/agentsam-hooks/bindings/rust/src/lib.rs +397 -0
  44. package/packages/agentsam-hooks/package.json +69 -0
  45. package/packages/agentsam-hooks/schema/manifest.json +54 -0
  46. package/packages/agentsam-hooks/scripts/clean-protocol.mjs +17 -0
  47. package/packages/agentsam-hooks/scripts/sync-protocol.mjs +27 -0
  48. package/packages/agentsam-hooks/src/adapters/agents.js +82 -0
  49. package/packages/agentsam-hooks/src/adapters/callback.js +4 -0
  50. package/packages/agentsam-hooks/src/adapters/capability.js +122 -0
  51. package/packages/agentsam-hooks/src/adapters/command.js +104 -0
  52. package/packages/agentsam-hooks/src/adapters/composite.js +31 -0
  53. package/packages/agentsam-hooks/src/adapters/http.js +58 -0
  54. package/packages/agentsam-hooks/src/adapters/lsp.js +116 -0
  55. package/packages/agentsam-hooks/src/adapters/mcp.js +76 -0
  56. package/packages/agentsam-hooks/src/adapters/provider.js +129 -0
  57. package/packages/agentsam-hooks/src/config.js +143 -0
  58. package/packages/agentsam-hooks/src/contracts.js +284 -0
  59. package/packages/agentsam-hooks/src/errors-generated.js +2732 -0
  60. package/packages/agentsam-hooks/src/index.d.ts +193 -0
  61. package/packages/agentsam-hooks/src/index.js +14 -0
  62. package/packages/agentsam-hooks/src/policy.js +34 -0
  63. package/packages/agentsam-hooks/src/runtime.js +312 -0
  64. package/packages/agentsam-hooks/src/store.js +264 -0
  65. package/packages/agentsam-hooks/test/adapters.test.mjs +216 -0
  66. package/packages/agentsam-hooks/test/runtime.test.mjs +258 -0
  67. package/packages/agentsam-knowledge/package.json +1 -1
  68. package/packages/agentsam-nav/dist/Nav.d.ts +43 -0
  69. package/packages/agentsam-nav/dist/NavCollapsible.d.ts +10 -0
  70. package/packages/agentsam-nav/dist/NavContent.d.ts +2 -0
  71. package/packages/agentsam-nav/dist/NavFooter.d.ts +2 -0
  72. package/packages/agentsam-nav/dist/NavGroup.d.ts +3 -0
  73. package/packages/agentsam-nav/dist/NavHeader.d.ts +2 -0
  74. package/packages/agentsam-nav/dist/NavMenu.d.ts +22 -0
  75. package/packages/agentsam-nav/dist/NavProvider.d.ts +35 -0
  76. package/packages/agentsam-nav/dist/NavTrigger.d.ts +2 -0
  77. package/packages/agentsam-nav/dist/Surfaces.d.ts +41 -0
  78. package/packages/agentsam-nav/dist/browser.d.ts +11 -0
  79. package/packages/agentsam-nav/dist/browser.global.js +135 -0
  80. package/packages/agentsam-nav/dist/browser.js +7290 -0
  81. package/packages/agentsam-nav/dist/contracts.d.ts +76 -0
  82. package/packages/agentsam-nav/dist/index.d.ts +11 -0
  83. package/packages/agentsam-nav/dist/index.js +7291 -0
  84. package/packages/agentsam-nav/dist/legacy.css +83 -0
  85. package/packages/agentsam-nav/dist/shell.css +113 -0
  86. package/packages/agentsam-nav/dist/theme.css +113 -0
  87. package/packages/agentsam-nav/dist/utils.d.ts +1 -0
  88. package/packages/agentsam-nav/package.json +1 -1
  89. package/packages/agentsam-queue-control/package.json +1 -1
  90. package/packages/agentsam-repository/package.json +1 -1
  91. package/packages/agentsam-repository/src/goap.js.bak-20260922011251 +670 -0
  92. package/packages/agentsam-vault/package.json +1 -1
  93. package/packages/agentsam-workbench/package.json +3 -3
  94. package/packages/connectors/cloudflare/package.json +1 -1
  95. package/packages/heuristic-theme/package.json +1 -1
  96. package/packages/identity/package.json +2 -2
  97. package/packages/providers/completeful/package.json +1 -1
  98. package/packages/theme-church-site/package.json +2 -2
  99. package/packages/theme-companions-site/package.json +2 -2
  100. package/packages/theme-floors-site/package.json +2 -2
  101. package/packages/theme-fuelnfree-site/package.json +2 -2
  102. package/packages/theme-handyman-site/package.json +2 -2
  103. package/packages/theme-insurance-site/package.json +2 -2
  104. package/packages/theme-scenes/package.json +1 -1
  105. package/packages/theme-shinshu-site/package.json +2 -2
  106. package/packages/work-graph/package.json +1 -1
  107. package/protocol/apps/agentsam.app.v1.schema.json +1 -1
  108. package/protocol/apps/agentsam.host-install.v1.schema.json +1 -1
  109. package/protocol/assets/asset-record.v1.schema.json +1 -1
  110. package/protocol/brand/brandpack.v1.schema.json +1 -1
  111. package/protocol/capabilities/capability-manifest.schema.json +1 -1
  112. package/protocol/capabilities/knowledge-search-input.schema.json +1 -0
  113. package/protocol/credentials/issue.v1.schema.json +1 -1
  114. package/protocol/database/agentsam.schema-pack.v1.schema.json +1 -1
  115. package/protocol/database/connection.v1.schema.json +1 -1
  116. package/protocol/embeddings/embedding-profile.v1.schema.json +1 -1
  117. package/protocol/errors/README.md +36 -0
  118. package/protocol/errors/agentsam.error.v2.schema.json +821 -0
  119. package/protocol/errors/error-catalog.json +3506 -158
  120. package/protocol/errors/error-envelope.schema.json +195 -52
  121. package/protocol/errors/recovery.v1.schema.json +1 -1
  122. package/protocol/features/agentsam.feature.v1.schema.json +1 -1
  123. package/protocol/hooks/README.md +18 -0
  124. package/protocol/hooks/agentsam.hook.output.v1.schema.json +27 -0
  125. package/protocol/hooks/agentsam.hook.receipt.v1.schema.json +61 -0
  126. package/protocol/hooks/agentsam.hook.v1.schema.json +35 -0
  127. package/protocol/hooks/agentsam.hooks.config.v1.schema.json +78 -0
  128. package/protocol/local-engine/benchmark.v1.schema.json +1 -1
  129. package/protocol/local-engine/engine.v1.schema.json +1 -1
  130. package/protocol/models/model-inventory-v2.schema.json +1 -1
  131. package/protocol/models/provider-model-snapshot.v1.schema.json +22 -0
  132. package/protocol/rpc/v1/errors.proto +1 -0
  133. package/protocol/runtime/agentsam.runtime.v1.schema.json +1 -1
  134. package/protocol/runtime/workspace-fs.v1.schema.json +1 -1
  135. package/protocol/sam/activity.v1.schema.json +1 -1
  136. package/protocol/sam/answer.v1.schema.json +1 -1
  137. package/protocol/sam/calibration.v1.schema.json +1 -1
  138. package/protocol/sam/decision-receipt.v1.schema.json +1 -1
  139. package/protocol/sam/evaluation.v1.schema.json +1 -1
  140. package/protocol/sam/operation.schema.json +1 -1
  141. package/protocol/sam/outcome.v1.schema.json +1 -1
  142. package/protocol/sam/question.v1.schema.json +1 -1
  143. package/protocol/sam/result.schema.json +1 -1
  144. package/protocol/sam/state.v1.schema.json +1 -1
  145. package/protocol/scoring/scorecard.v1.schema.json +1 -1
  146. package/protocol/skills/agentsam.interaction.v1.schema.json +1 -1
  147. package/protocol/skills/agentsam.skill.v1.schema.json +1 -1
  148. package/protocol/theme-refinery/product-metadata.schema.json +1 -1
  149. package/protocol/theme-refinery/reconciliation-receipt.schema.json +1 -1
  150. package/protocol/ui/icon.v1.schema.json +1 -1
  151. package/python/README.md +20 -0
  152. package/python/agentsam_sdk/errors/__init__.py +4 -0
  153. package/python/agentsam_sdk/errors/vocabulary.py +2730 -0
  154. package/python/agentsam_sdk/hooks/__init__.py +32 -0
  155. package/python/agentsam_sdk/hooks/adapters.py +132 -0
  156. package/python/agentsam_sdk/hooks/contracts.py +121 -0
  157. package/python/agentsam_sdk/hooks/runtime.py +253 -0
  158. package/python/tests/test_hooks.py +115 -0
  159. package/src/agent/responses-runner.js +145 -1
  160. package/src/cli/command-catalog.js +1 -0
  161. package/src/cli.js +9 -0
  162. package/src/commands/cheat-sheet.js +1 -0
  163. package/src/commands/hooks.js +260 -0
  164. package/src/commands/models.js +1 -1
  165. package/src/commands/shell.js +114 -9
  166. package/src/errors/diagnostic.js +9 -9
  167. package/src/index.js +32 -0
  168. package/src/knowledge/service/grpc-codec.js +2 -0
  169. package/src/lib/slash-commands.js +1 -0
  170. package/src/local/runtime-store.js +46 -0
  171. package/src/models/catalog.js +0 -49
  172. package/src/models/inventory-core.js +69 -0
  173. package/src/plugins/index.js +8 -1
  174. package/src/plugins/registry.js +56 -0
  175. package/src/rpc/generated/errors_pb.js +49 -1
  176. package/src/tools/runtime.js +13 -5
@@ -0,0 +1,143 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { HOOK_CONFIG_SCHEMA, matchesHookInput, normalizeHookEvent } from './contracts.js';
4
+ import { createHookRuntime } from './runtime.js';
5
+ import { createCommandHookAdapter } from './adapters/command.js';
6
+ import { createHttpHookAdapter } from './adapters/http.js';
7
+
8
+ export const HOOK_CONFIG_FILENAMES = Object.freeze(['agentsam.hooks.json', path.join('.agentsam', 'hooks.json')]);
9
+
10
+ function object(value) {
11
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
12
+ }
13
+
14
+ function clean(value) {
15
+ return value == null ? '' : String(value).trim();
16
+ }
17
+
18
+ function assertInside(base, candidate, label) {
19
+ const realBase = fs.realpathSync(base);
20
+ const realCandidate = fs.realpathSync(candidate);
21
+ const relative = path.relative(realBase, realCandidate);
22
+ if (relative.startsWith('..') || path.isAbsolute(relative)) throw new Error(`${label}_outside_config_directory:${candidate}`);
23
+ }
24
+
25
+ function resolveEnvReferences(value, env) {
26
+ if (Array.isArray(value)) return value.map((row) => resolveEnvReferences(row, env));
27
+ if (object(value)) return Object.fromEntries(Object.entries(value).map(([key, row]) => [key, resolveEnvReferences(row, env)]));
28
+ if (typeof value !== 'string' || !value.startsWith('env:')) return value;
29
+ const name = value.slice(4);
30
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error(`invalid_hook_env_reference:${name}`);
31
+ if (env[name] == null) throw new Error(`missing_hook_environment:${name}`);
32
+ return env[name];
33
+ }
34
+
35
+ export function findHookConfig(startDirectory = process.cwd(), options = {}) {
36
+ let current = path.resolve(startDirectory);
37
+ const stopAt = path.resolve(options.stopAt || path.parse(current).root);
38
+ while (true) {
39
+ for (const filename of options.filenames || HOOK_CONFIG_FILENAMES) {
40
+ const candidate = path.join(current, filename);
41
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
42
+ }
43
+ if (current === stopAt || current === path.parse(current).root) return null;
44
+ const parent = path.dirname(current);
45
+ if (parent === current) return null;
46
+ current = parent;
47
+ }
48
+ }
49
+
50
+ export function normalizeHookConfig(value, options = {}) {
51
+ if (!object(value)) throw new TypeError('hook_config_must_be_object');
52
+ for (const key of Object.keys(value)) {
53
+ if (!['schema', 'adapters', 'hooks'].includes(key)) throw new Error(`unsupported_hook_config_field:${key}`);
54
+ }
55
+ const schema = clean(value.schema);
56
+ if (schema !== HOOK_CONFIG_SCHEMA) throw new Error(`unsupported_hook_config_schema:${schema}`);
57
+ const adapters = object(value.adapters) ? value.adapters : {};
58
+ const hooks = object(value.hooks) ? value.hooks : {};
59
+ const normalizedAdapters = {};
60
+ for (const [id, adapter] of Object.entries(adapters)) {
61
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(id) || !object(adapter)) throw new Error(`invalid_hook_adapter:${id}`);
62
+ const type = clean(adapter.type).toLowerCase();
63
+ if (!['command', 'http'].includes(type)) throw new Error(`unsupported_hook_adapter_type:${type || '<missing>'}`);
64
+ const allowed = type === 'command'
65
+ ? new Set(['type', 'command', 'args', 'cwd', 'env', 'inherit_environment', 'timeout_ms', 'max_output_bytes'])
66
+ : new Set(['type', 'url', 'headers', 'timeout_ms', 'max_response_bytes']);
67
+ for (const key of Object.keys(adapter)) if (!allowed.has(key)) throw new Error(`unsupported_hook_adapter_field:${id}:${key}`);
68
+ normalizedAdapters[id] = structuredClone(adapter);
69
+ }
70
+ const normalizedHooks = {};
71
+ for (const [event, entries] of Object.entries(hooks)) {
72
+ const hook = normalizeHookEvent(event);
73
+ normalizedHooks[hook] = (Array.isArray(entries) ? entries : [entries]).map((entry, index) => {
74
+ if (typeof entry === 'string') return { id: `${hook}:${index + 1}`, adapter: entry };
75
+ if (!object(entry)) throw new TypeError(`invalid_configured_hook:${hook}:${index}`);
76
+ for (const key of Object.keys(entry)) {
77
+ if (!['id', 'adapter', 'priority', 'timeout_ms', 'failure_mode', 'enabled', 'match', 'metadata'].includes(key)) {
78
+ throw new Error(`unsupported_configured_hook_field:${hook}:${key}`);
79
+ }
80
+ }
81
+ const adapter = clean(entry.adapter);
82
+ if (!normalizedAdapters[adapter]) throw new Error(`unknown_hook_adapter:${adapter}`);
83
+ if (entry.match !== undefined && !object(entry.match)) throw new TypeError(`configured_hook_match_must_be_object:${hook}:${index}`);
84
+ return { ...structuredClone(entry), id: clean(entry.id || `${hook}:${index + 1}`), adapter };
85
+ });
86
+ }
87
+ return Object.freeze({
88
+ schema: HOOK_CONFIG_SCHEMA,
89
+ adapters: Object.freeze(normalizedAdapters),
90
+ hooks: Object.freeze(normalizedHooks),
91
+ source: options.source || null,
92
+ });
93
+ }
94
+
95
+ export function loadHookConfig(filename, options = {}) {
96
+ const discovered = filename || findHookConfig(options.cwd || process.cwd(), options);
97
+ if (!discovered) throw new Error('agentsam_hook_config_not_found');
98
+ const resolved = path.resolve(discovered);
99
+ let parsed;
100
+ try { parsed = JSON.parse(fs.readFileSync(resolved, 'utf8')); }
101
+ catch (error) { throw new Error(`hook_config_unreadable:${resolved}:${error.message}`); }
102
+ return normalizeHookConfig(parsed, { source: resolved });
103
+ }
104
+
105
+ export function createConfiguredHookAdapter(definition, configDirectory, options = {}) {
106
+ const resolved = resolveEnvReferences(definition, options.env || process.env);
107
+ if (resolved.type === 'http') return createHttpHookAdapter({ ...resolved, fetchImpl: options.fetchImpl });
108
+ const cwd = resolved.cwd ? path.resolve(configDirectory, resolved.cwd) : configDirectory;
109
+ if (options.restrictCommandCwd !== false) assertInside(configDirectory, cwd, 'hook_command_cwd');
110
+ let command = clean(resolved.command);
111
+ if (!command) throw new TypeError('configured_hook_command_required');
112
+ if (command.startsWith('./') || command.startsWith('../')) command = path.resolve(configDirectory, command);
113
+ return createCommandHookAdapter({ ...resolved, command, cwd });
114
+ }
115
+
116
+ export function createHookRuntimeFromConfig(configOrFilename, options = {}) {
117
+ const config = typeof configOrFilename === 'string' || configOrFilename == null
118
+ ? loadHookConfig(configOrFilename, options)
119
+ : normalizeHookConfig(configOrFilename, { source: options.source });
120
+ const configDirectory = config.source ? path.dirname(config.source) : path.resolve(options.cwd || process.cwd());
121
+ const handlers = new Map();
122
+ for (const [id, definition] of Object.entries(config.adapters)) {
123
+ handlers.set(id, createConfiguredHookAdapter(definition, configDirectory, options));
124
+ }
125
+ const runtime = options.runtime || createHookRuntime({ clock: options.clock, onReceipt: options.onReceipt });
126
+ for (const [hook, entries] of Object.entries(config.hooks)) {
127
+ for (const entry of entries) {
128
+ runtime.register(hook, {
129
+ ...entry,
130
+ matches: entry.match ? (envelope) => matchesHookInput(entry.match, envelope.input) : undefined,
131
+ handler: handlers.get(entry.adapter),
132
+ metadata: {
133
+ ...(entry.metadata || {}),
134
+ hook_source: 'config',
135
+ adapter: entry.adapter,
136
+ adapter_type: config.adapters[entry.adapter]?.type || null,
137
+ config_source: config.source,
138
+ },
139
+ });
140
+ }
141
+ }
142
+ return Object.freeze({ runtime, config });
143
+ }
@@ -0,0 +1,284 @@
1
+ export const HOOK_PROTOCOL_SCHEMA = 'agentsam.hook.v1';
2
+ export const HOOK_CONFIG_SCHEMA = 'agentsam.hooks.config.v1';
3
+ export const HOOK_RECEIPT_SCHEMA = 'agentsam.hook.receipt.v1';
4
+
5
+ export const HOOK_EVENTS = Object.freeze([
6
+ 'session_start',
7
+ 'session_end',
8
+ 'user_prompt_submitted',
9
+ 'user_prompt_transformed',
10
+ 'pre_model_use',
11
+ 'post_model_use',
12
+ 'pre_tool_use',
13
+ 'post_tool_use',
14
+ 'post_tool_use_failure',
15
+ 'error_occurred',
16
+ 'agent_stop',
17
+ 'subagent_start',
18
+ 'subagent_stop',
19
+ ]);
20
+
21
+ export const PERMISSION_DECISIONS = Object.freeze(['allow', 'deny', 'ask']);
22
+ export const ERROR_HANDLING_DECISIONS = Object.freeze(['retry', 'skip', 'abort']);
23
+ export const STOP_DECISIONS = Object.freeze(['allow', 'block']);
24
+ export const HOOK_FAILURE_MODES = Object.freeze(['open', 'closed', 'error']);
25
+
26
+ const EVENT_SET = new Set(HOOK_EVENTS);
27
+ const PERMISSION_SET = new Set(PERMISSION_DECISIONS);
28
+ const ERROR_HANDLING_SET = new Set(ERROR_HANDLING_DECISIONS);
29
+ const STOP_SET = new Set(STOP_DECISIONS);
30
+ const FAILURE_MODE_SET = new Set(HOOK_FAILURE_MODES);
31
+
32
+ const EVENT_ALIASES = Object.freeze(Object.fromEntries(HOOK_EVENTS.flatMap((event) => {
33
+ const camel = event.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
34
+ const pascal = camel[0].toUpperCase() + camel.slice(1);
35
+ return [[event, event], [camel, event], [`on${pascal}`, event]];
36
+ })));
37
+
38
+ const OUTPUT_ALIASES = Object.freeze({
39
+ permissionDecision: 'permission_decision',
40
+ permissionDecisionReason: 'permission_decision_reason',
41
+ modifiedArgs: 'modified_args',
42
+ modifiedResult: 'modified_result',
43
+ modifiedRequest: 'modified_request',
44
+ modifiedPrompt: 'modified_prompt',
45
+ modifiedTransformedPrompt: 'modified_transformed_prompt',
46
+ modifiedConfig: 'modified_config',
47
+ additionalContext: 'additional_context',
48
+ suppressOutput: 'suppress_output',
49
+ errorHandling: 'error_handling',
50
+ retryCount: 'retry_count',
51
+ userNotification: 'user_notification',
52
+ cleanupActions: 'cleanup_actions',
53
+ sessionSummary: 'session_summary',
54
+ });
55
+ const OUTPUT_FIELDS = new Set([
56
+ 'permission_decision', 'permission_decision_reason', 'modified_args', 'modified_result',
57
+ 'modified_request', 'modified_prompt', 'modified_transformed_prompt', 'modified_config', 'additional_context',
58
+ 'suppress_output', 'error_handling', 'retry_count', 'user_notification', 'decision',
59
+ 'reason', 'cleanup_actions', 'session_summary', 'metadata',
60
+ ]);
61
+
62
+ function clean(value) {
63
+ return value == null ? '' : String(value).trim();
64
+ }
65
+
66
+ function isObject(value) {
67
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
68
+ }
69
+
70
+ function clone(value) {
71
+ return value == null ? value : structuredClone(value);
72
+ }
73
+
74
+ function matchValue(expected, actual) {
75
+ if (expected == null || typeof expected !== 'object') return Object.is(expected, actual);
76
+ if (Array.isArray(expected)) {
77
+ return Array.isArray(actual) && expected.every((item) => actual.some((candidate) => matchValue(item, candidate)));
78
+ }
79
+ if ('$exists' in expected) return Boolean(expected.$exists) === (actual !== undefined && actual !== null);
80
+ if ('$eq' in expected) return matchValue(expected.$eq, actual);
81
+ if ('$in' in expected) {
82
+ if (!Array.isArray(expected.$in)) throw new TypeError('hook_match_$in_must_be_array');
83
+ return expected.$in.some((candidate) => matchValue(candidate, actual));
84
+ }
85
+ if ('$contains' in expected) {
86
+ if (typeof actual === 'string') return actual.includes(String(expected.$contains));
87
+ if (Array.isArray(actual)) return actual.some((candidate) => matchValue(expected.$contains, candidate));
88
+ return false;
89
+ }
90
+ if (!actual || typeof actual !== 'object' || Array.isArray(actual)) return false;
91
+ const entries = Object.entries(expected);
92
+ const unsupported = entries.find(([key]) => key.startsWith('$'));
93
+ if (unsupported) throw new Error(`unsupported_hook_match_operator:${unsupported[0]}`);
94
+ return entries.every(([key, value]) => matchValue(value, actual[key]));
95
+ }
96
+
97
+ /** Portable partial JSON matcher shared by config and stored hooks. */
98
+ export function matchesHookInput(match = {}, input = {}) {
99
+ if (!isObject(match)) throw new TypeError('hook_match_must_be_object');
100
+ return matchValue(match, input);
101
+ }
102
+
103
+ function asOptionalString(value, field = 'value') {
104
+ if (value == null) return undefined;
105
+ if (typeof value !== 'string') throw new TypeError(`${field}_must_be_string`);
106
+ const normalized = value.trim();
107
+ return normalized || undefined;
108
+ }
109
+
110
+ export function normalizeHookEvent(value) {
111
+ const source = clean(value);
112
+ const normalized = EVENT_ALIASES[source] || EVENT_ALIASES[source.replace(/^on_/, '')];
113
+ if (!normalized || !EVENT_SET.has(normalized)) throw new RangeError(`unsupported_hook_event:${source || '<missing>'}`);
114
+ return normalized;
115
+ }
116
+
117
+ export function normalizeHookFailureMode(value, hook) {
118
+ const fallback = hook === 'pre_tool_use' || hook === 'pre_model_use' ? 'closed' : 'open';
119
+ const normalized = clean(value || fallback).toLowerCase();
120
+ if (!FAILURE_MODE_SET.has(normalized)) throw new RangeError(`invalid_hook_failure_mode:${normalized}`);
121
+ return normalized;
122
+ }
123
+
124
+ export function createHookInvocation(value = {}) {
125
+ if (!isObject(value)) throw new TypeError('hook_invocation_must_be_object');
126
+ const output = {};
127
+ const stringFields = ['session_id', 'run_id', 'turn_id', 'message_id', 'agent_id', 'parent_agent_id', 'source'];
128
+ for (const [key, raw] of Object.entries(value)) {
129
+ if (!stringFields.includes(key) && key !== 'metadata') throw new Error(`unsupported_hook_invocation_field:${key}`);
130
+ if (raw == null) continue;
131
+ if (stringFields.includes(key)) {
132
+ if (typeof raw !== 'string') throw new TypeError(`${key}_must_be_string`);
133
+ const normalized = raw.trim();
134
+ if (normalized) output[key] = normalized;
135
+ } else {
136
+ if (!isObject(raw)) throw new TypeError('hook_invocation_metadata_must_be_object');
137
+ output.metadata = clone(raw);
138
+ }
139
+ }
140
+ return Object.freeze(output);
141
+ }
142
+
143
+ export function createHookEnvelope(event, input = {}, invocation = {}, options = {}) {
144
+ const hook = normalizeHookEvent(event);
145
+ if (!isObject(input)) throw new TypeError('hook_input_must_be_object');
146
+ const timestamp = Number(options.timestamp ?? Date.now());
147
+ if (!Number.isFinite(timestamp) || timestamp < 0) throw new TypeError('hook_timestamp_must_be_non_negative_number');
148
+ return Object.freeze({
149
+ schema: HOOK_PROTOCOL_SCHEMA,
150
+ hook,
151
+ timestamp,
152
+ cwd: clean(options.cwd || input.cwd || (typeof process !== 'undefined' && typeof process.cwd === 'function' ? process.cwd() : '')),
153
+ invocation: createHookInvocation(invocation),
154
+ input: Object.freeze(clone(input)),
155
+ });
156
+ }
157
+
158
+ function withAliases(value) {
159
+ const source = isObject(value) ? value : {};
160
+ const output = {};
161
+ for (const [key, raw] of Object.entries(source)) {
162
+ const normalized = OUTPUT_ALIASES[key] || key;
163
+ if (!OUTPUT_FIELDS.has(normalized)) throw new Error(`unsupported_hook_output_field:${key}`);
164
+ output[normalized] = raw;
165
+ }
166
+ return output;
167
+ }
168
+
169
+ export function normalizeHookOutput(event, value) {
170
+ const hook = normalizeHookEvent(event);
171
+ if (value == null) return Object.freeze({});
172
+ if (!isObject(value)) throw new TypeError(`hook_output_must_be_object:${hook}`);
173
+ const source = withAliases(value);
174
+ const output = {};
175
+
176
+ const permission = asOptionalString(source.permission_decision, 'permission_decision')?.toLowerCase();
177
+ if (permission) {
178
+ if (!['pre_tool_use', 'pre_model_use'].includes(hook)) throw new Error(`permission_decision_not_supported:${hook}`);
179
+ if (!PERMISSION_SET.has(permission)) throw new RangeError(`invalid_permission_decision:${permission}`);
180
+ output.permission_decision = permission;
181
+ }
182
+ const permissionReason = asOptionalString(source.permission_decision_reason, 'permission_decision_reason');
183
+ if (permissionReason) output.permission_decision_reason = permissionReason;
184
+
185
+ if (source.modified_args !== undefined) {
186
+ if (hook !== 'pre_tool_use') throw new Error(`modified_args_not_supported:${hook}`);
187
+ if (!isObject(source.modified_args)) throw new TypeError('modified_args_must_be_object');
188
+ output.modified_args = clone(source.modified_args);
189
+ }
190
+ if (source.modified_request !== undefined) {
191
+ if (hook !== 'pre_model_use') throw new Error(`modified_request_not_supported:${hook}`);
192
+ if (!isObject(source.modified_request)) throw new TypeError('modified_request_must_be_object');
193
+ output.modified_request = clone(source.modified_request);
194
+ }
195
+ if (source.modified_result !== undefined) {
196
+ if (!['post_tool_use', 'post_model_use'].includes(hook)) throw new Error(`modified_result_not_supported:${hook}`);
197
+ output.modified_result = clone(source.modified_result);
198
+ }
199
+ if (source.modified_prompt !== undefined) {
200
+ if (hook !== 'user_prompt_submitted') throw new Error(`modified_prompt_not_supported:${hook}`);
201
+ if (typeof source.modified_prompt !== 'string') throw new TypeError('modified_prompt_must_be_string');
202
+ output.modified_prompt = source.modified_prompt;
203
+ }
204
+ if (source.modified_transformed_prompt !== undefined) {
205
+ if (hook !== 'user_prompt_transformed') throw new Error(`modified_transformed_prompt_not_supported:${hook}`);
206
+ if (typeof source.modified_transformed_prompt !== 'string') throw new TypeError('modified_transformed_prompt_must_be_string');
207
+ output.modified_transformed_prompt = source.modified_transformed_prompt;
208
+ }
209
+ if (source.modified_config !== undefined) {
210
+ if (hook !== 'session_start') throw new Error(`modified_config_not_supported:${hook}`);
211
+ if (!isObject(source.modified_config)) throw new TypeError('modified_config_must_be_object');
212
+ output.modified_config = clone(source.modified_config);
213
+ }
214
+
215
+ const context = asOptionalString(source.additional_context, 'additional_context');
216
+ if (context) output.additional_context = context;
217
+ if (source.suppress_output !== undefined) {
218
+ if (typeof source.suppress_output !== 'boolean') throw new TypeError('suppress_output_must_be_boolean');
219
+ output.suppress_output = source.suppress_output;
220
+ }
221
+
222
+ const handling = asOptionalString(source.error_handling, 'error_handling')?.toLowerCase();
223
+ if (handling) {
224
+ if (hook !== 'error_occurred') throw new Error(`error_handling_not_supported:${hook}`);
225
+ if (!ERROR_HANDLING_SET.has(handling)) throw new RangeError(`invalid_error_handling:${handling}`);
226
+ output.error_handling = handling;
227
+ }
228
+ if (source.retry_count !== undefined) {
229
+ const count = Number(source.retry_count);
230
+ if (!Number.isInteger(count) || count < 0 || count > 10) throw new TypeError('retry_count_must_be_integer_between_0_and_10');
231
+ output.retry_count = count;
232
+ }
233
+ const notification = asOptionalString(source.user_notification, 'user_notification');
234
+ if (notification) output.user_notification = notification;
235
+
236
+ const decision = asOptionalString(source.decision, 'decision')?.toLowerCase();
237
+ if (decision) {
238
+ if (hook !== 'agent_stop') throw new Error(`stop_decision_not_supported:${hook}`);
239
+ if (!STOP_SET.has(decision)) throw new RangeError(`invalid_stop_decision:${decision}`);
240
+ output.decision = decision;
241
+ }
242
+ const reason = asOptionalString(source.reason, 'reason');
243
+ if (reason) output.reason = reason;
244
+
245
+ if (source.cleanup_actions !== undefined) {
246
+ if (hook !== 'session_end' || !Array.isArray(source.cleanup_actions) || source.cleanup_actions.some((row) => typeof row !== 'string')) {
247
+ throw new TypeError('cleanup_actions_must_be_session_end_string_array');
248
+ }
249
+ output.cleanup_actions = [...source.cleanup_actions];
250
+ }
251
+ const summary = asOptionalString(source.session_summary, 'session_summary');
252
+ if (summary) output.session_summary = summary;
253
+ if (source.metadata !== undefined) {
254
+ if (!isObject(source.metadata)) throw new TypeError('metadata_must_be_object');
255
+ output.metadata = clone(source.metadata);
256
+ }
257
+
258
+ return Object.freeze(output);
259
+ }
260
+
261
+ export function normalizeHookDefinition(event, value, index = 0) {
262
+ const hook = normalizeHookEvent(event);
263
+ const source = typeof value === 'function' ? { handler: value } : value;
264
+ if (!isObject(source)) throw new TypeError(`hook_definition_must_be_object:${hook}`);
265
+ if (typeof source.handler !== 'function') throw new TypeError(`hook_handler_required:${hook}`);
266
+ if (source.matches != null && typeof source.matches !== 'function') throw new TypeError(`hook_matches_must_be_function:${hook}`);
267
+ const id = clean(source.id || `${hook}:${index + 1}`);
268
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(id)) throw new Error(`invalid_hook_id:${id}`);
269
+ const timeoutMs = Number(source.timeout_ms ?? source.timeoutMs ?? 10_000);
270
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 600_000) throw new RangeError(`invalid_hook_timeout_ms:${timeoutMs}`);
271
+ const priority = Number(source.priority ?? 100);
272
+ if (!Number.isFinite(priority)) throw new TypeError(`invalid_hook_priority:${priority}`);
273
+ return Object.freeze({
274
+ id,
275
+ hook,
276
+ handler: source.handler,
277
+ matches: source.matches,
278
+ priority,
279
+ timeout_ms: timeoutMs,
280
+ failure_mode: normalizeHookFailureMode(source.failure_mode ?? source.failureMode, hook),
281
+ enabled: source.enabled !== false,
282
+ metadata: Object.freeze(isObject(source.metadata) ? clone(source.metadata) : {}),
283
+ });
284
+ }