helloagents 3.1.5 → 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 +42 -15
- package/README_CN.md +45 -18
- package/bootstrap-lite.md +23 -22
- package/bootstrap.md +25 -26
- 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-context.mjs +2 -1
- 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-context.mjs +2 -2
- package/scripts/runtime-scope.mjs +1 -1
- package/scripts/workflow-plan-files.mjs +1 -1
- package/scripts/workflow-recommendation.mjs +1 -1
- package/skills/commands/ask/SKILL.md +100 -0
- package/skills/commands/auto/SKILL.md +6 -9
- package/skills/commands/build/SKILL.md +5 -2
- package/skills/commands/help/SKILL.md +13 -13
- package/skills/commands/plan/SKILL.md +7 -10
- package/skills/commands/prd/SKILL.md +14 -14
- package/skills/helloagents/SKILL.md +3 -3
- package/skills/qa-review/SKILL.md +9 -0
- package/templates/plans/tasks.md +3 -3
- package/skills/commands/idea/SKILL.md +0 -56
- package/skills/commands/office/SKILL.md +0 -86
|
@@ -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
|
+
})
|
|
@@ -12,6 +12,7 @@ const COMMAND_ALIASES = {
|
|
|
12
12
|
do: 'build',
|
|
13
13
|
design: 'plan',
|
|
14
14
|
review: 'qa',
|
|
15
|
+
idea: 'ask',
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
function buildRuntimeRootBlock(pkgRoot) {
|
|
@@ -165,7 +166,7 @@ export function buildSemanticRouteInstruction(cwd, payload = {}) {
|
|
|
165
166
|
'请根据用户请求的真实意图选路,不依赖关键词表。',
|
|
166
167
|
buildDelegatedTaskHint(),
|
|
167
168
|
'任务分层:T0=探索/比较;T1=低风险小改动或显式验证;T2=多文件功能/新项目/需要结构化产物;T3=高风险或不可逆操作。',
|
|
168
|
-
'路由映射:~
|
|
169
|
+
'路由映射:~ask=一问一答交互澄清,不创建文件;~build=明确实现;~qa=统一质量审查/验证/修复/收尾;~plan=结构化规划;~prd=重型规格;~auto=自动选择并继续执行后续阶段。',
|
|
169
170
|
'若判定为 T3,默认先走 ~plan / ~prd;纯质量审查、验真或收尾请求才优先 ~qa。',
|
|
170
171
|
`涉及 UI 任务时,设计决策优先级:当前活跃 plan / PRD → ${describeProjectStoreFile(cwd, 'DESIGN.md')} → 已读取的 hello-ui 规则;同时所有 UI 任务都必须满足 UI 质量基线。`,
|
|
171
172
|
projectStorageHint,
|
|
@@ -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 []
|
|
@@ -78,13 +78,13 @@ export function clearRouteContext(options = {}) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export function writeRouteContext({ cwd, skillName, sourceSkillName = skillName, payload = {}, env, ppid }) {
|
|
81
|
-
const shouldEnsureProjectLocal = skillName !== '
|
|
81
|
+
const shouldEnsureProjectLocal = skillName !== 'ask' && skillName !== 'help'
|
|
82
82
|
const scope = getRuntimeScope(cwd, { payload, env, ppid, ensureProjectLocal: shouldEnsureProjectLocal })
|
|
83
83
|
const context = {
|
|
84
84
|
cwd: normalizePath(cwd),
|
|
85
85
|
skillName,
|
|
86
86
|
sourceSkillName,
|
|
87
|
-
zeroSideEffect: skillName === '
|
|
87
|
+
zeroSideEffect: skillName === 'ask', // 写入会话胶囊供未来运行时消费;当前由 SKILL.md 铁律 + guard 设计共同保证边界
|
|
88
88
|
identity: extractPayloadIdentity(payload),
|
|
89
89
|
source: routeSource(payload),
|
|
90
90
|
promptHash: hashPrompt(payload.prompt),
|
|
@@ -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
|
}
|