memorix 1.8.7 → 1.8.9
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/CHANGELOG.md +26 -0
- package/README.md +9 -1
- package/README.zh-CN.md +10 -1
- package/dist/cli/index.js +466 -256
- package/dist/index.js +313 -179
- package/dist/memcode-runtime/CHANGELOG.md +26 -0
- package/dist/sdk.js +306 -172
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -2
- package/docs/INTEGRATIONS.md +1 -0
- package/docs/hooks-architecture.md +18 -13
- package/llms-full.txt +3 -1
- package/llms.txt +2 -1
- package/package.json +3 -2
- package/plugins/antigravity/memorix/plugin.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +2 -2
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +2 -2
- package/plugins/codex/memorix/.codex-plugin/plugin.json +3 -3
- package/plugins/copilot/memorix/plugin.json +2 -2
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/hermes/memorix/plugin.yaml +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +3 -3
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +3 -3
- package/src/cli/commands/agent-integrations.ts +2 -1
- package/src/cli/commands/hooks-install.ts +2 -0
- package/src/cli/commands/integrate.ts +2 -0
- package/src/cli/commands/setup.ts +12 -5
- package/src/hooks/handler.ts +6 -0
- package/src/hooks/installers/index.ts +181 -6
- package/src/hooks/normalizer.ts +60 -1
- package/src/hooks/types.ts +3 -1
- package/src/integrations/registry.ts +9 -0
|
@@ -30,6 +30,10 @@ function resolveHookCommand(): string {
|
|
|
30
30
|
return 'memorix';
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function getGrokHome(): string {
|
|
34
|
+
return process.env.GROK_HOME?.trim() || path.join(os.homedir(), '.grok');
|
|
35
|
+
}
|
|
36
|
+
|
|
33
37
|
function resolveWindowsMemorixShim(): string | null {
|
|
34
38
|
try {
|
|
35
39
|
const output = String(execSync('where.exe memorix.cmd', {
|
|
@@ -83,6 +87,114 @@ function generateClaudeConfig(): Record<string, unknown> {
|
|
|
83
87
|
};
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Generate Grok Build hook config.
|
|
92
|
+
* Format: ~/.grok/hooks/memorix.json (own file; do not merge into RuleSync dests)
|
|
93
|
+
* Grok uses its official JSON hook format with Claude-compatible event names. Command is `memorix hook` (a
|
|
94
|
+
* shell command, not a relative path — Grok prefixes non-absolute commands
|
|
95
|
+
* with the JSON file directory).
|
|
96
|
+
* See: https://docs.x.ai grok hooks user guide
|
|
97
|
+
*/
|
|
98
|
+
function generateGrokConfig(): Record<string, unknown> {
|
|
99
|
+
const cmd = `${resolveHookCommand()} hook --agent grok`;
|
|
100
|
+
const hookEntry = {
|
|
101
|
+
type: 'command',
|
|
102
|
+
command: cmd,
|
|
103
|
+
timeout: 10,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
hooks: {
|
|
108
|
+
SessionStart: [{ hooks: [hookEntry] }],
|
|
109
|
+
SessionEnd: [{ hooks: [hookEntry] }],
|
|
110
|
+
PostToolUse: [{ hooks: [hookEntry] }],
|
|
111
|
+
PostToolUseFailure: [{ hooks: [hookEntry] }],
|
|
112
|
+
UserPromptSubmit: [{ hooks: [hookEntry] }],
|
|
113
|
+
PreCompact: [{ hooks: [hookEntry] }],
|
|
114
|
+
PostCompact: [{ hooks: [hookEntry] }],
|
|
115
|
+
Stop: [{ hooks: [hookEntry] }],
|
|
116
|
+
StopFailure: [{ hooks: [hookEntry] }],
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isJsonRecord(value: unknown): value is Record<string, unknown> {
|
|
122
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isMemorixGrokCommand(value: unknown): boolean {
|
|
126
|
+
return value === 'memorix hook --agent grok' || value === 'memorix.cmd hook --agent grok';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isMemorixGrokHook(value: unknown): boolean {
|
|
130
|
+
return isJsonRecord(value) && value.type === 'command' && isMemorixGrokCommand(value.command);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function mergeGrokHookConfig(
|
|
134
|
+
existing: Record<string, unknown>,
|
|
135
|
+
generated: Record<string, unknown>,
|
|
136
|
+
): Record<string, unknown> {
|
|
137
|
+
const merged = { ...existing };
|
|
138
|
+
const existingHooks = isJsonRecord(existing.hooks) ? { ...existing.hooks } : {};
|
|
139
|
+
const generatedHooks = isJsonRecord(generated.hooks) ? generated.hooks : {};
|
|
140
|
+
|
|
141
|
+
for (const [event, generatedGroups] of Object.entries(generatedHooks)) {
|
|
142
|
+
const currentGroups = Array.isArray(existingHooks[event]) ? existingHooks[event] : [];
|
|
143
|
+
const preservedGroups = currentGroups.flatMap((group) => {
|
|
144
|
+
if (!isJsonRecord(group) || !Array.isArray(group.hooks)) return [group];
|
|
145
|
+
const preservedHooks = group.hooks.filter((hook) => !isMemorixGrokHook(hook));
|
|
146
|
+
if (preservedHooks.length === 0) return [];
|
|
147
|
+
return [{ ...group, hooks: preservedHooks }];
|
|
148
|
+
});
|
|
149
|
+
existingHooks[event] = [...preservedGroups, ...(Array.isArray(generatedGroups) ? generatedGroups : [])];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
merged.hooks = existingHooks;
|
|
153
|
+
return merged;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function removeMemorixGrokHooks(config: Record<string, unknown>): Record<string, unknown> {
|
|
157
|
+
const cleaned = { ...config };
|
|
158
|
+
const hooks = isJsonRecord(config.hooks) ? { ...config.hooks } : null;
|
|
159
|
+
if (!hooks) return cleaned;
|
|
160
|
+
|
|
161
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
162
|
+
if (!Array.isArray(groups)) continue;
|
|
163
|
+
const preservedGroups = groups.flatMap((group) => {
|
|
164
|
+
if (!isJsonRecord(group) || !Array.isArray(group.hooks)) return [group];
|
|
165
|
+
const preservedHooks = group.hooks.filter((hook) => !isMemorixGrokHook(hook));
|
|
166
|
+
if (preservedHooks.length === 0) return [];
|
|
167
|
+
return [{ ...group, hooks: preservedHooks }];
|
|
168
|
+
});
|
|
169
|
+
if (preservedGroups.length > 0) hooks[event] = preservedGroups;
|
|
170
|
+
else delete hooks[event];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (Object.keys(hooks).length > 0) cleaned.hooks = hooks;
|
|
174
|
+
else delete cleaned.hooks;
|
|
175
|
+
return cleaned;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function readGrokHookConfig(configPath: string): Promise<Record<string, unknown>> {
|
|
179
|
+
let content: string;
|
|
180
|
+
try {
|
|
181
|
+
content = await fs.readFile(configPath, 'utf-8');
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return {};
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!content.trim()) return {};
|
|
188
|
+
try {
|
|
189
|
+
const parsed: unknown = JSON.parse(content);
|
|
190
|
+
if (!isJsonRecord(parsed)) throw new Error('top-level value must be an object');
|
|
191
|
+
return parsed;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
194
|
+
throw new Error(`Cannot safely update Grok hook config ${configPath}: ${reason}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
86
198
|
/**
|
|
87
199
|
* Generate GitHub Copilot hook config.
|
|
88
200
|
* Format: .github/hooks/memorix.json — version:1 + bash/powershell fields
|
|
@@ -658,6 +770,8 @@ export function getProjectConfigPath(agent: AgentName, projectRoot: string): str
|
|
|
658
770
|
// uninstall all treat hooks as a no-op instead of falling through to the
|
|
659
771
|
// `.memorix/hooks.json` default (which would report a path that never exists).
|
|
660
772
|
return '';
|
|
773
|
+
case 'grok':
|
|
774
|
+
return path.join(projectRoot, '.grok', 'hooks', 'memorix.json');
|
|
661
775
|
default:
|
|
662
776
|
return path.join(projectRoot, '.memorix', 'hooks.json');
|
|
663
777
|
}
|
|
@@ -706,6 +820,8 @@ export function getGlobalConfigPath(agent: AgentName): string {
|
|
|
706
820
|
case 'workbuddy':
|
|
707
821
|
// WorkBuddy has no user-global instructions file — guidance is project-only.
|
|
708
822
|
return '';
|
|
823
|
+
case 'grok':
|
|
824
|
+
return path.join(getGrokHome(), 'hooks', 'memorix.json');
|
|
709
825
|
default:
|
|
710
826
|
return path.join(home, '.memorix', 'hooks.json');
|
|
711
827
|
}
|
|
@@ -734,6 +850,8 @@ export function getAgentRulesPath(agent: AgentName, root: string, global = false
|
|
|
734
850
|
case 'dsh':
|
|
735
851
|
// The user-global AGENTS.md lives in the harness home DSH reads.
|
|
736
852
|
return path.join(process.env.DSH_HOME?.trim() || path.join(root, '.dsh'), 'AGENTS.md');
|
|
853
|
+
case 'grok':
|
|
854
|
+
return path.join(getGrokHome(), 'AGENTS.md');
|
|
737
855
|
case 'workbuddy':
|
|
738
856
|
// WorkBuddy has no user-global instructions file — guidance is project-only.
|
|
739
857
|
return '';
|
|
@@ -766,6 +884,8 @@ export function getAgentRulesPath(agent: AgentName, root: string, global = false
|
|
|
766
884
|
return path.join(root, 'AGENTS.md');
|
|
767
885
|
case 'workbuddy':
|
|
768
886
|
return path.join(root, 'AGENTS.md');
|
|
887
|
+
case 'grok':
|
|
888
|
+
return path.join(root, 'AGENTS.md');
|
|
769
889
|
default:
|
|
770
890
|
return path.join(root, '.agent', 'rules', 'memorix.md');
|
|
771
891
|
}
|
|
@@ -837,6 +957,13 @@ export async function detectInstalledAgents(): Promise<AgentName[]> {
|
|
|
837
957
|
agents.push('kiro');
|
|
838
958
|
} catch { /* not installed */ }
|
|
839
959
|
|
|
960
|
+
// Check for Grok Build
|
|
961
|
+
const grokDir = getGrokHome();
|
|
962
|
+
try {
|
|
963
|
+
await fs.access(grokDir);
|
|
964
|
+
agents.push('grok');
|
|
965
|
+
} catch { /* not installed */ }
|
|
966
|
+
|
|
840
967
|
// Check for Codex
|
|
841
968
|
const codexDir = path.join(home, '.codex');
|
|
842
969
|
try {
|
|
@@ -953,6 +1080,12 @@ export async function installHooks(
|
|
|
953
1080
|
if (basename === 'AGENTS.md' || basename === 'GEMINI.md' || basename === 'CONTEXT.md') {
|
|
954
1081
|
continue;
|
|
955
1082
|
}
|
|
1083
|
+
// Grok's dedicated JSON file may also contain user-owned hook groups.
|
|
1084
|
+
// Its merge/uninstall path removes only Memorix commands, so never
|
|
1085
|
+
// delete the whole file during a reinstall cleanup.
|
|
1086
|
+
if (agent === 'grok' && path.resolve(entry.path) === path.resolve(configPath)) {
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
956
1089
|
await unlink(entry.path);
|
|
957
1090
|
await removeFile(projectRoot, entry.path);
|
|
958
1091
|
} catch { /* file already gone */ }
|
|
@@ -965,6 +1098,9 @@ export async function installHooks(
|
|
|
965
1098
|
case 'claude':
|
|
966
1099
|
generated = generateClaudeConfig();
|
|
967
1100
|
break;
|
|
1101
|
+
case 'grok':
|
|
1102
|
+
generated = generateGrokConfig();
|
|
1103
|
+
break;
|
|
968
1104
|
case 'copilot':
|
|
969
1105
|
generated = generateCopilotConfig();
|
|
970
1106
|
break;
|
|
@@ -1080,7 +1216,19 @@ export async function installHooks(
|
|
|
1080
1216
|
// Ensure directory exists
|
|
1081
1217
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
1082
1218
|
|
|
1083
|
-
if (agent === '
|
|
1219
|
+
if (agent === 'grok') {
|
|
1220
|
+
// Grok discovers one file per hook source and does not require the
|
|
1221
|
+
// version field used by Cursor. Preserve any user-owned hook groups in
|
|
1222
|
+
// this file and replace only Memorix's own command entries.
|
|
1223
|
+
const existing = await readGrokHookConfig(configPath);
|
|
1224
|
+
const merged = mergeGrokHookConfig(existing, generated as Record<string, unknown>);
|
|
1225
|
+
await fs.writeFile(configPath, JSON.stringify(merged, null, 2), 'utf-8');
|
|
1226
|
+
|
|
1227
|
+
try {
|
|
1228
|
+
const { recordFile } = await import('../../audit/index.js');
|
|
1229
|
+
await recordFile(projectRoot, 'hook', configPath, agent);
|
|
1230
|
+
} catch { /* audit is optional */ }
|
|
1231
|
+
} else if (agent === 'kiro') {
|
|
1084
1232
|
// Kiro uses multiple .kiro.hook files
|
|
1085
1233
|
const hookFiles = generateKiroHookFiles();
|
|
1086
1234
|
const hooksDir = path.join(path.dirname(configPath));
|
|
@@ -1186,6 +1334,9 @@ export async function installHooks(
|
|
|
1186
1334
|
case 'kiro':
|
|
1187
1335
|
events.push('session_end', 'user_prompt', 'post_edit');
|
|
1188
1336
|
break;
|
|
1337
|
+
case 'grok':
|
|
1338
|
+
events.push('session_start', 'post_tool', 'user_prompt', 'pre_compact', 'post_compact', 'session_end');
|
|
1339
|
+
break;
|
|
1189
1340
|
}
|
|
1190
1341
|
|
|
1191
1342
|
// Install agent rules alongside hooks
|
|
@@ -1217,7 +1368,7 @@ async function installAgentRules(agent: AgentName, projectRoot: string, global =
|
|
|
1217
1368
|
try {
|
|
1218
1369
|
await fs.mkdir(path.dirname(rulesPath), { recursive: true });
|
|
1219
1370
|
|
|
1220
|
-
if (agent === 'claude' || agent === 'codex' || agent === 'opencode' || agent === 'antigravity' || agent === 'gemini-cli' || agent === 'dsh') {
|
|
1371
|
+
if (agent === 'claude' || agent === 'codex' || agent === 'opencode' || agent === 'antigravity' || agent === 'gemini-cli' || agent === 'dsh' || agent === 'grok') {
|
|
1221
1372
|
// For shared context files (CLAUDE.md / AGENTS.md / GEMINI.md), append rather than overwrite.
|
|
1222
1373
|
try {
|
|
1223
1374
|
const existing = await fs.readFile(rulesPath, 'utf-8');
|
|
@@ -1422,7 +1573,16 @@ export async function uninstallHooks(
|
|
|
1422
1573
|
let success = false;
|
|
1423
1574
|
|
|
1424
1575
|
try {
|
|
1425
|
-
if (agent === '
|
|
1576
|
+
if (agent === 'grok') {
|
|
1577
|
+
const config = await readGrokHookConfig(configPath);
|
|
1578
|
+
const cleaned = removeMemorixGrokHooks(config);
|
|
1579
|
+
if (Object.keys(cleaned).length === 0) {
|
|
1580
|
+
await fs.unlink(configPath);
|
|
1581
|
+
} else {
|
|
1582
|
+
await fs.writeFile(configPath, JSON.stringify(cleaned, null, 2), 'utf-8');
|
|
1583
|
+
}
|
|
1584
|
+
success = true;
|
|
1585
|
+
} else if (agent === 'kiro' || agent === 'opencode') {
|
|
1426
1586
|
await fs.unlink(configPath);
|
|
1427
1587
|
success = true;
|
|
1428
1588
|
} else {
|
|
@@ -1448,6 +1608,14 @@ export async function uninstallHooks(
|
|
|
1448
1608
|
const agentFiles = prevFiles.filter(e => e.agent === agent);
|
|
1449
1609
|
for (const entry of agentFiles) {
|
|
1450
1610
|
try {
|
|
1611
|
+
// The Grok hook file can contain user-owned groups. The dedicated
|
|
1612
|
+
// config pass above already removed Memorix entries safely; only
|
|
1613
|
+
// forget the audit record here, never unlink the shared file.
|
|
1614
|
+
if (agent === 'grok' && path.resolve(entry.path) === path.resolve(configPath)) {
|
|
1615
|
+
await removeFile(projectRoot, entry.path);
|
|
1616
|
+
auditCleaned = true;
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1451
1619
|
const basename = path.basename(entry.path);
|
|
1452
1620
|
// Shared context files: remove only the Memorix block, not the whole file
|
|
1453
1621
|
if (basename === 'AGENTS.md' || basename === 'GEMINI.md' || basename === 'CONTEXT.md') {
|
|
@@ -1530,7 +1698,7 @@ export async function getHookStatus(
|
|
|
1530
1698
|
projectRoot: string,
|
|
1531
1699
|
): Promise<Array<{ agent: AgentName; installed: boolean; outdated: boolean; verified: boolean; runtimeReady: boolean; configPath: string }>> {
|
|
1532
1700
|
const results: Array<{ agent: AgentName; installed: boolean; outdated: boolean; verified: boolean; runtimeReady: boolean; configPath: string }> = [];
|
|
1533
|
-
const agents: AgentName[] = ['claude', 'copilot', 'windsurf', 'cursor', 'kiro', 'codex', 'antigravity', 'gemini-cli', 'opencode', 'pi', 'trae'];
|
|
1701
|
+
const agents: AgentName[] = ['claude', 'copilot', 'windsurf', 'cursor', 'kiro', 'codex', 'antigravity', 'gemini-cli', 'opencode', 'pi', 'trae', 'grok'];
|
|
1534
1702
|
|
|
1535
1703
|
for (const agent of agents) {
|
|
1536
1704
|
const projectPath = getProjectConfigPath(agent, projectRoot);
|
|
@@ -1542,7 +1710,9 @@ export async function getHookStatus(
|
|
|
1542
1710
|
|
|
1543
1711
|
// Config-based agents: file existence = verified (agent reads config directly)
|
|
1544
1712
|
// Plugin/package agents (OpenCode, Pi): file existence alone does not prove runtime load.
|
|
1545
|
-
|
|
1713
|
+
// A Grok project hook can still be skipped until the host grants
|
|
1714
|
+
// `/hooks-trust`; only its always-trusted global file is verified by path.
|
|
1715
|
+
const verifiedByDefault = agent !== 'opencode' && agent !== 'pi' && agent !== 'grok';
|
|
1546
1716
|
|
|
1547
1717
|
try {
|
|
1548
1718
|
await fs.access(projectPath);
|
|
@@ -1576,11 +1746,16 @@ export async function getHookStatus(
|
|
|
1576
1746
|
runtimeReady = detectPwsh();
|
|
1577
1747
|
}
|
|
1578
1748
|
|
|
1749
|
+
const verified = installed && (
|
|
1750
|
+
verifiedByDefault ||
|
|
1751
|
+
(agent === 'grok' && usedPath === globalPath)
|
|
1752
|
+
);
|
|
1753
|
+
|
|
1579
1754
|
results.push({
|
|
1580
1755
|
agent,
|
|
1581
1756
|
installed,
|
|
1582
1757
|
outdated,
|
|
1583
|
-
verified
|
|
1758
|
+
verified,
|
|
1584
1759
|
runtimeReady,
|
|
1585
1760
|
configPath: usedPath,
|
|
1586
1761
|
});
|
package/src/hooks/normalizer.ts
CHANGED
|
@@ -20,6 +20,7 @@ const EVENT_MAP: Record<string, HookEvent> = {
|
|
|
20
20
|
post_command: 'post_command',
|
|
21
21
|
post_tool: 'post_tool',
|
|
22
22
|
pre_compact: 'pre_compact',
|
|
23
|
+
post_compact: 'post_compact',
|
|
23
24
|
session_end: 'session_end',
|
|
24
25
|
post_response: 'post_response',
|
|
25
26
|
|
|
@@ -32,6 +33,13 @@ const EVENT_MAP: Record<string, HookEvent> = {
|
|
|
32
33
|
Stop: 'session_end',
|
|
33
34
|
SessionEnd: 'session_end',
|
|
34
35
|
|
|
36
|
+
// Grok Build (Claude-compatible PascalCase plus snake_case hookEventName)
|
|
37
|
+
user_prompt_submit: 'user_prompt',
|
|
38
|
+
pre_tool_use: 'post_tool',
|
|
39
|
+
post_tool_use: 'post_tool',
|
|
40
|
+
post_tool_use_failure: 'post_tool',
|
|
41
|
+
stop_failure: 'session_end',
|
|
42
|
+
|
|
35
43
|
// GitHub Copilot (camelCase, different names from Cursor)
|
|
36
44
|
userPromptSubmitted: 'user_prompt',
|
|
37
45
|
preToolUse: 'post_tool',
|
|
@@ -197,8 +205,48 @@ function normalizeCompactionMetadata(
|
|
|
197
205
|
}
|
|
198
206
|
|
|
199
207
|
/**
|
|
200
|
-
*
|
|
208
|
+
* Normalize the official Grok Build hook payload.
|
|
201
209
|
*/
|
|
210
|
+
function normalizeGrok(payload: Record<string, unknown>, event: HookEvent): Partial<NormalizedHookInput> {
|
|
211
|
+
const result: Partial<NormalizedHookInput> = {
|
|
212
|
+
sessionId: firstString(payload.sessionId, payload.session_id, process.env.GROK_SESSION_ID) ?? '',
|
|
213
|
+
cwd: firstString(payload.cwd, payload.workspaceRoot, payload.workspace_root, process.env.GROK_WORKSPACE_ROOT) ?? '',
|
|
214
|
+
transcriptPath: firstString(payload.transcriptPath, payload.transcript_path),
|
|
215
|
+
};
|
|
216
|
+
const toolName = firstString(payload.toolName, payload.tool_name);
|
|
217
|
+
if (toolName) {
|
|
218
|
+
result.toolName = toolName;
|
|
219
|
+
const toolInput = asRecord(payload.toolInput) ?? asRecord(payload.tool_input);
|
|
220
|
+
result.toolInput = toolInput;
|
|
221
|
+
if (/^(bash|run_terminal_command)$/i.test(toolName)) {
|
|
222
|
+
result.command = firstString(toolInput?.command, toolInput?.cmd);
|
|
223
|
+
}
|
|
224
|
+
if (/^(write|edit|multi_edit|multiedittool|search_replace)$/i.test(toolName)) {
|
|
225
|
+
result.filePath = firstString(toolInput?.path, toolInput?.file_path, toolInput?.filePath);
|
|
226
|
+
}
|
|
227
|
+
result.toolResult = stringifyValue(
|
|
228
|
+
payload.toolResult ?? payload.tool_response ?? payload.tool_result ?? payload.error,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
if (event === 'user_prompt') {
|
|
232
|
+
result.userPrompt = firstString(payload.prompt, payload.userPrompt, payload.user_prompt) ?? '';
|
|
233
|
+
}
|
|
234
|
+
if (event === 'post_response' || event === 'session_end') {
|
|
235
|
+
result.aiResponse = firstString(
|
|
236
|
+
payload.lastAssistantMessage,
|
|
237
|
+
payload.last_assistant_message,
|
|
238
|
+
payload.assistantMessage,
|
|
239
|
+
payload.assistant_message,
|
|
240
|
+
payload.aiResponse,
|
|
241
|
+
payload.ai_response,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (event === 'session_start') {
|
|
245
|
+
result.sessionStartReason = firstString(payload.source, payload.sessionStartReason, payload.session_start_reason);
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
|
|
202
250
|
function detectAgent(payload: Record<string, unknown>): AgentName {
|
|
203
251
|
// Highest priority: explicit agent identity injected by memorix hook --agent flag
|
|
204
252
|
// This is set by generated hook configs when host payloads are ambiguous.
|
|
@@ -222,6 +270,12 @@ function detectAgent(payload: Record<string, unknown>): AgentName {
|
|
|
222
270
|
if (payload.agent === 'omp') return 'omp';
|
|
223
271
|
if (payload.agent === 'hermes') return 'hermes';
|
|
224
272
|
if (payload.agent === 'openclaw') return 'openclaw';
|
|
273
|
+
if (payload.agent === 'grok') return 'grok';
|
|
274
|
+
|
|
275
|
+
// Grok Build uses camelCase hookEventName + sessionId
|
|
276
|
+
if ('hookEventName' in payload && ('sessionId' in payload || 'GROK_SESSION_ID' in process.env)) {
|
|
277
|
+
return 'grok';
|
|
278
|
+
}
|
|
225
279
|
|
|
226
280
|
// Claude Code uses hook_event_name + session_id
|
|
227
281
|
if ('hook_event_name' in payload && 'session_id' in payload) return 'claude';
|
|
@@ -286,6 +340,8 @@ function extractEventName(payload: Record<string, unknown>, agent: AgentName): s
|
|
|
286
340
|
return (payload.hook_event_name as string) ?? '';
|
|
287
341
|
case 'kiro':
|
|
288
342
|
return (payload.event_type as string) ?? '';
|
|
343
|
+
case 'grok':
|
|
344
|
+
return (payload.hookEventName as string) ?? (payload.hook_event_name as string) ?? '';
|
|
289
345
|
default:
|
|
290
346
|
return '';
|
|
291
347
|
}
|
|
@@ -734,6 +790,9 @@ export function normalizeHookInput(payload: Record<string, unknown>): Normalized
|
|
|
734
790
|
case 'codebuddy':
|
|
735
791
|
agentSpecific = normalizeClaude(payload, event);
|
|
736
792
|
break;
|
|
793
|
+
case 'grok':
|
|
794
|
+
agentSpecific = normalizeGrok(payload, event);
|
|
795
|
+
break;
|
|
737
796
|
default:
|
|
738
797
|
agentSpecific = { sessionId: '', cwd: '' };
|
|
739
798
|
}
|
package/src/hooks/types.ts
CHANGED
|
@@ -44,7 +44,8 @@ export type AgentName =
|
|
|
44
44
|
| 'dsh'
|
|
45
45
|
| 'workbuddy'
|
|
46
46
|
| 'pi'
|
|
47
|
-
| 'omp'
|
|
47
|
+
| 'omp'
|
|
48
|
+
| 'grok';
|
|
48
49
|
|
|
49
50
|
/** Support tier for hook agents */
|
|
50
51
|
export type SupportTier = 'core' | 'extended' | 'community';
|
|
@@ -68,6 +69,7 @@ export const AGENT_SUPPORT_TIER: Record<AgentName, SupportTier> = {
|
|
|
68
69
|
pi: 'community',
|
|
69
70
|
omp: 'community',
|
|
70
71
|
antigravity: 'community',
|
|
72
|
+
grok: 'community',
|
|
71
73
|
};
|
|
72
74
|
|
|
73
75
|
/** Normalized hook input — agent-agnostic */
|
|
@@ -65,6 +65,15 @@ export const MEMORIX_INTEGRATIONS: MemorixIntegration[] = [
|
|
|
65
65
|
install: 'memorix setup --agent copilot',
|
|
66
66
|
surfaces: ['MCP', 'skills', 'hooks'],
|
|
67
67
|
},
|
|
68
|
+
{
|
|
69
|
+
agent: 'grok',
|
|
70
|
+
name: 'Grok Build',
|
|
71
|
+
entry: 'official-config',
|
|
72
|
+
status: 'ready',
|
|
73
|
+
install: 'memorix setup --agent grok --global',
|
|
74
|
+
surfaces: ['hooks', 'AGENTS.md'],
|
|
75
|
+
note: 'Writes ~/.grok/hooks/memorix.json and leaves Grok MCP host-owned; setup does not write config.toml.',
|
|
76
|
+
},
|
|
68
77
|
{
|
|
69
78
|
agent: 'cursor',
|
|
70
79
|
name: 'Cursor',
|