aiwcli 0.10.3 → 0.11.0

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 (189) hide show
  1. package/bin/run.js +1 -1
  2. package/dist/commands/clear.js +28 -131
  3. package/dist/commands/init/index.js +3 -3
  4. package/dist/lib/gitignore-manager.d.ts +32 -0
  5. package/dist/lib/gitignore-manager.js +141 -2
  6. package/dist/templates/CLAUDE.md +8 -8
  7. package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
  8. package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
  9. package/dist/templates/_shared/.claude/settings.json +7 -7
  10. package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
  11. package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
  12. package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
  13. package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
  14. package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
  15. package/dist/templates/_shared/hooks-ts/session_end.ts +104 -0
  16. package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
  17. package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
  18. package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
  19. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
  20. package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
  21. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +12 -12
  22. package/dist/templates/_shared/lib-ts/base/constants.ts +22 -15
  23. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +129 -50
  24. package/dist/templates/_shared/lib-ts/base/inference.ts +28 -21
  25. package/dist/templates/_shared/lib-ts/base/logger.ts +31 -15
  26. package/dist/templates/_shared/lib-ts/base/state-io.ts +9 -7
  27. package/dist/templates/_shared/lib-ts/base/stop-words.ts +131 -131
  28. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +139 -0
  29. package/dist/templates/_shared/lib-ts/base/utils.ts +69 -69
  30. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +30 -24
  31. package/dist/templates/_shared/lib-ts/context/context-selector.ts +50 -32
  32. package/dist/templates/_shared/lib-ts/context/context-store.ts +76 -48
  33. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +61 -37
  34. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +10 -6
  35. package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +11 -10
  36. package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +159 -0
  37. package/dist/templates/_shared/lib-ts/templates/formatters.ts +6 -4
  38. package/dist/templates/_shared/lib-ts/types.ts +68 -55
  39. package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
  40. package/dist/templates/_shared/scripts/resume_handoff.ts +321 -0
  41. package/dist/templates/_shared/scripts/save_handoff.ts +21 -21
  42. package/dist/templates/_shared/scripts/status_line.ts +733 -0
  43. package/dist/templates/cc-native/.claude/settings.json +175 -185
  44. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
  45. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +0 -2
  46. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
  47. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
  48. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +921 -0
  49. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
  50. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +157 -0
  51. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +709 -0
  52. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
  53. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +124 -0
  54. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
  55. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
  56. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
  57. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +119 -0
  58. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +162 -0
  59. package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
  60. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +249 -0
  61. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +155 -0
  62. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
  63. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +106 -0
  64. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
  65. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
  66. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +243 -0
  67. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
  68. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +310 -0
  69. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
  70. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -9
  71. package/oclif.manifest.json +1 -1
  72. package/package.json +1 -1
  73. package/dist/templates/_shared/hooks/__init__.py +0 -16
  74. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  75. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  76. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  77. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  78. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  79. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  80. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  81. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  82. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  83. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  84. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  85. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  86. package/dist/templates/_shared/hooks/archive_plan.py +0 -177
  87. package/dist/templates/_shared/hooks/context_monitor.py +0 -270
  88. package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
  89. package/dist/templates/_shared/hooks/pre_compact.py +0 -104
  90. package/dist/templates/_shared/hooks/session_end.py +0 -173
  91. package/dist/templates/_shared/hooks/session_start.py +0 -206
  92. package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
  93. package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
  94. package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
  95. package/dist/templates/_shared/lib/__init__.py +0 -1
  96. package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  97. package/dist/templates/_shared/lib/base/__init__.py +0 -65
  98. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  99. package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
  100. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  101. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  102. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  103. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  104. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  105. package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
  106. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  107. package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
  108. package/dist/templates/_shared/lib/base/constants.py +0 -358
  109. package/dist/templates/_shared/lib/base/hook_utils.py +0 -339
  110. package/dist/templates/_shared/lib/base/inference.py +0 -307
  111. package/dist/templates/_shared/lib/base/logger.py +0 -305
  112. package/dist/templates/_shared/lib/base/stop_words.py +0 -221
  113. package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
  114. package/dist/templates/_shared/lib/base/utils.py +0 -263
  115. package/dist/templates/_shared/lib/context/__init__.py +0 -102
  116. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  117. package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
  118. package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
  119. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  120. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  121. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  122. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  123. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  124. package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
  125. package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
  126. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  127. package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
  128. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  129. package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
  130. package/dist/templates/_shared/lib/context/context_selector.py +0 -508
  131. package/dist/templates/_shared/lib/context/context_store.py +0 -653
  132. package/dist/templates/_shared/lib/context/plan_manager.py +0 -303
  133. package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
  134. package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
  135. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  136. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  137. package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
  138. package/dist/templates/_shared/lib/templates/README.md +0 -206
  139. package/dist/templates/_shared/lib/templates/__init__.py +0 -36
  140. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  141. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  142. package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
  143. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  144. package/dist/templates/_shared/lib/templates/formatters.py +0 -146
  145. package/dist/templates/_shared/lib/templates/plan_context.py +0 -73
  146. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  147. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  148. package/dist/templates/_shared/scripts/save_handoff.py +0 -357
  149. package/dist/templates/_shared/scripts/status_line.py +0 -716
  150. package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
  151. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
  152. package/dist/templates/cc-native/MIGRATION.md +0 -86
  153. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  154. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  155. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  156. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  157. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  158. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  159. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -130
  160. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -954
  161. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
  162. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
  163. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
  164. package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
  165. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  166. package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
  167. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  168. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  169. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  170. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  171. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  172. package/dist/templates/cc-native/_cc-native/lib/constants.py +0 -45
  173. package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
  174. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
  175. package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
  176. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  177. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  178. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  179. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  180. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  181. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +0 -215
  182. package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
  183. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
  184. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
  185. package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
  186. package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1071
  187. package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
  188. package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
  189. package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
@@ -5,10 +5,10 @@
5
5
  * See SPEC.md §4
6
6
  */
7
7
 
8
+ import * as crypto from "node:crypto";
8
9
  import * as fs from "node:fs";
10
+ import * as _os from "node:os";
9
11
  import * as path from "node:path";
10
- import * as os from "node:os";
11
- import * as crypto from "node:crypto";
12
12
 
13
13
  /**
14
14
  * Write file atomically with retry logic.
@@ -21,7 +21,7 @@ export function atomicWrite(
21
21
  content: string,
22
22
  maxAttempts = 2,
23
23
  backoffMs: number[] = [500, 1000],
24
- ): [boolean, string | null] {
24
+ ): [boolean, null | string] {
25
25
  // Ensure parent directory exists
26
26
  const dir = path.dirname(filePath);
27
27
  fs.mkdirSync(dir, { recursive: true });
@@ -53,7 +53,7 @@ export function atomicWrite(
53
53
  fs.renameSync(tmpPath, filePath);
54
54
 
55
55
  return [true, null];
56
- } catch (e: any) {
56
+ } catch (error: any) {
57
57
  // Clean up temp file
58
58
  try {
59
59
  fs.unlinkSync(tmpPath);
@@ -62,11 +62,11 @@ export function atomicWrite(
62
62
  }
63
63
 
64
64
  if (attempt < maxAttempts - 1) {
65
- const waitMs = backoffMs[Math.min(attempt, backoffMs.length - 1)] ?? backoffMs[backoffMs.length - 1] ?? 500;
65
+ const waitMs = backoffMs[Math.min(attempt, backoffMs.length - 1)] ?? backoffMs.at(-1) ?? 500;
66
66
  sleepSync(waitMs);
67
67
  } else {
68
- const errType = e?.constructor?.name ?? "Error";
69
- const errMsg = String(e).split("\n")[0]?.slice(0, 200) ?? "";
68
+ const errType = error?.constructor?.name ?? "Error";
69
+ const errMsg = String(error).split("\n")[0]?.slice(0, 200) ?? "";
70
70
  return [false, `${errType}: ${errMsg}`];
71
71
  }
72
72
  }
@@ -85,7 +85,7 @@ export function atomicAppend(
85
85
  content: string,
86
86
  maxAttempts = 2,
87
87
  backoffMs: number[] = [500, 1000],
88
- ): [boolean, string | null] {
88
+ ): [boolean, null | string] {
89
89
  // Ensure parent directory exists
90
90
  const dir = path.dirname(filePath);
91
91
  fs.mkdirSync(dir, { recursive: true });
@@ -112,13 +112,13 @@ export function atomicAppend(
112
112
  }
113
113
 
114
114
  return [true, null];
115
- } catch (e: any) {
115
+ } catch (error: any) {
116
116
  if (attempt < maxAttempts - 1) {
117
- const waitMs = backoffMs[Math.min(attempt, backoffMs.length - 1)] ?? backoffMs[backoffMs.length - 1] ?? 500;
117
+ const waitMs = backoffMs[Math.min(attempt, backoffMs.length - 1)] ?? backoffMs.at(-1) ?? 500;
118
118
  sleepSync(waitMs);
119
119
  } else {
120
- const errType = e?.constructor?.name ?? "Error";
121
- const errMsg = String(e).split("\n")[0]?.slice(0, 200) ?? "";
120
+ const errType = error?.constructor?.name ?? "Error";
121
+ const errMsg = String(error).split("\n")[0]?.slice(0, 200) ?? "";
122
122
  return [false, `${errType}: ${errMsg}`];
123
123
  }
124
124
  }
@@ -3,8 +3,9 @@
3
3
  * See SPEC.md §2
4
4
  */
5
5
 
6
- import * as path from "node:path";
7
6
  import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+
8
9
  import { logWarn } from "./logger.js";
9
10
 
10
11
  // Directory names (relative to project root)
@@ -28,9 +29,9 @@ export const RETRY_BACKOFF_MS = [500, 1000];
28
29
 
29
30
  // Windows reserved filenames
30
31
  const WINDOWS_RESERVED = new Set([
31
- "CON", "PRN", "AUX", "NUL",
32
- "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
33
- "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
32
+ "AUX", "COM1", "COM2", "COM3",
33
+ "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "CON", "LPT1", "LPT2",
34
+ "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "NUL", "PRN",
34
35
  ]);
35
36
 
36
37
  /**
@@ -41,9 +42,9 @@ export function sanitizeContextId(contextId: string): string {
41
42
  if (!contextId) return "context";
42
43
 
43
44
  let result = contextId.toLowerCase();
44
- result = result.replace(/[^a-z0-9_-]/g, "-");
45
- result = result.replace(/[-_]+/g, "-");
46
- result = result.replace(/^[-_]+|[-_]+$/g, "");
45
+ result = result.replaceAll(/[^a-z0-9_-]/g, "-");
46
+ result = result.replaceAll(/[-_]+/g, "-");
47
+ result = result.replaceAll(/^[-_]+|[-_]+$/g, "");
47
48
 
48
49
  if (result.length > MAX_CONTEXT_ID_LENGTH) {
49
50
  result = result.slice(0, MAX_CONTEXT_ID_LENGTH).replace(/[-_]+$/, "");
@@ -101,13 +102,18 @@ export function getProjectRoot(payloadCwd?: string): string {
101
102
  } else {
102
103
  return envDir;
103
104
  }
104
- }
105
+ }
106
+
105
107
  if (payloadCwd) return payloadCwd;
106
108
  return process.cwd();
107
109
  }
108
110
 
109
111
  // §2.4 — Path functions
110
112
 
113
+ export function getAiwcliDir(projectRoot?: string): string {
114
+ return path.join(projectRoot ?? getProjectRoot(), ".aiwcli");
115
+ }
116
+
111
117
  export function getOutputDir(projectRoot?: string): string {
112
118
  return path.join(projectRoot ?? getProjectRoot(), OUTPUT_DIR);
113
119
  }
@@ -230,7 +236,8 @@ export function getHandoffFolderPath(
230
236
  while (fs.existsSync(folder)) {
231
237
  folder = path.join(handoffsDir, `${ts}-${counter}`);
232
238
  counter++;
233
- }
239
+ }
240
+
234
241
  return folder;
235
242
  }
236
243
 
@@ -268,8 +275,8 @@ export function sanitizeFilename(
268
275
  maxLen = 32,
269
276
  allowLeadingDot = false,
270
277
  ): string {
271
- let result = s.replace(/[^A-Za-z0-9._-]+/g, "_");
272
- result = result.replace(/^[._-]+|[._-]+$/g, "").slice(0, maxLen) || "unknown";
278
+ let result = s.replaceAll(/[^A-Za-z0-9._-]+/g, "_");
279
+ result = result.replaceAll(/^[._-]+|[._-]+$/g, "").slice(0, maxLen) || "unknown";
273
280
 
274
281
  if (!allowLeadingDot) {
275
282
  result = result.replace(/^\.+/, "");
@@ -285,10 +292,10 @@ export function sanitizeFilename(
285
292
 
286
293
  export function sanitizeTitle(s: string, maxLen = 50): string {
287
294
  let result = s.toLowerCase().trim();
288
- result = result.replace(/ /g, "-");
289
- result = result.replace(/[^a-z0-9._-]+/g, "_");
290
- result = result.replace(/[-_]+/g, "-");
291
- result = result.replace(/^[._-]+|[._-]+$/g, "").slice(0, maxLen) || "unknown";
295
+ result = result.replaceAll(' ', "-");
296
+ result = result.replaceAll(/[^a-z0-9._-]+/g, "_");
297
+ result = result.replaceAll(/[-_]+/g, "-");
298
+ result = result.replaceAll(/^[._-]+|[._-]+$/g, "").slice(0, maxLen) || "unknown";
292
299
 
293
300
  const baseName = (result.split(".")[0] ?? result).toUpperCase();
294
301
  if (WINDOWS_RESERVED.has(baseName)) {
@@ -5,24 +5,26 @@
5
5
  */
6
6
 
7
7
  import * as fs from "node:fs";
8
- import { logDebug, logInfo, logWarn, logError, logHookError, logDiagnostic, hookLog, setContextPath, getContextPath as _getContextPath } from "./logger.js";
8
+
9
9
  import { getProjectRoot } from "./constants.js";
10
+ import { getContextPath as _getContextPath, hookLog, logDebug, setSessionId } from "./logger.js";
10
11
  import { getContextBySessionId } from "../context/context-store.js";
11
12
  import type { HookInput, HookOutput } from "../types.js";
12
13
 
13
14
  // Re-export logger functions for convenience (matches Python hook_utils re-exports)
14
- export { logDebug, logInfo, logWarn, logError, logHookError, logDiagnostic, hookLog, setContextPath };
15
+
15
16
 
16
17
  // Context window baseline: tokens not visible in hook data §5.9
17
18
  export const CONTEXT_BASELINE_TOKENS = 22_600;
18
19
  export const DEFAULT_CONTEXT_WINDOW_SIZE = 200_000;
19
20
 
20
21
  // Event metadata stash — populated by loadHookInput(), read by runHook()
21
- let _lastHookEvent: string | null = null;
22
- let _lastToolName: string | null = null;
22
+ let _lastHookEvent: null | string = null;
23
+ let _lastToolName: null | string = null;
24
+ let _cachedHookName: null | string = null;
23
25
 
24
26
  // Pre-fetched input stash
25
- let _prefetchedInput: Record<string, any> | null = null;
27
+ let _prefetchedInput: null | Record<string, any> = null;
26
28
 
27
29
  /**
28
30
  * Load and parse JSON from stdin (or return prefetched input if set).
@@ -37,12 +39,13 @@ export function loadHookInput(): HookInput | null {
37
39
  _lastHookEvent = result.hook_event_name ?? null;
38
40
  _lastToolName = result.tool_name ?? null;
39
41
  }
42
+
40
43
  return result as HookInput;
41
44
  }
42
45
 
43
46
  try {
44
47
  // Read entire stdin using fd 0 (cross-platform, works on Windows)
45
- const inputData = fs.readFileSync(0, "utf-8").trim();
48
+ const inputData = fs.readFileSync(0, "utf8").trim();
46
49
  if (!inputData) return null;
47
50
 
48
51
  const result = JSON.parse(inputData);
@@ -50,6 +53,7 @@ export function loadHookInput(): HookInput | null {
50
53
  _lastHookEvent = result.hook_event_name ?? null;
51
54
  _lastToolName = result.tool_name ?? null;
52
55
  }
56
+
53
57
  return result as HookInput;
54
58
  } catch {
55
59
  return null;
@@ -76,7 +80,7 @@ export function validateHookEvent(
76
80
  */
77
81
  export function getToolInput(
78
82
  payload: HookInput,
79
- ): Record<string, any> | null {
83
+ ): null | Record<string, any> {
80
84
  const toolInput = payload.tool_input;
81
85
  return toolInput && typeof toolInput === "object" ? toolInput : null;
82
86
  }
@@ -92,53 +96,76 @@ export function checkSkipPersistence(
92
96
  const toolInput = getToolInput(payload);
93
97
  if (!toolInput) return false;
94
98
 
95
- const metadata = toolInput.metadata;
99
+ const {metadata} = toolInput;
96
100
  if (metadata && typeof metadata === "object" && metadata.skip_persistence) {
97
101
  logDebug(hookName, "Skipping persistence (skip_persistence flag set)");
98
102
  return true;
99
103
  }
104
+
100
105
  return false;
101
106
  }
102
107
 
103
108
  /**
104
109
  * Emit hookSpecificOutput with additionalContext to stdout.
110
+ * hookEventName is required by Claude Code's Zod validator (discriminated union).
111
+ * Auto-detected from stdin payload (set by loadHookInput/runHook).
105
112
  * See SPEC.md §5.5
106
113
  */
107
114
  export function emitContext(additionalContext: string): void {
115
+ const eventName = _lastHookEvent ?? undefined;
108
116
  const out: HookOutput = {
109
117
  hookSpecificOutput: {
118
+ ...(eventName ? { hookEventName: eventName } : {}),
110
119
  additionalContext,
111
120
  },
112
121
  };
113
- process.stdout.write(JSON.stringify(out) + "\n");
122
+ const json = JSON.stringify(out);
123
+ _logEmit("context", additionalContext.length, { additionalContext });
124
+ process.stdout.write(json + "\n");
114
125
  }
115
126
 
116
127
  /**
117
128
  * Emit hookSpecificOutput that denies the tool call with context and reason.
129
+ * hookEventName is required by Claude Code's Zod validator (discriminated union).
130
+ * Auto-detected from stdin payload (set by loadHookInput/runHook).
118
131
  * See SPEC.md §5.6
119
132
  */
120
133
  export function emitContextAndBlock(
121
134
  additionalContext: string,
122
135
  reason: string,
123
136
  ): void {
137
+ const eventName = _lastHookEvent ?? undefined;
124
138
  const out: HookOutput = {
125
139
  hookSpecificOutput: {
140
+ ...(eventName ? { hookEventName: eventName } : {}),
126
141
  additionalContext,
127
142
  permissionDecision: "deny",
128
143
  permissionDecisionReason: reason,
129
144
  },
130
145
  };
131
- process.stdout.write(JSON.stringify(out) + "\n");
146
+ const json = JSON.stringify(out);
147
+ _logEmit("block", additionalContext.length, { additionalContext, blockReason: reason });
148
+ process.stdout.write(json + "\n");
149
+ }
150
+
151
+ /** Log hook output (context or block) to hook-log.jsonl for visibility. */
152
+ function _logEmit(type: "block" | "context", chars: number, payload: Record<string, any>): void {
153
+ const hook = _cachedHookName ?? "unknown";
154
+ const msg = type === "block"
155
+ ? `HOOK_OUTPUT [${type}] ${chars} chars, reason="${(payload.blockReason ?? "").slice(0, 80)}"`
156
+ : `HOOK_OUTPUT [${type}] ${chars} chars`;
157
+ hookLog("info", hook, msg, { data: payload });
132
158
  }
133
159
 
134
160
  /**
135
161
  * Auto-detect template origin from the hook script path.
136
162
  */
137
163
  function detectTemplate(scriptPath = ""): string {
138
- const p = (scriptPath || (process.argv[1] ?? "")).replace(/\\/g, "/");
164
+ const p = (scriptPath || (process.argv[1] ?? "")).replaceAll('\\', "/");
139
165
  if (p.includes("/_shared/hooks/") || p.startsWith("_shared/hooks/")) {
140
166
  return "shared";
141
167
  }
168
+
142
169
  const match = p.match(/_([a-z][a-z0-9-]*)\/hooks\//);
143
170
  if (match?.[1]) return match[1]; // e.g., "cc-native"
144
171
  return "unknown";
@@ -151,7 +178,7 @@ function detectTemplate(scriptPath = ""): string {
151
178
  */
152
179
  export function parseContextWindow(
153
180
  hookInput: HookInput,
154
- ): [number | null, number | null] {
181
+ ): [null | number, null | number] {
155
182
  const contextWindow = hookInput.context_window;
156
183
  if (!contextWindow) return [null, null];
157
184
 
@@ -177,7 +204,7 @@ export function parseContextWindow(
177
204
  */
178
205
  export function getContextPercentRemaining(
179
206
  hookInput: HookInput,
180
- ): [number | null, number | null, number | null] {
207
+ ): [null | number, null | number, null | number] {
181
208
  const [tokensUsed, maxTokens] = parseContextWindow(hookInput);
182
209
 
183
210
  if (tokensUsed !== null && maxTokens !== null && maxTokens > 0) {
@@ -206,6 +233,45 @@ export function getContextPercentRemaining(
206
233
  return [null, null, null];
207
234
  }
208
235
 
236
+ /**
237
+ * Read stdin early and extract session_id + event metadata.
238
+ * Stashes parsed input for loadHookInput() to consume later.
239
+ */
240
+ function _earlyReadInput(prefetchedInput?: Record<string, any>): void {
241
+ if (prefetchedInput !== undefined) {
242
+ _prefetchedInput = prefetchedInput;
243
+ }
244
+
245
+ // If we already have prefetched input, extract metadata from it
246
+ if (_prefetchedInput && typeof _prefetchedInput === "object") {
247
+ _lastHookEvent = _prefetchedInput.hook_event_name ?? null;
248
+ _lastToolName = _prefetchedInput.tool_name ?? null;
249
+ if (_prefetchedInput.session_id) {
250
+ setSessionId(_prefetchedInput.session_id);
251
+ }
252
+
253
+ return;
254
+ }
255
+
256
+ // Read stdin now so HOOK_START can include sid
257
+ try {
258
+ const inputData = fs.readFileSync(0, "utf8").trim();
259
+ if (inputData) {
260
+ const parsed = JSON.parse(inputData);
261
+ if (parsed && typeof parsed === "object") {
262
+ _prefetchedInput = parsed;
263
+ _lastHookEvent = parsed.hook_event_name ?? null;
264
+ _lastToolName = parsed.tool_name ?? null;
265
+ if (parsed.session_id) {
266
+ setSessionId(parsed.session_id);
267
+ }
268
+ }
269
+ }
270
+ } catch {
271
+ // Non-fatal — loadHookInput will return null
272
+ }
273
+ }
274
+
209
275
  /**
210
276
  * Standard hook entry point with lifecycle logging.
211
277
  * See SPEC.md §5.7
@@ -215,23 +281,21 @@ export function runHook(
215
281
  hookName = "unknown",
216
282
  prefetchedInput?: Record<string, any>,
217
283
  ): never {
218
- if (prefetchedInput !== undefined) {
219
- _prefetchedInput = prefetchedInput;
220
- }
284
+ _earlyReadInput(prefetchedInput);
285
+ _cachedHookName = hookName;
221
286
 
222
287
  const startTime = performance.now();
223
288
  const template = detectTemplate();
224
289
  const event = _lastHookEvent ?? "unknown";
225
290
  const tool = _lastToolName;
226
291
 
227
- // HOOK_START
228
292
  const startData: Record<string, any> = {
229
293
  lifecycle: "start",
230
294
  template,
231
295
  event,
232
296
  };
233
297
  if (tool) startData.tool = tool;
234
- logInfo(hookName, "HOOK_START", { data: startData });
298
+ hookLog("info", hookName, "HOOK_START", { data: startData });
235
299
 
236
300
  let exitCode = 0;
237
301
  let status = "success";
@@ -240,17 +304,17 @@ export function runHook(
240
304
  try {
241
305
  const result = mainFunc();
242
306
  exitCode = typeof result === "number" ? result : 0;
243
- status = exitCode !== 0 ? "blocked" : "success";
244
- } catch (e: any) {
245
- if (e instanceof Error && e.message.startsWith("SystemExit:")) {
246
- const code = parseInt(e.message.slice(11), 10);
247
- exitCode = isNaN(code) ? (e.message.slice(11) ? 1 : 0) : code;
307
+ status = exitCode === 0 ? "success" : "blocked";
308
+ } catch (error: any) {
309
+ if (error instanceof Error && error.message.startsWith("SystemExit:")) {
310
+ const code = parseInt(error.message.slice(11), 10);
311
+ exitCode = isNaN(code) ? (error.message.slice(11) ? 1 : 0) : code;
248
312
  status = exitCode !== 0 ? "blocked" : "success";
249
313
  } else {
250
314
  exitCode = 0; // Non-blocking
251
315
  status = "error";
252
- const stack = e instanceof Error ? e.stack ?? "" : "";
253
- errorInfo = [e instanceof Error ? e : new Error(String(e)), stack];
316
+ const stack = error instanceof Error ? error.stack ?? "" : "";
317
+ errorInfo = [error instanceof Error ? error : new Error(String(error)), stack];
254
318
  }
255
319
  }
256
320
 
@@ -268,47 +332,45 @@ export function runHookAsync(
268
332
  hookName = "unknown",
269
333
  prefetchedInput?: Record<string, any>,
270
334
  ): void {
271
- if (prefetchedInput !== undefined) {
272
- _prefetchedInput = prefetchedInput;
273
- }
335
+ _earlyReadInput(prefetchedInput);
336
+ _cachedHookName = hookName;
274
337
 
275
338
  const startTime = performance.now();
276
339
  const template = detectTemplate();
277
340
  const event = _lastHookEvent ?? "unknown";
278
341
  const tool = _lastToolName;
279
342
 
280
- // HOOK_START
281
343
  const startData: Record<string, any> = {
282
344
  lifecycle: "start",
283
345
  template,
284
346
  event,
285
347
  };
286
348
  if (tool) startData.tool = tool;
287
- logInfo(hookName, "HOOK_START", { data: startData });
349
+ hookLog("info", hookName, "HOOK_START", { data: startData });
288
350
 
289
351
  mainFunc()
290
352
  .then((result) => {
291
353
  const exitCode = typeof result === "number" ? result : 0;
292
- _emitHookEnd(hookName, startTime, exitCode, exitCode !== 0 ? "blocked" : "success", null, startData, event, tool, template);
293
- process.exit(exitCode);
354
+ _emitHookEnd(hookName, startTime, exitCode, exitCode === 0 ? "success" : "blocked", null, startData, event, tool, template);
355
+ _drainAndExit(exitCode);
294
356
  })
295
- .catch((e: any) => {
357
+ .catch((error: any) => {
296
358
  let exitCode = 0;
297
359
  let status = "error";
298
360
  let errorInfo: [Error, string] | null = null;
299
361
 
300
- if (e instanceof Error && e.message.startsWith("SystemExit:")) {
301
- const code = parseInt(e.message.slice(11), 10);
302
- exitCode = isNaN(code) ? (e.message.slice(11) ? 1 : 0) : code;
362
+ if (error instanceof Error && error.message.startsWith("SystemExit:")) {
363
+ const code = parseInt(error.message.slice(11), 10);
364
+ exitCode = isNaN(code) ? (error.message.slice(11) ? 1 : 0) : code;
303
365
  status = exitCode !== 0 ? "blocked" : "success";
304
366
  } else {
305
367
  exitCode = 0; // Non-blocking (fail open)
306
- const stack = e instanceof Error ? e.stack ?? "" : "";
307
- errorInfo = [e instanceof Error ? e : new Error(String(e)), stack];
368
+ const stack = error instanceof Error ? error.stack ?? "" : "";
369
+ errorInfo = [error instanceof Error ? error : new Error(String(error)), stack];
308
370
  }
309
371
 
310
372
  _emitHookEnd(hookName, startTime, exitCode, status, errorInfo, startData, event, tool, template);
311
- process.exit(exitCode);
373
+ _drainAndExit(exitCode);
312
374
  });
313
375
  }
314
376
 
@@ -321,17 +383,13 @@ function _emitHookEnd(
321
383
  errorInfo: [Error, string] | null,
322
384
  startData: Record<string, any>,
323
385
  event: string,
324
- tool: string | null,
386
+ tool: null | string,
325
387
  template: string,
326
388
  ): void {
327
- // Retroactive HOOK_START to per-context log
389
+ // Retroactive HOOK_START to per-context log (context_path resolved after main runs)
328
390
  const resolvedAfter = _getContextPath();
329
391
  if (resolvedAfter && fs.existsSync(resolvedAfter)) {
330
- hookLog("info", hookName, "HOOK_START", {
331
- data: startData,
332
- context_path: resolvedAfter,
333
- stderr: false,
334
- });
392
+ hookLog("info", hookName, "HOOK_START", { data: startData });
335
393
  }
336
394
 
337
395
  const durationMs = Math.round((performance.now() - startTime) * 10) / 10;
@@ -350,11 +408,32 @@ function _emitHookEnd(
350
408
  if (errorInfo) {
351
409
  const [err, tb] = errorInfo;
352
410
  endData.error_type = err.constructor.name;
353
- logHookError(hookName, err, endEvent, tb);
354
- logError(hookName, `HOOK_END: ${err}`, { data: endData, traceback_str: tb });
411
+ hookLog("error", hookName, `[${endEvent}] ${err.constructor.name}: ${String(err).replaceAll(/[\n\r]/g, " ").slice(0, 200)}`, { traceback_str: tb });
412
+ hookLog("error", hookName, `HOOK_END: ${err}`, { data: endData, traceback_str: tb });
355
413
  } else if (status === "blocked") {
356
- logWarn(hookName, "HOOK_END", { data: endData });
414
+ hookLog("warn", hookName, "HOOK_END", { data: endData });
357
415
  } else {
358
- logInfo(hookName, "HOOK_END", { data: endData });
416
+ hookLog("info", hookName, "HOOK_END", { data: endData });
359
417
  }
360
418
  }
419
+
420
+ /**
421
+ * Drain stdout before exiting to ensure pipe consumers receive all data.
422
+ * On Windows, stdout to a pipe is fully buffered — process.exit() can
423
+ * discard unflushed data. This waits for the write buffer to drain.
424
+ */
425
+ function _drainAndExit(code: number): void {
426
+ // If stdout is already finished or not writable, exit immediately
427
+ if (!process.stdout.writable || process.stdout.writableFinished) {
428
+ process.exit(code);
429
+ }
430
+
431
+ // Attempt to end stdout and wait for drain
432
+ const timeout = setTimeout(() => process.exit(code), 1000); // safety fallback
433
+ process.stdout.end(() => {
434
+ clearTimeout(timeout);
435
+ process.exit(code);
436
+ });
437
+ }
438
+
439
+ export {hookLog, logBlocking, logDebug, logDiagnostic, logError, logHookError, logInfo, logWarn, setContextPath, setSessionId} from "./logger.js";
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { execFileSync } from "node:child_process";
8
+
8
9
  import { logDebug, logWarn } from "./logger.js";
9
10
  import { STOP_WORDS } from "./stop-words.js";
10
11
  import { cleanTextForSlug } from "./utils.js";
@@ -44,19 +45,25 @@ export function inference(
44
45
 
45
46
  try {
46
47
  const isWin = process.platform === "win32";
47
- let stdout: string;
48
+ // On Windows with shell:true, Node.js sets windowsVerbatimArguments —
49
+ // args are joined with spaces, NOT individually quoted. We must manually
50
+ // wrap multi-word/special-char args in "..." for cmd.exe parsing.
51
+ // Inside double quotes: "" = literal ", and |&<> are safe.
52
+ const empty = isWin ? '""' : "";
53
+ let promptArg = fullPrompt;
54
+ if (isWin) {
55
+ promptArg = '"' + fullPrompt.replaceAll(/\r?\n/g, " ").replaceAll('"', '""') + '"';
56
+ }
48
57
 
49
- // Use execFileSync with shell option on Windows for safe argument passing
50
- // (no string interpolation — avoids command injection)
51
- stdout = execFileSync(
58
+ const stdout = execFileSync(
52
59
  "claude",
53
- ["--model", model, "--print", "--setting-sources", "", "-p", fullPrompt],
60
+ ["--model", model, "--print", "--setting-sources", empty, "-p", promptArg],
54
61
  {
55
62
  timeout: timeoutSec * 1000,
56
63
  env,
57
64
  encoding: "utf-8",
58
65
  stdio: ["pipe", "pipe", "pipe"],
59
- shell: isWin, // Windows needs shell for command resolution
66
+ shell: isWin, // Windows needs shell for .cmd resolution
60
67
  },
61
68
  );
62
69
 
@@ -66,10 +73,10 @@ export function inference(
66
73
  output: stdout.trim(),
67
74
  latency_ms: latencyMs,
68
75
  };
69
- } catch (e: any) {
76
+ } catch (error: any) {
70
77
  const latencyMs = Date.now() - startTime;
71
78
 
72
- if (e.code === "ETIMEDOUT" || e.killed) {
79
+ if (error.code === "ETIMEDOUT" || error.killed) {
73
80
  return {
74
81
  success: false,
75
82
  output: "",
@@ -78,7 +85,7 @@ export function inference(
78
85
  };
79
86
  }
80
87
 
81
- if (e.code === "ENOENT") {
88
+ if (error.code === "ENOENT") {
82
89
  return {
83
90
  success: false,
84
91
  output: "",
@@ -88,11 +95,11 @@ export function inference(
88
95
  }
89
96
 
90
97
  // Non-zero exit code
91
- if (e.status !== undefined && e.status !== 0) {
98
+ if (error.status !== undefined && error.status !== 0) {
92
99
  return {
93
100
  success: false,
94
- output: (e.stdout ?? "").toString().trim(),
95
- error: (e.stderr ?? "").toString().trim() || `Exit code: ${e.status}`,
101
+ output: (error.stdout ?? "").toString().trim(),
102
+ error: (error.stderr ?? "").toString().trim() || `Exit code: ${error.status}`,
96
103
  latency_ms: latencyMs,
97
104
  };
98
105
  }
@@ -100,7 +107,7 @@ export function inference(
100
107
  return {
101
108
  success: false,
102
109
  output: "",
103
- error: String(e),
110
+ error: String(error),
104
111
  latency_ms: latencyMs,
105
112
  };
106
113
  }
@@ -126,13 +133,13 @@ Output ONLY the keywords separated by spaces, nothing else.`;
126
133
  export function generateSemanticSummary(
127
134
  prompt: string,
128
135
  timeout = 15,
129
- ): string | null {
136
+ ): null | string {
130
137
  const result = inference(CONTEXT_ID_SYSTEM_PROMPT, prompt, "standard", timeout);
131
138
 
132
139
  if (!result.success || !result.output) return null;
133
140
 
134
141
  let summary = result.output.trim();
135
- summary = summary.replace(/^["']+|["']+$/g, "");
142
+ summary = summary.replaceAll(/^["']+|["']+$/g, "");
136
143
  summary = summary.replace(/[.!?]+$/, "");
137
144
 
138
145
  // Filter stop words
@@ -187,7 +194,7 @@ Respond with ONLY a JSON object: {"slug": "your 8-12 word phrase here"}`;
187
194
  export function generateContextIdSlug(
188
195
  prompt: string,
189
196
  timeout = 3,
190
- ): string | null {
197
+ ): null | string {
191
198
  const truncated = prompt.slice(0, 500);
192
199
 
193
200
  const result = inference(CONTEXT_ID_SLUG_PROMPT, truncated, "fast", timeout);
@@ -200,7 +207,7 @@ export function generateContextIdSlug(
200
207
  const raw = result.output.trim();
201
208
 
202
209
  // Parse JSON response, fall back to raw text
203
- let slug: string | null = null;
210
+ let slug: null | string = null;
204
211
  try {
205
212
  const parsed = JSON.parse(raw);
206
213
  if (parsed && typeof parsed === "object" && "slug" in parsed) {
@@ -213,11 +220,11 @@ export function generateContextIdSlug(
213
220
  if (!slug) slug = raw;
214
221
 
215
222
  // Clean up
216
- slug = slug.replace(/^["'`]+|["'`]+$/g, "");
223
+ slug = slug.replaceAll(/^["'`]+|["'`]+$/g, "");
217
224
  slug = slug.replace(/[.!?]+$/, "");
218
- slug = slug.replace(/-/g, " ");
219
- slug = slug.replace(/[^a-zA-Z0-9 ]/g, "");
220
- slug = slug.replace(/\s+/g, " ").trim();
225
+ slug = slug.replaceAll('-', " ");
226
+ slug = slug.replaceAll(/[^a-zA-Z0-9 ]/g, "");
227
+ slug = slug.replaceAll(/\s+/g, " ").trim();
221
228
 
222
229
  const words = slug.split(" ");
223
230