@tanstack/intent 0.2.1 → 0.3.1

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 (50) hide show
  1. package/README.md +1 -0
  2. package/dist/{artifact-coverage-DFtI6V_H.mjs → artifact-coverage-DerRKsWw.mjs} +1 -1
  3. package/dist/artifact-coverage-Dia0ZRPy.mjs +2 -0
  4. package/dist/{cli-error-DDAO6DIL.mjs → cli-error-BebkXaTJ.mjs} +1 -1
  5. package/dist/cli.mjs +18 -13
  6. package/dist/command-BBzoalDz.mjs +388 -0
  7. package/dist/{install-Bv2qdHwd.mjs → command-BMdho_4n.mjs} +85 -55
  8. package/dist/{command-runner-B5OofX0E.mjs → command-runner-BFvtjLMh.mjs} +1 -1
  9. package/dist/{core-DaAr5MBD.mjs → core-Bqyle4Vm.mjs} +12 -10
  10. package/dist/core.d.mts +10 -2
  11. package/dist/core.mjs +1 -1
  12. package/dist/{display-CnpA7XuV.mjs → display-Dc1feMcZ.mjs} +2 -2
  13. package/dist/{edit-package-json-D8xfcy2X.mjs → edit-package-json-CdnA7_TA.mjs} +2 -2
  14. package/dist/{exclude-DbHwcgQQ.mjs → exclude-IfSv-XI_.mjs} +2 -2
  15. package/dist/{excludes-ByvSbmmj.mjs → excludes-BEi9N7Ys.mjs} +1 -1
  16. package/dist/{setup-github-actions-IxZTZihi.mjs → github-actions-D41VZqWh.mjs} +2 -2
  17. package/dist/index.d.mts +9 -9
  18. package/dist/index.mjs +8 -8
  19. package/dist/{list-C-eGocZP.mjs → list-DUXFVM5r.mjs} +21 -5
  20. package/dist/{load-DDNrmeBz.mjs → load-DieqITz5.mjs} +3 -3
  21. package/dist/{meta-CF4XIYOo.mjs → meta-Dk7d0N2J.mjs} +2 -2
  22. package/dist/{package-manager-Dw7lYcI0.mjs → package-manager-C63Zi9q1.mjs} +1 -1
  23. package/dist/{skill-paths-Bm1P6IYe.mjs → paths-B0KW7rmz.mjs} +2 -2
  24. package/dist/{project-context-oi_m7paK.mjs → project-context-CALU5-15.mjs} +1 -1
  25. package/dist/{setup-CdfBc7Oe.d.mts → project-setup-Bvmg5uYy.d.mts} +2 -2
  26. package/dist/{resolver-Uwx8B5jv.mjs → resolver-6i-WBbh8.mjs} +3 -3
  27. package/dist/{scanner-C5bzzri5.mjs → scanner-B1pcLFee.mjs} +68 -68
  28. package/dist/{setup-Cx1r2y-1.mjs → setup-D5qLjoqf.mjs} +16 -9
  29. package/dist/setup-DFajGERl.mjs +3 -0
  30. package/dist/setup.d.mts +1 -1
  31. package/dist/setup.mjs +2 -2
  32. package/dist/source-policy-C5S58Cno.mjs +2 -0
  33. package/dist/{source-policy-D__bcpoU.mjs → source-policy-CzZqrSTS.mjs} +36 -11
  34. package/dist/{stale-DhjSTIt-.mjs → stale-DlNJHwga.mjs} +2 -2
  35. package/dist/{staleness-DoZU3lzy.mjs → staleness-I_jAT1Ge.mjs} +3 -3
  36. package/dist/{staleness-B5Cqe77_.mjs → staleness-bEZ8BeGq.mjs} +1 -1
  37. package/dist/{cli-support-BANzHEBM.mjs → support-CDhR09kb.mjs} +1 -1
  38. package/dist/{cli-support-z64kSJOO.mjs → support-D7lyVf_J.mjs} +9 -9
  39. package/dist/{types-ByXUTBJ2.d.mts → types-Bx6-umBo.d.mts} +1 -1
  40. package/dist/{skill-use-B2xRF1i9.mjs → use-plp2M918.mjs} +1 -1
  41. package/dist/{utils-BKBDYbCx.mjs → utils-BpmAIjiN.mjs} +1 -1
  42. package/dist/{utils-6FtqhOYf.mjs → utils-Bw7HwOo5.mjs} +1 -1
  43. package/dist/{validate-BAU0uzvQ.mjs → validate-Cm-gfxGX.mjs} +5 -5
  44. package/dist/{workflow-review-Bo2kPVXV.mjs → workflow-review-Bx8x6_uF.mjs} +1 -1
  45. package/dist/{workflow-review-B4AfwtHH.mjs → workflow-review-CEwwmDdD.mjs} +1 -1
  46. package/dist/{workspace-patterns-BDoJIWk-.mjs → workspace-patterns-hW0v_meY.mjs} +2 -2
  47. package/dist/{workspace-patterns-CrL8hAbd.mjs → workspace-patterns-qoXkCfEX.mjs} +1 -1
  48. package/package.json +2 -1
  49. package/dist/artifact-coverage-CXX6wav1.mjs +0 -2
  50. package/dist/source-policy-CTeI29oF.mjs +0 -2
package/README.md CHANGED
@@ -122,6 +122,7 @@ The real risk with any derived artifact is staleness. `npx @tanstack/intent@late
122
122
  | Command | Description |
123
123
  | -------------------------------------------------- | --------------------------------------------------- |
124
124
  | `npx @tanstack/intent@latest install` | Set up skill loading guidance in agent config files |
125
+ | `npx @tanstack/intent@latest hooks install` | Install hook enforcement for supported agents |
125
126
  | `npx @tanstack/intent@latest list [--json]` | Discover local intent-enabled packages |
126
127
  | `npx @tanstack/intent@latest load <use>` | Load `<package>#<skill>` SKILL.md content |
127
128
  | `npx @tanstack/intent@latest meta` | List meta-skills for library maintainers |
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { parse } from "yaml";
4
- //#region src/artifact-coverage.ts
4
+ //#region src/staleness/artifact-coverage.ts
5
5
  function isRecord(value) {
6
6
  return !!value && typeof value === "object" && !Array.isArray(value);
7
7
  }
@@ -0,0 +1,2 @@
1
+ import { t as readIntentArtifacts } from "./artifact-coverage-DerRKsWw.mjs";
2
+ export { readIntentArtifacts };
@@ -1,4 +1,4 @@
1
- //#region src/cli-error.ts
1
+ //#region src/shared/cli-error.ts
2
2
  const CLI_FAILURE = Symbol("CliFailure");
3
3
  function fail(message, exitCode = 1) {
4
4
  throw {
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as isCliFailure, t as fail } from "./cli-error-DDAO6DIL.mjs";
2
+ import { n as isCliFailure, t as fail } from "./cli-error-BebkXaTJ.mjs";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { cac } from "cac";
@@ -7,48 +7,53 @@ import { cac } from "cac";
7
7
  function createCli() {
8
8
  const cli = cac("intent");
9
9
  cli.usage("<command> [options]");
10
- cli.command("list", "Discover intent-enabled packages from the project or workspace").usage("list [--json] [--debug] [--global] [--global-only] [--no-notices]").option("--json", "Output JSON").option("--debug", "Print discovery debug details to stderr").option("--global", "Include global packages after project packages").option("--global-only", "List global packages only").option("--no-notices", "Suppress non-critical notices on stderr").example("list").example("list --json").example("list --global").action(async (options) => {
11
- const { runListCommand } = await import("./list-C-eGocZP.mjs");
10
+ cli.command("list", "Discover intent-enabled packages from the project or workspace").usage("list [--json] [--debug] [--global] [--global-only] [--show-hidden] [--no-notices]").option("--json", "Output JSON").option("--debug", "Print discovery debug details to stderr").option("--global", "Include global packages after project packages").option("--global-only", "List global packages only").option("--show-hidden", "Show hidden skill sources not listed in intent.skills").option("--no-notices", "Suppress non-critical notices on stderr").example("list").example("list --json").example("list --global").action(async (options) => {
11
+ const { runListCommand } = await import("./list-DUXFVM5r.mjs");
12
12
  await runListCommand(options);
13
13
  });
14
14
  cli.command("exclude [action] [pattern]", "Manage package.json intent.exclude entries").usage("exclude [list|add|remove] [pattern] [--json]").option("--json", "Output JSON list of configured exclude patterns").example("exclude").example("exclude list --json").example("exclude add @tanstack/router#experimental-*").example("exclude remove @tanstack/router#experimental-*").action(async (action, pattern, options) => {
15
- const { runExcludeCommand } = await import("./exclude-DbHwcgQQ.mjs");
15
+ const { runExcludeCommand } = await import("./exclude-IfSv-XI_.mjs");
16
16
  await runExcludeCommand(action, pattern, options);
17
17
  });
18
18
  cli.command("load [use]", "Load a compact skill use and print its SKILL.md").usage("load <use> [--path] [--json] [--debug] [--global] [--global-only]").option("--path", "Print the resolved skill path instead of file content").option("--json", "Output JSON").option("--debug", "Print resolution debug details to stderr").option("--global", "Load from project packages, then global packages").option("--global-only", "Load from global packages only").example("load @tanstack/query#core").example("load @tanstack/query#core --path").action(async (use, options) => {
19
- const { runLoadCommand } = await import("./load-DDNrmeBz.mjs");
19
+ const { runLoadCommand } = await import("./load-DieqITz5.mjs");
20
20
  await runLoadCommand(use, options);
21
21
  });
22
22
  cli.command("meta [name]", "List meta-skills, or print one by name").usage("meta [name]").example("meta").example("meta domain-discovery").action(async (name) => {
23
- const [{ getMetaDir }, { runMetaCommand }] = await Promise.all([import("./cli-support-BANzHEBM.mjs"), import("./meta-CF4XIYOo.mjs")]);
23
+ const [{ getMetaDir }, { runMetaCommand }] = await Promise.all([import("./support-CDhR09kb.mjs"), import("./meta-Dk7d0N2J.mjs")]);
24
24
  await runMetaCommand(name, getMetaDir());
25
25
  });
26
26
  cli.command("validate [dir]", "Validate skill files").usage("validate [dir] [--github-summary] [--fix] [--check]").option("--github-summary", "Write a GitHub Actions step summary").option("--fix", "Rewrite fixable SKILL.md frontmatter issues").option("--check", "Fail if fixable SKILL.md frontmatter issues would be rewritten").example("validate").example("validate packages/query/skills").action(async (dir, options) => {
27
- const { runValidateCommand } = await import("./validate-BAU0uzvQ.mjs");
27
+ const { runValidateCommand } = await import("./validate-Cm-gfxGX.mjs");
28
28
  await runValidateCommand(dir, options);
29
29
  });
30
30
  cli.command("install", "Create or update skill loading guidance in an agent config file").usage("install [--map] [--dry-run] [--print-prompt] [--global] [--global-only] [--no-notices]").option("--map", "Write explicit skill-to-task mappings").option("--dry-run", "Print the generated block without writing").option("--print-prompt", "Print the legacy agent setup prompt instead of writing").option("--global", "Include global packages after project packages").option("--global-only", "Install mappings from global packages only").option("--no-notices", "Suppress non-critical notices on stderr").example("install").example("install --map").example("install --dry-run").example("install --print-prompt").example("install --global").action(async (options) => {
31
- const [{ scanIntentsOrFail }, { runInstallCommand }] = await Promise.all([import("./cli-support-BANzHEBM.mjs"), import("./install-Bv2qdHwd.mjs")]);
31
+ const [{ scanIntentsOrFail }, { runInstallCommand }] = await Promise.all([import("./support-CDhR09kb.mjs"), import("./command-BMdho_4n.mjs")]);
32
32
  await runInstallCommand(options, scanIntentsOrFail);
33
33
  });
34
+ cli.command("hooks [action]", "Manage agent hooks that enforce skill loading").usage("hooks install [--scope project|user] [--agents copilot,claude,codex|all]").option("--scope <scope>", "Hook install scope: project or user").option("--agents <agents>", "Hook agents: copilot,claude,codex, or all").example("hooks install").example("hooks install --scope user --agents copilot").action(async (action, options) => {
35
+ if (action !== "install") fail("Unknown hooks action: expected install.");
36
+ const { runHooksInstallCommand } = await import("./command-BBzoalDz.mjs");
37
+ runHooksInstallCommand(options);
38
+ });
34
39
  cli.command("scaffold", "Print maintainer scaffold prompt").usage("scaffold").action(async () => {
35
- const [{ getMetaDir }, { runScaffoldCommand }] = await Promise.all([import("./cli-support-BANzHEBM.mjs"), import("./scaffold-D8TAMXvs.mjs")]);
40
+ const [{ getMetaDir }, { runScaffoldCommand }] = await Promise.all([import("./support-CDhR09kb.mjs"), import("./scaffold-D8TAMXvs.mjs")]);
36
41
  runScaffoldCommand(getMetaDir());
37
42
  });
38
43
  cli.command("stale [dir]", "Check skills for staleness in the current package or workspace").usage("stale [dir] [--json] [--github-review]").option("--json", "Output JSON").option("--github-review", "Write GitHub Actions review PR files").option("--package-label <label>", "Fallback package label for review PRs").example("stale").example("stale packages/query").example("stale --json").action(async (targetDir, options) => {
39
- const [{ resolveStaleTargets }, { runStaleCommand }] = await Promise.all([import("./cli-support-BANzHEBM.mjs"), import("./stale-DhjSTIt-.mjs")]);
44
+ const [{ resolveStaleTargets }, { runStaleCommand }] = await Promise.all([import("./support-CDhR09kb.mjs"), import("./stale-DlNJHwga.mjs")]);
40
45
  await runStaleCommand(targetDir, options, resolveStaleTargets);
41
46
  });
42
47
  cli.command("edit-package-json", "Update package.json files so skills are published").usage("edit-package-json").action(async () => {
43
- const { runEditPackageJsonCommand } = await import("./edit-package-json-D8xfcy2X.mjs");
48
+ const { runEditPackageJsonCommand } = await import("./edit-package-json-CdnA7_TA.mjs");
44
49
  await runEditPackageJsonCommand(process.cwd());
45
50
  });
46
51
  cli.command("setup", "Copy Intent CI workflow templates into .github/workflows/").usage("setup").action(async () => {
47
- const [{ getMetaDir }, { runSetupGithubActionsCommand }] = await Promise.all([import("./cli-support-BANzHEBM.mjs"), import("./setup-github-actions-IxZTZihi.mjs")]);
52
+ const [{ getMetaDir }, { runSetupGithubActionsCommand }] = await Promise.all([import("./support-CDhR09kb.mjs"), import("./github-actions-D41VZqWh.mjs")]);
48
53
  await runSetupGithubActionsCommand(process.cwd(), getMetaDir());
49
54
  });
50
55
  cli.command("setup-github-actions", "Copy Intent CI workflow templates into .github/workflows/").usage("setup-github-actions").action(async () => {
51
- const [{ getMetaDir }, { runSetupGithubActionsCommand }] = await Promise.all([import("./cli-support-BANzHEBM.mjs"), import("./setup-github-actions-IxZTZihi.mjs")]);
56
+ const [{ getMetaDir }, { runSetupGithubActionsCommand }] = await Promise.all([import("./support-CDhR09kb.mjs"), import("./github-actions-D41VZqWh.mjs")]);
52
57
  await runSetupGithubActionsCommand(process.cwd(), getMetaDir());
53
58
  });
54
59
  cli.command("help [command]", "Display help for a command").action((commandName) => {
@@ -0,0 +1,388 @@
1
+ import { t as fail } from "./cli-error-BebkXaTJ.mjs";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, relative } from "node:path";
4
+ import { homedir } from "node:os";
5
+ //#region src/hooks/adapters.ts
6
+ const HOOK_SCRIPT_DIR = ".intent/hooks";
7
+ const HOOK_AGENT_ADAPTERS = {
8
+ claude: {
9
+ agent: "claude",
10
+ configKind: "claude-settings",
11
+ supportedScopes: new Set(["project", "user"]),
12
+ paths: (scope, { homeDir, root }) => {
13
+ const project = scope === "project";
14
+ return {
15
+ configPath: project ? join(root, ".claude", "settings.json") : join(homeDir, ".claude", "settings.json"),
16
+ scriptPath: project ? join(root, HOOK_SCRIPT_DIR, "intent-claude-gate.mjs") : join(homeDir, ".tanstack", "intent", "hooks", "intent-claude-gate.mjs")
17
+ };
18
+ }
19
+ },
20
+ codex: {
21
+ agent: "codex",
22
+ configKind: "codex-hooks",
23
+ supportedScopes: new Set(["project", "user"]),
24
+ paths: (scope, { homeDir, root }) => {
25
+ const project = scope === "project";
26
+ return {
27
+ configPath: project ? join(root, ".codex", "hooks.json") : join(homeDir, ".codex", "hooks.json"),
28
+ scriptPath: project ? join(root, HOOK_SCRIPT_DIR, "intent-codex-gate.mjs") : join(homeDir, ".tanstack", "intent", "hooks", "intent-codex-gate.mjs")
29
+ };
30
+ }
31
+ },
32
+ copilot: {
33
+ agent: "copilot",
34
+ configKind: "copilot-hooks",
35
+ supportedScopes: new Set(["user"]),
36
+ paths: (_scope, { copilotHome, homeDir }) => ({
37
+ configPath: join(copilotHome ?? join(homeDir, ".copilot"), "hooks", "hooks.json"),
38
+ scriptPath: join(homeDir, ".tanstack", "intent", "hooks", "intent-copilot-gate.mjs")
39
+ })
40
+ }
41
+ };
42
+ const ALL_HOOK_AGENTS = [
43
+ "copilot",
44
+ "claude",
45
+ "codex"
46
+ ];
47
+ //#endregion
48
+ //#region src/hooks/policy.ts
49
+ const EDIT_TOOLS_BY_AGENT = {
50
+ claude: new Set([
51
+ "Write",
52
+ "Edit",
53
+ "MultiEdit",
54
+ "NotebookEdit"
55
+ ]),
56
+ codex: new Set([
57
+ "apply_patch",
58
+ "Write",
59
+ "Edit"
60
+ ]),
61
+ copilot: new Set([
62
+ "Write",
63
+ "Edit",
64
+ "MultiEdit",
65
+ "NotebookEdit"
66
+ ])
67
+ };
68
+ const GATE_DENY_REASON = "Blocked: load matching TanStack guidance before editing. Follow this repo's TanStack guidance setup, then retry the edit.";
69
+ //#endregion
70
+ //#region src/hooks/install.ts
71
+ const STATUS_MESSAGE = "Checking Intent guidance";
72
+ function runInstallHooks({ agents, copilotHome, homeDir = homedir(), root, scope }) {
73
+ const resolvedScope = parseScope(scope);
74
+ return parseAgents(agents).map((agent) => installAgentHook({
75
+ agent,
76
+ copilotHome,
77
+ homeDir,
78
+ root,
79
+ scope: resolvedScope
80
+ }));
81
+ }
82
+ function validateHookInstallOptions({ agents, scope }) {
83
+ parseScope(scope);
84
+ parseAgents(agents);
85
+ }
86
+ function buildHookRunnerScript(agent) {
87
+ const editTools = [...EDIT_TOOLS_BY_AGENT[agent]].sort();
88
+ return `#!/usr/bin/env node
89
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
90
+ import { tmpdir } from 'node:os'
91
+ import { dirname, join } from 'node:path'
92
+ import { createHash } from 'node:crypto'
93
+
94
+ const AGENT = ${JSON.stringify(agent)}
95
+ const EDIT_TOOLS = new Set(${JSON.stringify(editTools)})
96
+ const GATE_DENY_REASON = ${JSON.stringify(GATE_DENY_REASON)}
97
+ const INTENT_COMMAND_PATTERN = /(?:^|&&|\\|\\||;|\\|)\\s*((?:bunx\\s+@tanstack\\/intent(?:@latest)?)|(?:pnpm\\s+exec\\s+intent)|(?:pnpm\\s+dlx\\s+@tanstack\\/intent(?:@latest)?)|(?:npx\\s+@tanstack\\/intent(?:@latest)?)|(?:yarn\\s+dlx\\s+@tanstack\\/intent(?:@latest)?)|(?:intent))\\s+(list|load)(?:\\s+([^\\s|;&]+))?/i
98
+
99
+ try {
100
+ const event = readEventFromStdin()
101
+ const stateFile = stateFileForEvent(event)
102
+ const observation = observationFromEvent(event)
103
+
104
+ if (observation) {
105
+ appendObservation(stateFile, observation)
106
+ }
107
+
108
+ const toolName = event?.tool_name ?? event?.toolName
109
+ if (typeof toolName === 'string' && EDIT_TOOLS.has(toolName) && !hasLoad(stateFile)) {
110
+ process.stdout.write(JSON.stringify(denyOutput()))
111
+ }
112
+ } catch {
113
+ }
114
+
115
+ process.exit(0)
116
+
117
+ function readEventFromStdin() {
118
+ try {
119
+ return JSON.parse(readFileSync(0, 'utf8'))
120
+ } catch {
121
+ return {}
122
+ }
123
+ }
124
+
125
+ function stateFileForEvent(event) {
126
+ const sessionId = typeof event?.session_id === 'string' ? event.session_id : 'unknown'
127
+ const cwd = typeof event?.cwd === 'string' ? event.cwd : process.cwd()
128
+ const key = createHash('sha256').update(AGENT + '\\0' + cwd + '\\0' + sessionId).digest('hex')
129
+ return join(tmpdir(), 'tanstack-intent-hooks', key + '.jsonl')
130
+ }
131
+
132
+ function observationFromEvent(event) {
133
+ if (!event || typeof event !== 'object') return undefined
134
+ const toolName = event.tool_name ?? event.toolName
135
+ const toolInput = event.tool_input ?? event.toolArgs
136
+ if (toolName !== 'Bash') return undefined
137
+ const command = typeof toolInput === 'string' ? safeCommandFromString(toolInput) : commandFromObject(toolInput)
138
+ const parsed = parseIntentInvocation(command)
139
+ if (!parsed || typeof command !== 'string') return undefined
140
+ return { action: parsed.action, skillUse: parsed.skillUse, raw: command }
141
+ }
142
+
143
+ function parseIntentInvocation(command) {
144
+ if (typeof command !== 'string') return undefined
145
+ const match = command.match(INTENT_COMMAND_PATTERN)
146
+ if (!match?.[1] || !match[2]) return undefined
147
+ const action = match[2].toLowerCase()
148
+ if (action !== 'list' && action !== 'load') return undefined
149
+ const skillUse = action === 'load' ? match[3] : undefined
150
+ if (action === 'load' && !skillUse) return undefined
151
+ return action === 'load' ? { action, skillUse } : { action }
152
+ }
153
+
154
+ function commandFromObject(value) {
155
+ return value && typeof value === 'object' ? value.command : undefined
156
+ }
157
+
158
+ function safeCommandFromString(value) {
159
+ try {
160
+ const command = commandFromObject(JSON.parse(value))
161
+ return typeof command === 'string' ? command : value
162
+ } catch {
163
+ return value
164
+ }
165
+ }
166
+
167
+ function appendObservation(stateFile, observation) {
168
+ try {
169
+ mkdirSync(dirname(stateFile), { recursive: true })
170
+ appendFileSync(stateFile, JSON.stringify({ ts: new Date().toISOString(), ...observation }) + '\\n')
171
+ } catch {
172
+ }
173
+ }
174
+
175
+ function hasLoad(stateFile) {
176
+ if (!existsSync(stateFile)) return false
177
+ try {
178
+ return readFileSync(stateFile, 'utf8')
179
+ .split('\\n')
180
+ .filter(Boolean)
181
+ .some((line) => {
182
+ try {
183
+ return JSON.parse(line).action === 'load'
184
+ } catch {
185
+ return false
186
+ }
187
+ })
188
+ } catch {
189
+ return false
190
+ }
191
+ }
192
+
193
+ function denyOutput() {
194
+ if (AGENT === 'copilot') {
195
+ return { permissionDecision: 'deny', permissionDecisionReason: GATE_DENY_REASON }
196
+ }
197
+
198
+ return {
199
+ hookSpecificOutput: {
200
+ hookEventName: 'PreToolUse',
201
+ permissionDecision: 'deny',
202
+ permissionDecisionReason: GATE_DENY_REASON,
203
+ },
204
+ }
205
+ }
206
+ `;
207
+ }
208
+ function formatHookInstallResult(result) {
209
+ if (result.status === "skipped") return `Skipped Intent hooks for ${result.agent}: ${result.reason}`;
210
+ const target = result.configPath ? formatPath(result.configPath) : result.agent;
211
+ switch (result.status) {
212
+ case "created": return `Installed Intent hooks for ${result.agent} (${result.scope}) in ${target}.`;
213
+ case "updated": return `Updated Intent hooks for ${result.agent} (${result.scope}) in ${target}.`;
214
+ case "unchanged": return `No changes to Intent hooks for ${result.agent} (${result.scope}); already current.`;
215
+ }
216
+ }
217
+ function installAgentHook({ agent, copilotHome, homeDir, root, scope }) {
218
+ const adapter = HOOK_AGENT_ADAPTERS[agent];
219
+ if (!adapter.supportedScopes.has(scope)) return {
220
+ agent,
221
+ configPath: null,
222
+ reason: "project scope is not supported; use --scope user",
223
+ scope,
224
+ scriptPath: null,
225
+ status: "skipped"
226
+ };
227
+ const { configPath, scriptPath } = adapter.paths(scope, {
228
+ copilotHome: copilotHome ?? process.env.COPILOT_HOME,
229
+ homeDir,
230
+ root
231
+ });
232
+ return hookInstallResult({
233
+ agent,
234
+ configPath,
235
+ scope,
236
+ scriptPath,
237
+ scriptStatus: writeIfChanged(scriptPath, buildHookRunnerScript(agent)),
238
+ configStatus: updateJsonConfig(configPath, (config) => upsertAdapterPreToolUseHook({
239
+ config,
240
+ configKind: adapter.configKind,
241
+ project: scope === "project",
242
+ scriptPath
243
+ }))
244
+ });
245
+ }
246
+ function hookInstallResult({ agent, configPath, configStatus, scope, scriptPath, scriptStatus }) {
247
+ return {
248
+ agent,
249
+ configPath,
250
+ scope,
251
+ scriptPath,
252
+ status: scriptStatus === "created" || configStatus === "created" ? "created" : scriptStatus === "updated" || configStatus === "updated" ? "updated" : "unchanged"
253
+ };
254
+ }
255
+ function upsertAdapterPreToolUseHook({ config, configKind, project, scriptPath }) {
256
+ switch (configKind) {
257
+ case "claude-settings": return upsertClaudePreToolUseHook(config, project, scriptPath);
258
+ case "codex-hooks": return upsertCodexPreToolUseHook(config, project, scriptPath);
259
+ case "copilot-hooks": return upsertCopilotPreToolUseHook(config, scriptPath);
260
+ }
261
+ }
262
+ function upsertClaudePreToolUseHook(config, project, scriptPath) {
263
+ const hooks = objectValue(config.hooks);
264
+ hooks.PreToolUse = upsertHookGroup(arrayValue(hooks.PreToolUse), {
265
+ matcher: "Bash|Write|Edit|MultiEdit|NotebookEdit",
266
+ hooks: [{
267
+ type: "command",
268
+ command: "node",
269
+ args: [project ? "${CLAUDE_PROJECT_DIR}/.intent/hooks/intent-claude-gate.mjs" : scriptPath],
270
+ timeout: 10,
271
+ statusMessage: STATUS_MESSAGE
272
+ }]
273
+ });
274
+ return {
275
+ ...config,
276
+ hooks
277
+ };
278
+ }
279
+ function upsertCodexPreToolUseHook(config, project, scriptPath) {
280
+ const hooks = objectValue(config.hooks);
281
+ hooks.PreToolUse = upsertHookGroup(arrayValue(hooks.PreToolUse), {
282
+ matcher: "Bash|apply_patch|Edit|Write",
283
+ hooks: [{
284
+ type: "command",
285
+ command: project ? "node \"$(git rev-parse --show-toplevel)/.intent/hooks/intent-codex-gate.mjs\"" : `node ${quoteShell(scriptPath)}`,
286
+ timeout: 10,
287
+ statusMessage: STATUS_MESSAGE
288
+ }]
289
+ });
290
+ return {
291
+ ...config,
292
+ hooks
293
+ };
294
+ }
295
+ function upsertCopilotPreToolUseHook(config, scriptPath) {
296
+ const hooks = objectValue(config.hooks);
297
+ hooks.PreToolUse = upsertHookGroup(arrayValue(hooks.PreToolUse), { command: `node ${quoteShell(scriptPath)}` });
298
+ return {
299
+ ...config,
300
+ hooks
301
+ };
302
+ }
303
+ function upsertHookGroup(groups, nextGroup) {
304
+ return [...groups.flatMap(withoutIntentHooks), nextGroup];
305
+ }
306
+ function withoutIntentHooks(value) {
307
+ if (!value || typeof value !== "object") return [value];
308
+ const hooks = arrayValue(value.hooks);
309
+ if (hooks.length === 0) return isIntentHook(value) ? [] : [value];
310
+ const nextHooks = hooks.filter((hook) => !isIntentHook(hook));
311
+ if (nextHooks.length === hooks.length) return [value];
312
+ if (nextHooks.length === 0) return [];
313
+ return [{
314
+ ...value,
315
+ hooks: nextHooks
316
+ }];
317
+ }
318
+ function isIntentHook(value) {
319
+ if (!value || typeof value !== "object") return false;
320
+ const entry = value;
321
+ return [typeof entry.command === "string" ? entry.command : "", ...Array.isArray(entry.args) ? entry.args.filter((arg) => typeof arg === "string") : []].some(isIntentGateScriptReference);
322
+ }
323
+ function isIntentGateScriptReference(value) {
324
+ return /(?:^|[\s"'\\/])(?:old-)?intent-(claude|codex|copilot)-gate\.mjs(?:$|[?#\s"'])/i.test(value);
325
+ }
326
+ function updateJsonConfig(filePath, update) {
327
+ const existed = existsSync(filePath);
328
+ const current = existed ? readFileSync(filePath, "utf8") : "";
329
+ const parsed = current.trim() ? parseJsonObject(filePath, current) : {};
330
+ const next = `${JSON.stringify(update(parsed), null, 2)}\n`;
331
+ if (current === next) return "unchanged";
332
+ mkdirSync(dirname(filePath), { recursive: true });
333
+ writeFileSync(filePath, next);
334
+ return existed ? "updated" : "created";
335
+ }
336
+ function writeIfChanged(filePath, content) {
337
+ const existed = existsSync(filePath);
338
+ if (existed && readFileSync(filePath, "utf8") === content) return "unchanged";
339
+ mkdirSync(dirname(filePath), { recursive: true });
340
+ writeFileSync(filePath, content);
341
+ return existed ? "updated" : "created";
342
+ }
343
+ function parseAgents(value) {
344
+ if (!value || value === "all") return ALL_HOOK_AGENTS;
345
+ const agents = value.split(",").map((agent) => agent.trim()).filter(Boolean);
346
+ const invalid = agents.filter((agent) => !ALL_HOOK_AGENTS.includes(agent));
347
+ if (invalid.length > 0) fail(`Unknown hook agent: ${invalid.join(", ")}. Expected copilot, claude, codex, or all.`);
348
+ return [...new Set(agents)];
349
+ }
350
+ function parseScope(value) {
351
+ if (!value) return "project";
352
+ if (value === "project" || value === "user") return value;
353
+ fail(`Unknown hook scope: ${value}. Expected project or user.`);
354
+ }
355
+ function parseJsonObject(filePath, content) {
356
+ try {
357
+ const parsed = JSON.parse(content);
358
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
359
+ } catch (err) {
360
+ fail(`Failed to parse ${formatPath(filePath)}: ${err instanceof Error ? err.message : String(err)}`);
361
+ }
362
+ fail(`Failed to parse ${formatPath(filePath)}: expected a JSON object.`);
363
+ }
364
+ function objectValue(value) {
365
+ return value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
366
+ }
367
+ function arrayValue(value) {
368
+ return Array.isArray(value) ? value : [];
369
+ }
370
+ function quoteShell(value) {
371
+ return `'${value.replace(/'/g, `'\\''`)}'`;
372
+ }
373
+ function formatPath(filePath) {
374
+ return relative(process.cwd(), filePath) || filePath;
375
+ }
376
+ //#endregion
377
+ //#region src/commands/hooks/command.ts
378
+ function runHooksInstallCommand(options) {
379
+ validateHookInstallOptions(options);
380
+ const results = runInstallHooks({
381
+ agents: options.agents,
382
+ root: process.cwd(),
383
+ scope: options.scope
384
+ });
385
+ for (const result of results) console.log(formatHookInstallResult(result));
386
+ }
387
+ //#endregion
388
+ export { runHooksInstallCommand };