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.
Files changed (49) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.cursor-plugin/plugin.json +24 -0
  4. package/.grok-plugin/plugin.json +24 -0
  5. package/README.md +42 -15
  6. package/README_CN.md +45 -18
  7. package/bootstrap-lite.md +23 -22
  8. package/bootstrap.md +25 -26
  9. package/cli.mjs +2 -0
  10. package/gemini-extension.json +1 -1
  11. package/hooks/hooks-cursor.json +41 -0
  12. package/hooks/hooks-grok.json +98 -0
  13. package/install.ps1 +3 -3
  14. package/install.sh +3 -3
  15. package/package.json +5 -2
  16. package/scripts/advisor-state.mjs +1 -1
  17. package/scripts/cli-config.mjs +1 -1
  18. package/scripts/cli-cursor.mjs +42 -0
  19. package/scripts/cli-doctor.mjs +173 -3
  20. package/scripts/cli-grok.mjs +54 -0
  21. package/scripts/cli-host-detect.mjs +67 -1
  22. package/scripts/cli-hosts.mjs +5 -0
  23. package/scripts/cli-lifecycle-hosts.mjs +197 -1
  24. package/scripts/cli-lifecycle.mjs +3 -1
  25. package/scripts/cli-messages.mjs +28 -14
  26. package/scripts/cli-runtime-root.mjs +284 -1
  27. package/scripts/cli-utils.mjs +36 -0
  28. package/scripts/cursor-hook.mjs +193 -0
  29. package/scripts/guard.mjs +6 -4
  30. package/scripts/notify-context.mjs +2 -1
  31. package/scripts/notify-events.mjs +2 -0
  32. package/scripts/notify-source.mjs +2 -0
  33. package/scripts/notify.mjs +3 -2
  34. package/scripts/plan-contract.mjs +1 -1
  35. package/scripts/runtime-context.mjs +2 -2
  36. package/scripts/runtime-scope.mjs +1 -1
  37. package/scripts/workflow-plan-files.mjs +1 -1
  38. package/scripts/workflow-recommendation.mjs +1 -1
  39. package/skills/commands/ask/SKILL.md +100 -0
  40. package/skills/commands/auto/SKILL.md +6 -9
  41. package/skills/commands/build/SKILL.md +5 -2
  42. package/skills/commands/help/SKILL.md +13 -13
  43. package/skills/commands/plan/SKILL.md +7 -10
  44. package/skills/commands/prd/SKILL.md +14 -14
  45. package/skills/helloagents/SKILL.md +3 -3
  46. package/skills/qa-review/SKILL.md +9 -0
  47. package/templates/plans/tasks.md +3 -3
  48. package/skills/commands/idea/SKILL.md +0 -56
  49. package/skills/commands/office/SKILL.md +0 -86
@@ -1,11 +1,22 @@
1
- import { copyFileSync, existsSync, mkdtempSync, realpathSync, renameSync } from 'node:fs'
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
+ }
@@ -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 HOST = IS_GEMINI ? 'gemini' : 'claude'
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
- '路由映射:~idea=只读探索与方向比较,不创建文件;~office=只读价值/范围评估,不创建文件;~build=明确实现;~qa=统一质量审查/验证/修复/收尾;~plan=结构化规划;~prd=重型规格;~auto=自动选择并继续执行后续阶段。',
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
  }
@@ -5,7 +5,9 @@ import { resolveSessionToken } from './session-token.mjs'
5
5
  const HOST_LABELS = {
6
6
  codex: 'Codex',
7
7
  claude: 'Claude Code',
8
+ cursor: 'Cursor',
8
9
  gemini: 'Gemini',
10
+ grok: 'Grok Build',
9
11
  }
10
12
 
11
13
  function normalizePath(filePath = '') {
@@ -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 !== 'idea' && skillName !== 'office' && skillName !== 'help'
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 === 'idea' || skillName === 'office',
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
  }