helloagents 3.1.7 → 3.1.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +24 -0
- package/.grok-plugin/plugin.json +24 -0
- package/README.md +37 -9
- package/README_CN.md +39 -11
- package/bootstrap-lite.md +4 -5
- package/bootstrap.md +4 -5
- package/cli.mjs +2 -0
- package/gemini-extension.json +1 -1
- package/hooks/hooks-cursor.json +41 -0
- package/hooks/hooks-grok.json +98 -0
- package/install.ps1 +3 -3
- package/install.sh +3 -3
- package/package.json +5 -2
- package/scripts/advisor-state.mjs +1 -1
- package/scripts/cli-config.mjs +1 -1
- package/scripts/cli-cursor.mjs +42 -0
- package/scripts/cli-doctor.mjs +173 -3
- package/scripts/cli-grok.mjs +54 -0
- package/scripts/cli-host-detect.mjs +67 -1
- package/scripts/cli-hosts.mjs +5 -0
- package/scripts/cli-lifecycle-hosts.mjs +197 -1
- package/scripts/cli-lifecycle.mjs +3 -1
- package/scripts/cli-messages.mjs +28 -14
- package/scripts/cli-runtime-root.mjs +284 -1
- package/scripts/cli-utils.mjs +36 -0
- package/scripts/cursor-hook.mjs +193 -0
- package/scripts/guard.mjs +6 -4
- package/scripts/notify-events.mjs +2 -0
- package/scripts/notify-source.mjs +2 -0
- package/scripts/notify.mjs +3 -2
- package/scripts/plan-contract.mjs +1 -1
- package/scripts/runtime-scope.mjs +1 -1
- package/skills/commands/help/SKILL.md +11 -11
|
@@ -1,11 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
copyFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs'
|
|
2
11
|
import { dirname, join, resolve } from 'node:path'
|
|
3
12
|
|
|
4
13
|
import { copyEntries, createLink, ensureDir, removeIfExists } from './cli-utils.mjs'
|
|
5
14
|
|
|
6
15
|
export const RUNTIME_ROOT_ENTRIES = [
|
|
7
16
|
'.claude-plugin',
|
|
17
|
+
'.cursor-plugin',
|
|
8
18
|
'.codex-plugin',
|
|
19
|
+
'.grok-plugin',
|
|
9
20
|
'assets',
|
|
10
21
|
'bootstrap-lite.md',
|
|
11
22
|
'bootstrap.md',
|
|
@@ -23,6 +34,17 @@ export const RUNTIME_ROOT_ENTRIES = [
|
|
|
23
34
|
'templates',
|
|
24
35
|
]
|
|
25
36
|
|
|
37
|
+
const CURSOR_PLUGIN_ENTRIES = [
|
|
38
|
+
'.cursor-plugin',
|
|
39
|
+
'hooks',
|
|
40
|
+
'LICENSE.md',
|
|
41
|
+
'README.md',
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
export const GROK_MARKETPLACE_NAME = 'helloagents-grok-marketplace'
|
|
45
|
+
export const GROK_PLUGIN_NAME = 'helloagents'
|
|
46
|
+
export const CURSOR_PLUGIN_NAME = 'helloagents'
|
|
47
|
+
|
|
26
48
|
/** Return the stable per-user runtime copy used by host integrations. */
|
|
27
49
|
export function getStableRuntimeRoot(home) {
|
|
28
50
|
return join(home, '.helloagents', 'helloagents')
|
|
@@ -33,11 +55,26 @@ export function getClaudeMarketplaceRoot(home) {
|
|
|
33
55
|
return join(home, '.helloagents', 'host-projections', 'claude-marketplace')
|
|
34
56
|
}
|
|
35
57
|
|
|
58
|
+
/** Return the Cursor local-plugin projection root derived from the shared runtime copy. */
|
|
59
|
+
export function getCursorPluginRoot(home) {
|
|
60
|
+
return join(home, '.helloagents', 'host-projections', 'cursor-local-plugin', CURSOR_PLUGIN_NAME)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Return the Cursor managed local plugin install root. */
|
|
64
|
+
export function getCursorInstallRoot(home) {
|
|
65
|
+
return join(home, '.cursor', 'plugins', 'local', CURSOR_PLUGIN_NAME)
|
|
66
|
+
}
|
|
67
|
+
|
|
36
68
|
/** Return the Gemini extension projection root derived from the shared runtime copy. */
|
|
37
69
|
export function getGeminiExtensionRoot(home) {
|
|
38
70
|
return join(home, '.helloagents', 'host-projections', 'gemini')
|
|
39
71
|
}
|
|
40
72
|
|
|
73
|
+
/** Return the Grok marketplace projection root derived from the shared runtime copy. */
|
|
74
|
+
export function getGrokMarketplaceRoot(home) {
|
|
75
|
+
return join(home, '.helloagents', 'host-projections', GROK_MARKETPLACE_NAME)
|
|
76
|
+
}
|
|
77
|
+
|
|
41
78
|
function normalizePath(path) {
|
|
42
79
|
const resolved = resolve(path)
|
|
43
80
|
try {
|
|
@@ -73,6 +110,165 @@ function retryTransientFs(operation) {
|
|
|
73
110
|
throw lastError
|
|
74
111
|
}
|
|
75
112
|
|
|
113
|
+
function safeJson(filePath, fallback = null) {
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'))
|
|
116
|
+
} catch {
|
|
117
|
+
return fallback
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function safeText(filePath) {
|
|
122
|
+
try {
|
|
123
|
+
return readFileSync(filePath, 'utf-8')
|
|
124
|
+
} catch {
|
|
125
|
+
return ''
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function normalizeSummaryText(text = '') {
|
|
130
|
+
return String(text || '')
|
|
131
|
+
.replace(/\r\n/g, '\n')
|
|
132
|
+
.split('\n')
|
|
133
|
+
.map((line) => line.trim())
|
|
134
|
+
.filter(Boolean)
|
|
135
|
+
.join(' ')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function extractSkillDescription(skillFile) {
|
|
139
|
+
const text = safeText(skillFile)
|
|
140
|
+
if (!text) return ''
|
|
141
|
+
|
|
142
|
+
const lines = text.replace(/\r\n/g, '\n').split('\n')
|
|
143
|
+
let inCodeFence = false
|
|
144
|
+
for (const rawLine of lines) {
|
|
145
|
+
const line = rawLine.trim()
|
|
146
|
+
if (line.startsWith('```')) {
|
|
147
|
+
inCodeFence = !inCodeFence
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
if (inCodeFence || !line || line.startsWith('#')) continue
|
|
151
|
+
return normalizeSummaryText(line).slice(0, 280)
|
|
152
|
+
}
|
|
153
|
+
return ''
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function listSkillComponents(sourceRoot) {
|
|
157
|
+
const skillsRoot = join(sourceRoot, 'skills')
|
|
158
|
+
if (!existsSync(skillsRoot)) return []
|
|
159
|
+
|
|
160
|
+
return readdirSync(skillsRoot, { withFileTypes: true })
|
|
161
|
+
.filter((entry) => entry.isDirectory())
|
|
162
|
+
.map((entry) => {
|
|
163
|
+
const skillFile = join(skillsRoot, entry.name, 'SKILL.md')
|
|
164
|
+
if (!existsSync(skillFile)) return null
|
|
165
|
+
const description = extractSkillDescription(skillFile)
|
|
166
|
+
return description
|
|
167
|
+
? { name: entry.name, description }
|
|
168
|
+
: { name: entry.name }
|
|
169
|
+
})
|
|
170
|
+
.filter(Boolean)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function listGrokHookComponents(sourceRoot) {
|
|
174
|
+
const hooksData = safeJson(join(sourceRoot, 'hooks', 'hooks-grok.json'), {})
|
|
175
|
+
return Object.keys(hooksData.hooks || {}).map((name) => (
|
|
176
|
+
name === 'SessionStart'
|
|
177
|
+
? { name, description: 'startup|resume|clear|compact' }
|
|
178
|
+
: { name }
|
|
179
|
+
))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function readPackageMetadata(sourceRoot) {
|
|
183
|
+
const pkg = safeJson(join(sourceRoot, 'package.json'), {}) || {}
|
|
184
|
+
const grokManifest = safeJson(join(sourceRoot, '.grok-plugin', 'plugin.json'), {}) || {}
|
|
185
|
+
const author = (
|
|
186
|
+
typeof grokManifest.author === 'object' && grokManifest.author
|
|
187
|
+
? grokManifest.author
|
|
188
|
+
: (typeof pkg.author === 'object' && pkg.author ? pkg.author : {})
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
name: grokManifest.name || GROK_PLUGIN_NAME,
|
|
193
|
+
version: grokManifest.version || pkg.version || '0.0.0',
|
|
194
|
+
description: grokManifest.description || pkg.description || '',
|
|
195
|
+
homepage: grokManifest.homepage || pkg.homepage || '',
|
|
196
|
+
repository: typeof grokManifest.repository === 'string'
|
|
197
|
+
? grokManifest.repository
|
|
198
|
+
: (typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url || ''),
|
|
199
|
+
authorName: author.name || 'HelloWind',
|
|
200
|
+
authorEmail: author.email || '',
|
|
201
|
+
keywords: Array.isArray(grokManifest.keywords)
|
|
202
|
+
? grokManifest.keywords
|
|
203
|
+
: (Array.isArray(pkg.keywords) ? pkg.keywords : []),
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function buildGrokMarketplaceManifest(metadata) {
|
|
208
|
+
return {
|
|
209
|
+
name: GROK_MARKETPLACE_NAME,
|
|
210
|
+
description: 'Local development marketplace for the HelloAGENTS Grok Build plugin.',
|
|
211
|
+
owner: {
|
|
212
|
+
name: metadata.authorName,
|
|
213
|
+
...(metadata.authorEmail ? { email: metadata.authorEmail } : {}),
|
|
214
|
+
},
|
|
215
|
+
plugins: [
|
|
216
|
+
{
|
|
217
|
+
name: metadata.name || GROK_PLUGIN_NAME,
|
|
218
|
+
description: metadata.description,
|
|
219
|
+
category: 'development',
|
|
220
|
+
version: metadata.version,
|
|
221
|
+
source: {
|
|
222
|
+
type: 'local',
|
|
223
|
+
path: `./plugins/${GROK_PLUGIN_NAME}`,
|
|
224
|
+
},
|
|
225
|
+
...(metadata.homepage ? { homepage: metadata.homepage } : {}),
|
|
226
|
+
...(metadata.keywords.length ? { keywords: metadata.keywords } : {}),
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function buildClaudeCompatibleMarketplaceManifest(metadata) {
|
|
233
|
+
return {
|
|
234
|
+
name: GROK_MARKETPLACE_NAME,
|
|
235
|
+
description: 'Local development marketplace for the HelloAGENTS Grok Build plugin.',
|
|
236
|
+
owner: {
|
|
237
|
+
name: metadata.authorName,
|
|
238
|
+
...(metadata.authorEmail ? { email: metadata.authorEmail } : {}),
|
|
239
|
+
},
|
|
240
|
+
plugins: [
|
|
241
|
+
{
|
|
242
|
+
name: metadata.name || GROK_PLUGIN_NAME,
|
|
243
|
+
description: metadata.description,
|
|
244
|
+
source: `./plugins/${GROK_PLUGIN_NAME}`,
|
|
245
|
+
version: metadata.version,
|
|
246
|
+
category: 'development',
|
|
247
|
+
strict: false,
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function buildGrokPluginIndex(sourceRoot) {
|
|
254
|
+
return {
|
|
255
|
+
version: 1,
|
|
256
|
+
plugins: {
|
|
257
|
+
[GROK_PLUGIN_NAME]: {
|
|
258
|
+
components: {
|
|
259
|
+
hooks: listGrokHookComponents(sourceRoot),
|
|
260
|
+
skills: listSkillComponents(sourceRoot),
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function writeJsonFile(filePath, value) {
|
|
268
|
+
ensureDir(dirname(filePath))
|
|
269
|
+
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf-8')
|
|
270
|
+
}
|
|
271
|
+
|
|
76
272
|
function syncRuntimeTree(sourceRoot, targetRoot, { materializeGeminiHooks = false } = {}) {
|
|
77
273
|
const source = resolve(sourceRoot)
|
|
78
274
|
const target = resolve(targetRoot)
|
|
@@ -129,6 +325,78 @@ export function syncGeminiExtensionRoot(sourceRoot, extensionRoot) {
|
|
|
129
325
|
return syncRuntimeTree(sourceRoot, extensionRoot, { materializeGeminiHooks: true })
|
|
130
326
|
}
|
|
131
327
|
|
|
328
|
+
function syncCursorPluginTree(sourceRoot, targetRoot) {
|
|
329
|
+
const source = resolve(sourceRoot)
|
|
330
|
+
const target = resolve(targetRoot)
|
|
331
|
+
if (samePath(source, target)) {
|
|
332
|
+
return { synced: false, root: target }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const parent = dirname(target)
|
|
336
|
+
ensureDir(parent)
|
|
337
|
+
const staging = mkdtempSync(join(parent, '.helloagents-cursor-plugin-'))
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
copyEntries(source, staging, CURSOR_PLUGIN_ENTRIES)
|
|
341
|
+
retryTransientFs(() => {
|
|
342
|
+
removeIfExists(target)
|
|
343
|
+
renameSync(staging, target)
|
|
344
|
+
})
|
|
345
|
+
return { synced: true, root: target }
|
|
346
|
+
} catch (error) {
|
|
347
|
+
removeIfExists(staging)
|
|
348
|
+
throw error
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Sync a materialized Cursor local-plugin projection derived from the shared runtime copy. */
|
|
353
|
+
export function syncCursorPluginRoot(sourceRoot, cursorPluginRoot) {
|
|
354
|
+
return syncCursorPluginTree(sourceRoot, cursorPluginRoot)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Sync the Cursor local-plugin install directory as a real plugin copy. */
|
|
358
|
+
export function syncCursorInstallRoot(sourceRoot, installRoot) {
|
|
359
|
+
return syncCursorPluginTree(sourceRoot, installRoot)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Sync a materialized Grok marketplace projection derived from the shared runtime copy. */
|
|
363
|
+
export function syncGrokMarketplaceRoot(sourceRoot, marketplaceRoot) {
|
|
364
|
+
const source = resolve(sourceRoot)
|
|
365
|
+
const target = resolve(marketplaceRoot)
|
|
366
|
+
if (samePath(source, target)) {
|
|
367
|
+
return { synced: false, root: target }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const parent = dirname(target)
|
|
371
|
+
ensureDir(parent)
|
|
372
|
+
const staging = mkdtempSync(join(parent, '.helloagents-grok-marketplace-'))
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
const pluginRoot = join(staging, 'plugins', GROK_PLUGIN_NAME)
|
|
376
|
+
copyEntries(source, pluginRoot, RUNTIME_ROOT_ENTRIES)
|
|
377
|
+
|
|
378
|
+
const sourceHooks = join(pluginRoot, 'hooks', 'hooks-grok.json')
|
|
379
|
+
const targetHooks = join(pluginRoot, 'hooks', 'hooks.json')
|
|
380
|
+
if (existsSync(sourceHooks)) {
|
|
381
|
+
copyFileSync(sourceHooks, targetHooks)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const metadata = readPackageMetadata(source)
|
|
385
|
+
writeJsonFile(join(staging, '.grok-plugin', 'marketplace.json'), buildGrokMarketplaceManifest(metadata))
|
|
386
|
+
writeJsonFile(join(staging, '.grok-plugin', 'plugin-index.json'), buildGrokPluginIndex(source))
|
|
387
|
+
writeJsonFile(join(staging, '.claude-plugin', 'marketplace.json'), buildClaudeCompatibleMarketplaceManifest(metadata))
|
|
388
|
+
|
|
389
|
+
retryTransientFs(() => {
|
|
390
|
+
removeIfExists(target)
|
|
391
|
+
renameSync(staging, target)
|
|
392
|
+
})
|
|
393
|
+
return { synced: true, root: target }
|
|
394
|
+
} catch (error) {
|
|
395
|
+
removeIfExists(staging)
|
|
396
|
+
throw error
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
132
400
|
/** Remove the stable runtime copy while leaving user settings under ~/.helloagents intact. */
|
|
133
401
|
export function removeRuntimeRoot(runtimeRoot) {
|
|
134
402
|
removeIfExists(runtimeRoot)
|
|
@@ -139,7 +407,22 @@ export function removeClaudeMarketplaceRoot(home) {
|
|
|
139
407
|
removeIfExists(getClaudeMarketplaceRoot(home))
|
|
140
408
|
}
|
|
141
409
|
|
|
410
|
+
/** Remove the Cursor local-plugin projection root. */
|
|
411
|
+
export function removeCursorPluginRoot(home) {
|
|
412
|
+
removeIfExists(getCursorPluginRoot(home))
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Remove the Cursor managed local plugin install root. */
|
|
416
|
+
export function removeCursorInstallRoot(home) {
|
|
417
|
+
removeIfExists(getCursorInstallRoot(home))
|
|
418
|
+
}
|
|
419
|
+
|
|
142
420
|
/** Remove the Gemini extension projection root. */
|
|
143
421
|
export function removeGeminiExtensionRoot(home) {
|
|
144
422
|
removeIfExists(getGeminiExtensionRoot(home))
|
|
145
423
|
}
|
|
424
|
+
|
|
425
|
+
/** Remove the Grok marketplace projection root. */
|
|
426
|
+
export function removeGrokMarketplaceRoot(home) {
|
|
427
|
+
removeIfExists(getGrokMarketplaceRoot(home))
|
|
428
|
+
}
|
package/scripts/cli-utils.mjs
CHANGED
|
@@ -140,6 +140,42 @@ export function cleanSettingsHooks(settingsPath, cleanPermissions = false) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/** Merge helloagents hooks into a standalone hooks.json file, preserving non-managed entries. */
|
|
144
|
+
export function mergeHooksConfig(hooksPath, hooksData) {
|
|
145
|
+
const config = safeJson(hooksPath) || {};
|
|
146
|
+
if (!config.version) config.version = 1;
|
|
147
|
+
if (!config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) {
|
|
148
|
+
config.hooks = {};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const [event, entries] of Object.entries(hooksData?.hooks || {})) {
|
|
152
|
+
if (!Array.isArray(config.hooks[event])) config.hooks[event] = [];
|
|
153
|
+
config.hooks[event] = config.hooks[event].filter((entry) => !JSON.stringify(entry).includes('helloagents'));
|
|
154
|
+
config.hooks[event].push(...entries);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
safeWrite(hooksPath, JSON.stringify(config, null, 2) + '\n');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Remove helloagents hooks from a standalone hooks.json file. */
|
|
161
|
+
export function cleanHooksConfig(hooksPath) {
|
|
162
|
+
const config = safeJson(hooksPath);
|
|
163
|
+
if (!config || !config.hooks || typeof config.hooks !== 'object') return;
|
|
164
|
+
|
|
165
|
+
for (const [event, entries] of Object.entries(config.hooks)) {
|
|
166
|
+
if (!Array.isArray(entries)) continue;
|
|
167
|
+
config.hooks[event] = entries.filter((entry) => !JSON.stringify(entry).includes('helloagents'));
|
|
168
|
+
if (!config.hooks[event].length) delete config.hooks[event];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!Object.keys(config.hooks).length) delete config.hooks;
|
|
172
|
+
if (Object.keys(config).length) {
|
|
173
|
+
safeWrite(hooksPath, JSON.stringify(config, null, 2) + '\n');
|
|
174
|
+
} else {
|
|
175
|
+
removeIfExists(hooksPath);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
143
179
|
function rewriteHookCommandToCli(command = '', pathVar = '') {
|
|
144
180
|
const replacements = new Map([
|
|
145
181
|
[`node "${pathVar}/scripts/notify.mjs"`, 'helloagents-js notify'],
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import { readFileSync } from 'node:fs'
|
|
5
|
+
import { dirname, join } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
import { normalizeNotifyPayload } from './notify-payload.mjs'
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
11
|
+
const EVENT = String(process.argv[2] || '').trim().toLowerCase()
|
|
12
|
+
|
|
13
|
+
const EVENT_CONFIG = {
|
|
14
|
+
'session-start': {
|
|
15
|
+
script: 'notify.mjs',
|
|
16
|
+
args: ['inject', '--cursor'],
|
|
17
|
+
hookEventName: 'sessionStart',
|
|
18
|
+
},
|
|
19
|
+
'pre-tool-use': {
|
|
20
|
+
script: 'guard.mjs',
|
|
21
|
+
args: ['--cursor'],
|
|
22
|
+
hookEventName: 'preToolUse',
|
|
23
|
+
},
|
|
24
|
+
'post-tool-use': {
|
|
25
|
+
script: 'guard.mjs',
|
|
26
|
+
args: ['post-write', '--cursor'],
|
|
27
|
+
hookEventName: 'postToolUse',
|
|
28
|
+
},
|
|
29
|
+
'pre-compact': {
|
|
30
|
+
script: 'notify.mjs',
|
|
31
|
+
args: ['pre-compact', '--cursor'],
|
|
32
|
+
hookEventName: 'preCompact',
|
|
33
|
+
},
|
|
34
|
+
'subagent-stop': {
|
|
35
|
+
script: 'ralph-loop.mjs',
|
|
36
|
+
args: ['subagent', '--cursor'],
|
|
37
|
+
hookEventName: 'subagentStop',
|
|
38
|
+
},
|
|
39
|
+
stop: {
|
|
40
|
+
script: 'notify.mjs',
|
|
41
|
+
args: ['stop', '--cursor'],
|
|
42
|
+
hookEventName: 'stop',
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readStdinJson() {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(readFileSync(0, 'utf-8'))
|
|
49
|
+
} catch {
|
|
50
|
+
return {}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveWorkspaceRoot(payload = {}) {
|
|
55
|
+
if (typeof payload.cwd === 'string' && payload.cwd.trim()) return payload.cwd.trim()
|
|
56
|
+
if (Array.isArray(payload.workspace_roots)) {
|
|
57
|
+
const first = payload.workspace_roots.find((entry) => typeof entry === 'string' && entry.trim())
|
|
58
|
+
if (first) return first.trim()
|
|
59
|
+
}
|
|
60
|
+
const envRoot = String(process.env.CURSOR_PROJECT_DIR || process.env.CLAUDE_PROJECT_DIR || '').trim()
|
|
61
|
+
return envRoot || process.cwd()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeCursorPayload(payload = {}, hookEventName = '') {
|
|
65
|
+
const normalized = normalizeNotifyPayload(payload)
|
|
66
|
+
normalized.cwd = resolveWorkspaceRoot(payload)
|
|
67
|
+
if (!normalized.hook_event_name && hookEventName) normalized.hook_event_name = hookEventName
|
|
68
|
+
if (!normalized.hookEventName && hookEventName) normalized.hookEventName = hookEventName
|
|
69
|
+
|
|
70
|
+
if (!normalized.tool_name) {
|
|
71
|
+
if (typeof payload.command === 'string' && payload.command.trim()) {
|
|
72
|
+
normalized.tool_name = 'Shell'
|
|
73
|
+
normalized.tool_input = {
|
|
74
|
+
...(normalized.tool_input && typeof normalized.tool_input === 'object' ? normalized.tool_input : {}),
|
|
75
|
+
command: payload.command,
|
|
76
|
+
}
|
|
77
|
+
} else if (typeof payload.file_path === 'string' && payload.file_path.trim()) {
|
|
78
|
+
normalized.tool_name = hookEventName === 'postToolUse' ? 'Write' : 'Read'
|
|
79
|
+
normalized.tool_input = {
|
|
80
|
+
...(normalized.tool_input && typeof normalized.tool_input === 'object' ? normalized.tool_input : {}),
|
|
81
|
+
file_path: payload.file_path,
|
|
82
|
+
content: payload.content,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (
|
|
88
|
+
normalized.tool_output === undefined
|
|
89
|
+
&& typeof payload.tool_output === 'string'
|
|
90
|
+
) {
|
|
91
|
+
normalized.tool_output = payload.tool_output
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return normalized
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function runInternalScript(scriptName, args, payload, hookEventName) {
|
|
98
|
+
const result = spawnSync(process.execPath, [join(__dirname, scriptName), ...args], {
|
|
99
|
+
input: JSON.stringify(normalizeCursorPayload(payload, hookEventName)),
|
|
100
|
+
encoding: 'utf-8',
|
|
101
|
+
env: {
|
|
102
|
+
...process.env,
|
|
103
|
+
HELLOAGENTS_HOOK_EVENT: hookEventName,
|
|
104
|
+
},
|
|
105
|
+
windowsHide: true,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
if (result.error) {
|
|
109
|
+
throw result.error
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (result.status && result.status !== 0) {
|
|
113
|
+
throw new Error((result.stderr || result.stdout || '').trim() || `${scriptName} exited with code ${result.status}`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const stdout = String(result.stdout || '').trim()
|
|
117
|
+
if (!stdout) return {}
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(stdout)
|
|
120
|
+
} catch {
|
|
121
|
+
throw new Error(`Invalid JSON from ${scriptName}: ${stdout}`)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildErrorResponse(event, error) {
|
|
126
|
+
const reason = `[HelloAGENTS Cursor Hook] ${error?.message || error}`
|
|
127
|
+
if (event === 'pre-tool-use') {
|
|
128
|
+
return {
|
|
129
|
+
permission: 'deny',
|
|
130
|
+
user_message: reason,
|
|
131
|
+
agent_message: reason,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (event === 'session-start') {
|
|
135
|
+
return {
|
|
136
|
+
additional_context: reason,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return {}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function translatePermissionResponse(result = {}) {
|
|
143
|
+
const denialReason = result?.hookSpecificOutput?.permissionDecisionReason || result?.reason || ''
|
|
144
|
+
if (result?.hookSpecificOutput?.permissionDecision === 'deny' || result?.decision === 'block') {
|
|
145
|
+
return {
|
|
146
|
+
permission: 'deny',
|
|
147
|
+
user_message: denialReason || '[HelloAGENTS] Operation blocked.',
|
|
148
|
+
agent_message: denialReason || '[HelloAGENTS] Operation blocked.',
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return {}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function translateCursorResponse(event, result = {}) {
|
|
155
|
+
if (event === 'session-start') {
|
|
156
|
+
const additionalContext = result?.hookSpecificOutput?.additionalContext
|
|
157
|
+
return additionalContext ? { additional_context: additionalContext } : {}
|
|
158
|
+
}
|
|
159
|
+
if (event === 'pre-tool-use') {
|
|
160
|
+
return translatePermissionResponse(result)
|
|
161
|
+
}
|
|
162
|
+
if (event === 'post-tool-use') {
|
|
163
|
+
const additionalContext = result?.hookSpecificOutput?.additionalContext
|
|
164
|
+
return additionalContext ? { additional_context: additionalContext } : {}
|
|
165
|
+
}
|
|
166
|
+
if (event === 'stop' || event === 'subagent-stop') {
|
|
167
|
+
if (result?.decision === 'block' && result?.reason) {
|
|
168
|
+
return {
|
|
169
|
+
followup_message: result.reason,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return {}
|
|
173
|
+
}
|
|
174
|
+
return {}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function main() {
|
|
178
|
+
const config = EVENT_CONFIG[EVENT]
|
|
179
|
+
if (!config) {
|
|
180
|
+
process.stderr.write(`cursor-hook.mjs: unknown event "${EVENT}"\n`)
|
|
181
|
+
process.exit(1)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const payload = readStdinJson()
|
|
185
|
+
try {
|
|
186
|
+
const result = runInternalScript(config.script, config.args, payload, config.hookEventName)
|
|
187
|
+
process.stdout.write(JSON.stringify(translateCursorResponse(EVENT, result)))
|
|
188
|
+
} catch (error) {
|
|
189
|
+
process.stdout.write(JSON.stringify(buildErrorResponse(EVENT, error)))
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
main()
|
package/scripts/guard.mjs
CHANGED
|
@@ -21,12 +21,14 @@ import {
|
|
|
21
21
|
|
|
22
22
|
const CONFIG_FILE = join(homedir(), '.helloagents', 'helloagents.json')
|
|
23
23
|
const IS_GEMINI = process.argv.includes('--gemini')
|
|
24
|
-
const
|
|
24
|
+
const IS_GROK = process.argv.includes('--grok')
|
|
25
|
+
const IS_CURSOR = process.argv.includes('--cursor')
|
|
26
|
+
const HOST = IS_GEMINI ? 'gemini' : (IS_GROK ? 'grok' : (IS_CURSOR ? 'cursor' : 'claude'))
|
|
25
27
|
const HOOK_EVENT = process.env.HELLOAGENTS_HOOK_EVENT
|
|
26
28
|
|| (
|
|
27
29
|
process.argv.includes('post-write')
|
|
28
|
-
? (IS_GEMINI ? 'AfterModel' : 'PostToolUse')
|
|
29
|
-
: (IS_GEMINI ? 'BeforeTool' : 'PreToolUse')
|
|
30
|
+
? (IS_GEMINI ? 'AfterModel' : (IS_CURSOR ? 'postToolUse' : 'PostToolUse'))
|
|
31
|
+
: (IS_GEMINI ? 'BeforeTool' : (IS_CURSOR ? 'preToolUse' : 'PreToolUse'))
|
|
30
32
|
)
|
|
31
33
|
|
|
32
34
|
function readSettings() {
|
|
@@ -176,4 +178,4 @@ main().catch((error) => {
|
|
|
176
178
|
})
|
|
177
179
|
process.stderr.write(`${reason}\n`)
|
|
178
180
|
process.exitCode = 1
|
|
179
|
-
})
|
|
181
|
+
})
|
|
@@ -8,6 +8,8 @@ export function resolveNotifyHost(argv = []) {
|
|
|
8
8
|
const args = Array.from(argv, (value) => String(value || ''));
|
|
9
9
|
const command = args[2] || args[0] || '';
|
|
10
10
|
if (args.includes('--gemini')) return 'gemini';
|
|
11
|
+
if (args.includes('--grok')) return 'grok';
|
|
12
|
+
if (args.includes('--cursor')) return 'cursor';
|
|
11
13
|
if (args.includes('--codex') || command === 'codex-notify') return 'codex';
|
|
12
14
|
return 'claude';
|
|
13
15
|
}
|
package/scripts/notify.mjs
CHANGED
|
@@ -34,12 +34,13 @@ const CONFIG_FILE = join(homedir(), '.helloagents', 'helloagents.json');
|
|
|
34
34
|
const cmd = process.argv[2] || '';
|
|
35
35
|
const HOST = resolveNotifyHost(process.argv);
|
|
36
36
|
const IS_GEMINI = HOST === 'gemini';
|
|
37
|
+
const IS_CURSOR = HOST === 'cursor';
|
|
37
38
|
const IS_CODEX = HOST === 'codex';
|
|
38
39
|
const IS_SILENT = process.argv.includes('--silent');
|
|
39
40
|
const EVENT_NAME = {
|
|
40
41
|
SessionStart: 'SessionStart',
|
|
41
|
-
UserPromptSubmit: IS_GEMINI ? 'BeforeAgent' : 'UserPromptSubmit',
|
|
42
|
-
PreCompact: IS_GEMINI ? 'BeforeAgent' : 'PreCompact',
|
|
42
|
+
UserPromptSubmit: IS_GEMINI ? 'BeforeAgent' : (IS_CURSOR ? 'beforeSubmitPrompt' : 'UserPromptSubmit'),
|
|
43
|
+
PreCompact: IS_GEMINI ? 'BeforeAgent' : (IS_CURSOR ? 'preCompact' : 'PreCompact'),
|
|
43
44
|
};
|
|
44
45
|
const RALPH_LOOP_ROUTE_COMMANDS = new Set(['qa', 'loop']);
|
|
45
46
|
const CODEX_HOOKS_FILE = join(homedir(), '.codex', 'hooks.json');
|
|
@@ -5,7 +5,7 @@ import { resolveProjectPlanDir } from './project-storage.mjs'
|
|
|
5
5
|
|
|
6
6
|
export const PLAN_CONTRACT_FILE_NAME = 'contract.json'
|
|
7
7
|
const VALID_QA_MODES = new Set(['standard', 'deep'])
|
|
8
|
-
const VALID_ADVISOR_SOURCES = new Set(['claude', 'codex', 'gemini'])
|
|
8
|
+
const VALID_ADVISOR_SOURCES = new Set(['claude', 'codex', 'cursor', 'gemini', 'grok'])
|
|
9
9
|
|
|
10
10
|
function normalizeStringArray(values) {
|
|
11
11
|
if (!Array.isArray(values)) return []
|
|
@@ -174,7 +174,7 @@ export function getProjectRoot(cwd) {
|
|
|
174
174
|
|
|
175
175
|
function getCarrierPathForRoot(root, host = '') {
|
|
176
176
|
if (!root) return ''
|
|
177
|
-
if (host === 'codex') return join(root, 'AGENTS.md')
|
|
177
|
+
if (host === 'codex' || host === 'grok' || host === 'cursor') return join(root, 'AGENTS.md')
|
|
178
178
|
if (host === 'gemini') return join(root, '.gemini', 'GEMINI.md')
|
|
179
179
|
return join(root, 'CLAUDE.md')
|
|
180
180
|
}
|
|
@@ -45,14 +45,14 @@ Trigger: ~help
|
|
|
45
45
|
如果当前 CLI 存在工作区限制导致家目录不可读,则明确说明“无法直接读取配置文件,以下按已注入设置或默认值展示”,不要改用无关工具或伪造已读取结果。
|
|
46
46
|
| 配置项 | 默认值 | 作用 | 适用 CLI |
|
|
47
47
|
|--------|-------|------|---------|
|
|
48
|
-
| output_language | "" | 空=跟随用户语言/填写则指定(如 zh-CN、en) | Claude Code + Gemini CLI + Codex CLI |
|
|
49
|
-
| output_format | true | true=主代理最终回复必须使用 HelloAGENTS 格式,流式/中间输出及子代理输出保持自然;false=自然输出 | Claude Code + Gemini CLI + Codex CLI |
|
|
50
|
-
| notify_level | 0 | 0=关闭/1=桌面通知/2=声音/3=两者 | Claude Code + Gemini CLI + Codex CLI |
|
|
51
|
-
| ralph_loop_enabled | true | 收尾 QA 门禁(显式 ~qa / ~loop 或收尾要求时触发审查、lint/test/build) | Claude Code + Gemini CLI + Codex CLI |
|
|
52
|
-
| guard_enabled | true | 阻断危险命令与写入后的安全扫描 | Claude Code + Gemini CLI + Codex CLI |
|
|
53
|
-
| kb_create_mode | 1 | 0=关闭/1=知识库已存在时自动同步/2=编码任务在知识库已存在或当前项目已初始化时自动创建或同步 | Claude Code + Gemini CLI + Codex CLI |
|
|
54
|
-
| project_store_mode | "local" | "local"=知识库/方案包保留在项目本地 `.helloagents/`;"repo-shared"=本地 `.helloagents/` 仅保留项目本地状态/运行态,知识库与方案包改写到 `~/.helloagents/projects/<repo-key>/` | Claude Code + Gemini CLI + Codex CLI |
|
|
55
|
-
| auto_commit_enabled | true | true=验证完成且有变更时自动执行本地提交;false=跳过自动提交,仍可手动用 `~commit` | Claude Code + Gemini CLI + Codex CLI |
|
|
56
|
-
| commit_attribution | "" | 空=不添加/填写内容则添加到 commit message | Claude Code + Gemini CLI + Codex CLI |
|
|
57
|
-
| install_mode | "standby" | 当前默认安装模式 | Claude Code + Gemini CLI + Codex CLI |
|
|
58
|
-
| host_install_modes | {} | 单 CLI 模式记录,优先于 install_mode | Claude Code + Gemini CLI + Codex CLI |
|
|
48
|
+
| output_language | "" | 空=跟随用户语言/填写则指定(如 zh-CN、en) | Claude Code + Gemini CLI + Grok Build + Codex CLI |
|
|
49
|
+
| output_format | true | true=主代理最终回复必须使用 HelloAGENTS 格式,流式/中间输出及子代理输出保持自然;false=自然输出 | Claude Code + Gemini CLI + Grok Build + Codex CLI |
|
|
50
|
+
| notify_level | 0 | 0=关闭/1=桌面通知/2=声音/3=两者 | Claude Code + Gemini CLI + Grok Build + Codex CLI |
|
|
51
|
+
| ralph_loop_enabled | true | 收尾 QA 门禁(显式 ~qa / ~loop 或收尾要求时触发审查、lint/test/build) | Claude Code + Gemini CLI + Grok Build + Codex CLI |
|
|
52
|
+
| guard_enabled | true | 阻断危险命令与写入后的安全扫描 | Claude Code + Gemini CLI + Grok Build + Codex CLI |
|
|
53
|
+
| kb_create_mode | 1 | 0=关闭/1=知识库已存在时自动同步/2=编码任务在知识库已存在或当前项目已初始化时自动创建或同步 | Claude Code + Gemini CLI + Grok Build + Codex CLI |
|
|
54
|
+
| project_store_mode | "local" | "local"=知识库/方案包保留在项目本地 `.helloagents/`;"repo-shared"=本地 `.helloagents/` 仅保留项目本地状态/运行态,知识库与方案包改写到 `~/.helloagents/projects/<repo-key>/` | Claude Code + Gemini CLI + Grok Build + Codex CLI |
|
|
55
|
+
| auto_commit_enabled | true | true=验证完成且有变更时自动执行本地提交;false=跳过自动提交,仍可手动用 `~commit` | Claude Code + Gemini CLI + Grok Build + Codex CLI |
|
|
56
|
+
| commit_attribution | "" | 空=不添加/填写内容则添加到 commit message | Claude Code + Gemini CLI + Grok Build + Codex CLI |
|
|
57
|
+
| install_mode | "standby" | 当前默认安装模式 | Claude Code + Gemini CLI + Grok Build + Codex CLI |
|
|
58
|
+
| host_install_modes | {} | 单 CLI 模式记录,优先于 install_mode | Claude Code + Gemini CLI + Grok Build + Codex CLI |
|