oh-my-claude-sisyphus 1.11.1 → 2.0.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 (211) hide show
  1. package/README.md +37 -12
  2. package/dist/__tests__/example.test.d.ts +2 -0
  3. package/dist/__tests__/example.test.d.ts.map +1 -0
  4. package/dist/__tests__/example.test.js +20 -0
  5. package/dist/__tests__/example.test.js.map +1 -0
  6. package/dist/__tests__/hooks.test.d.ts +2 -0
  7. package/dist/__tests__/hooks.test.d.ts.map +1 -0
  8. package/dist/__tests__/hooks.test.js +644 -0
  9. package/dist/__tests__/hooks.test.js.map +1 -0
  10. package/dist/__tests__/installer.test.d.ts +2 -0
  11. package/dist/__tests__/installer.test.d.ts.map +1 -0
  12. package/dist/__tests__/installer.test.js +369 -0
  13. package/dist/__tests__/installer.test.js.map +1 -0
  14. package/dist/__tests__/model-routing.test.d.ts +2 -0
  15. package/dist/__tests__/model-routing.test.d.ts.map +1 -0
  16. package/dist/__tests__/model-routing.test.js +814 -0
  17. package/dist/__tests__/model-routing.test.js.map +1 -0
  18. package/dist/__tests__/skills.test.d.ts +2 -0
  19. package/dist/__tests__/skills.test.d.ts.map +1 -0
  20. package/dist/__tests__/skills.test.js +126 -0
  21. package/dist/__tests__/skills.test.js.map +1 -0
  22. package/dist/__tests__/types.test.d.ts +2 -0
  23. package/dist/__tests__/types.test.d.ts.map +1 -0
  24. package/dist/__tests__/types.test.js +77 -0
  25. package/dist/__tests__/types.test.js.map +1 -0
  26. package/dist/agents/definitions.d.ts +1 -1
  27. package/dist/agents/definitions.d.ts.map +1 -1
  28. package/dist/agents/definitions.js +35 -3
  29. package/dist/agents/definitions.js.map +1 -1
  30. package/dist/agents/index.d.ts +1 -1
  31. package/dist/agents/index.d.ts.map +1 -1
  32. package/dist/agents/index.js +3 -1
  33. package/dist/agents/index.js.map +1 -1
  34. package/dist/agents/oracle.d.ts.map +1 -1
  35. package/dist/agents/oracle.js +43 -1
  36. package/dist/agents/oracle.js.map +1 -1
  37. package/dist/agents/orchestrator-sisyphus.js +2 -2
  38. package/dist/agents/orchestrator-sisyphus.js.map +1 -1
  39. package/dist/cli/index.js +22 -11
  40. package/dist/cli/index.js.map +1 -1
  41. package/dist/config/loader.d.ts.map +1 -1
  42. package/dist/config/loader.js +49 -0
  43. package/dist/config/loader.js.map +1 -1
  44. package/dist/features/auto-update.d.ts.map +1 -1
  45. package/dist/features/auto-update.js +14 -3
  46. package/dist/features/auto-update.js.map +1 -1
  47. package/dist/features/builtin-skills/skills.d.ts.map +1 -1
  48. package/dist/features/builtin-skills/skills.js +0 -1351
  49. package/dist/features/builtin-skills/skills.js.map +1 -1
  50. package/dist/features/index.d.ts +1 -0
  51. package/dist/features/index.d.ts.map +1 -1
  52. package/dist/features/index.js +14 -0
  53. package/dist/features/index.js.map +1 -1
  54. package/dist/features/model-routing/index.d.ts +34 -0
  55. package/dist/features/model-routing/index.d.ts.map +1 -0
  56. package/dist/features/model-routing/index.js +48 -0
  57. package/dist/features/model-routing/index.js.map +1 -0
  58. package/dist/features/model-routing/prompts/haiku.d.ts +54 -0
  59. package/dist/features/model-routing/prompts/haiku.d.ts.map +1 -0
  60. package/dist/features/model-routing/prompts/haiku.js +141 -0
  61. package/dist/features/model-routing/prompts/haiku.js.map +1 -0
  62. package/dist/features/model-routing/prompts/index.d.ts +45 -0
  63. package/dist/features/model-routing/prompts/index.d.ts.map +1 -0
  64. package/dist/features/model-routing/prompts/index.js +116 -0
  65. package/dist/features/model-routing/prompts/index.js.map +1 -0
  66. package/dist/features/model-routing/prompts/opus.d.ts +34 -0
  67. package/dist/features/model-routing/prompts/opus.d.ts.map +1 -0
  68. package/dist/features/model-routing/prompts/opus.js +153 -0
  69. package/dist/features/model-routing/prompts/opus.js.map +1 -0
  70. package/dist/features/model-routing/prompts/sonnet.d.ts +38 -0
  71. package/dist/features/model-routing/prompts/sonnet.d.ts.map +1 -0
  72. package/dist/features/model-routing/prompts/sonnet.js +149 -0
  73. package/dist/features/model-routing/prompts/sonnet.js.map +1 -0
  74. package/dist/features/model-routing/router.d.ts +92 -0
  75. package/dist/features/model-routing/router.d.ts.map +1 -0
  76. package/dist/features/model-routing/router.js +267 -0
  77. package/dist/features/model-routing/router.js.map +1 -0
  78. package/dist/features/model-routing/rules.d.ts +32 -0
  79. package/dist/features/model-routing/rules.d.ts.map +1 -0
  80. package/dist/features/model-routing/rules.js +224 -0
  81. package/dist/features/model-routing/rules.js.map +1 -0
  82. package/dist/features/model-routing/scorer.d.ts +35 -0
  83. package/dist/features/model-routing/scorer.d.ts.map +1 -0
  84. package/dist/features/model-routing/scorer.js +241 -0
  85. package/dist/features/model-routing/scorer.js.map +1 -0
  86. package/dist/features/model-routing/signals.d.ts +26 -0
  87. package/dist/features/model-routing/signals.d.ts.map +1 -0
  88. package/dist/features/model-routing/signals.js +283 -0
  89. package/dist/features/model-routing/signals.js.map +1 -0
  90. package/dist/features/model-routing/types.d.ts +195 -0
  91. package/dist/features/model-routing/types.d.ts.map +1 -0
  92. package/dist/features/model-routing/types.js +86 -0
  93. package/dist/features/model-routing/types.js.map +1 -0
  94. package/dist/hooks/agent-usage-reminder/index.d.ts +1 -1
  95. package/dist/hooks/agent-usage-reminder/index.d.ts.map +1 -1
  96. package/dist/hooks/agent-usage-reminder/index.js +1 -1
  97. package/dist/hooks/agent-usage-reminder/index.js.map +1 -1
  98. package/dist/hooks/auto-slash-command/executor.js.map +1 -1
  99. package/dist/hooks/auto-slash-command/index.d.ts +3 -3
  100. package/dist/hooks/auto-slash-command/index.d.ts.map +1 -1
  101. package/dist/hooks/auto-slash-command/index.js.map +1 -1
  102. package/dist/hooks/background-notification/index.js +1 -1
  103. package/dist/hooks/background-notification/index.js.map +1 -1
  104. package/dist/hooks/bridge.d.ts.map +1 -1
  105. package/dist/hooks/bridge.js.map +1 -1
  106. package/dist/hooks/comment-checker/filters.d.ts +1 -1
  107. package/dist/hooks/comment-checker/filters.d.ts.map +1 -1
  108. package/dist/hooks/comment-checker/filters.js +1 -1
  109. package/dist/hooks/comment-checker/filters.js.map +1 -1
  110. package/dist/hooks/comment-checker/index.js +1 -1
  111. package/dist/hooks/comment-checker/index.js.map +1 -1
  112. package/dist/hooks/context-window-limit-recovery/index.d.ts.map +1 -1
  113. package/dist/hooks/context-window-limit-recovery/index.js.map +1 -1
  114. package/dist/hooks/index.d.ts +3 -3
  115. package/dist/hooks/index.d.ts.map +1 -1
  116. package/dist/hooks/index.js +3 -3
  117. package/dist/hooks/index.js.map +1 -1
  118. package/dist/hooks/keyword-detector/index.d.ts +1 -1
  119. package/dist/hooks/keyword-detector/index.d.ts.map +1 -1
  120. package/dist/hooks/keyword-detector/index.js +1 -1
  121. package/dist/hooks/keyword-detector/index.js.map +1 -1
  122. package/dist/hooks/persistent-mode/index.d.ts.map +1 -1
  123. package/dist/hooks/persistent-mode/index.js.map +1 -1
  124. package/dist/hooks/plugin-patterns/index.d.ts.map +1 -1
  125. package/dist/hooks/plugin-patterns/index.js +12 -9
  126. package/dist/hooks/plugin-patterns/index.js.map +1 -1
  127. package/dist/hooks/preemptive-compaction/index.d.ts +2 -2
  128. package/dist/hooks/preemptive-compaction/index.d.ts.map +1 -1
  129. package/dist/hooks/preemptive-compaction/index.js +1 -11
  130. package/dist/hooks/preemptive-compaction/index.js.map +1 -1
  131. package/dist/hooks/ralph-loop/index.js.map +1 -1
  132. package/dist/hooks/rules-injector/matcher.js +1 -1
  133. package/dist/hooks/rules-injector/matcher.js.map +1 -1
  134. package/dist/hooks/session-recovery/index.d.ts +1 -1
  135. package/dist/hooks/session-recovery/index.d.ts.map +1 -1
  136. package/dist/hooks/session-recovery/index.js +1 -1
  137. package/dist/hooks/session-recovery/index.js.map +1 -1
  138. package/dist/hooks/sisyphus-orchestrator/index.d.ts.map +1 -1
  139. package/dist/hooks/sisyphus-orchestrator/index.js.map +1 -1
  140. package/dist/hooks/ultrawork-state/index.js +1 -1
  141. package/dist/hooks/ultrawork-state/index.js.map +1 -1
  142. package/dist/index.d.ts +2 -2
  143. package/dist/index.d.ts.map +1 -1
  144. package/dist/index.js +4 -2
  145. package/dist/index.js.map +1 -1
  146. package/dist/installer/hooks.d.ts +1 -1
  147. package/dist/installer/hooks.js +1 -1
  148. package/dist/installer/index.d.ts +8 -7
  149. package/dist/installer/index.d.ts.map +1 -1
  150. package/dist/installer/index.js +648 -2141
  151. package/dist/installer/index.js.map +1 -1
  152. package/dist/shared/types.d.ts +25 -0
  153. package/dist/shared/types.d.ts.map +1 -1
  154. package/dist/tools/lsp/servers.d.ts.map +1 -1
  155. package/dist/tools/lsp/servers.js +2 -1
  156. package/dist/tools/lsp/servers.js.map +1 -1
  157. package/package.json +18 -10
  158. package/scripts/install.sh +236 -260
  159. package/scripts/keyword-detector.mjs +209 -0
  160. package/scripts/persistent-mode.mjs +241 -0
  161. package/scripts/post-tool-verifier.mjs +217 -0
  162. package/scripts/pre-tool-enforcer.mjs +99 -0
  163. package/scripts/test-pr25.sh +525 -0
  164. package/dist/agents/model-lists.d.ts +0 -26
  165. package/dist/agents/model-lists.d.ts.map +0 -1
  166. package/dist/agents/model-lists.js +0 -62
  167. package/dist/agents/model-lists.js.map +0 -1
  168. package/dist/auth/index.d.ts +0 -10
  169. package/dist/auth/index.d.ts.map +0 -1
  170. package/dist/auth/index.js +0 -13
  171. package/dist/auth/index.js.map +0 -1
  172. package/dist/auth/manager.d.ts +0 -54
  173. package/dist/auth/manager.d.ts.map +0 -1
  174. package/dist/auth/manager.js +0 -248
  175. package/dist/auth/manager.js.map +0 -1
  176. package/dist/auth/oauth-google.d.ts +0 -47
  177. package/dist/auth/oauth-google.d.ts.map +0 -1
  178. package/dist/auth/oauth-google.js +0 -280
  179. package/dist/auth/oauth-google.js.map +0 -1
  180. package/dist/auth/oauth-openai.d.ts +0 -46
  181. package/dist/auth/oauth-openai.d.ts.map +0 -1
  182. package/dist/auth/oauth-openai.js +0 -264
  183. package/dist/auth/oauth-openai.js.map +0 -1
  184. package/dist/auth/pkce.d.ts +0 -14
  185. package/dist/auth/pkce.d.ts.map +0 -1
  186. package/dist/auth/pkce.js +0 -35
  187. package/dist/auth/pkce.js.map +0 -1
  188. package/dist/auth/storage.d.ts +0 -52
  189. package/dist/auth/storage.d.ts.map +0 -1
  190. package/dist/auth/storage.js +0 -230
  191. package/dist/auth/storage.js.map +0 -1
  192. package/dist/auth/types.d.ts +0 -76
  193. package/dist/auth/types.d.ts.map +0 -1
  194. package/dist/auth/types.js +0 -5
  195. package/dist/auth/types.js.map +0 -1
  196. package/dist/providers/index.d.ts +0 -8
  197. package/dist/providers/index.d.ts.map +0 -1
  198. package/dist/providers/index.js +0 -10
  199. package/dist/providers/index.js.map +0 -1
  200. package/dist/providers/registry.d.ts +0 -29
  201. package/dist/providers/registry.d.ts.map +0 -1
  202. package/dist/providers/registry.js +0 -162
  203. package/dist/providers/registry.js.map +0 -1
  204. package/dist/providers/router.d.ts +0 -40
  205. package/dist/providers/router.d.ts.map +0 -1
  206. package/dist/providers/router.js +0 -88
  207. package/dist/providers/router.js.map +0 -1
  208. package/dist/providers/types.d.ts +0 -92
  209. package/dist/providers/types.d.ts.map +0 -1
  210. package/dist/providers/types.js +0 -27
  211. package/dist/providers/types.js.map +0 -1
@@ -0,0 +1,644 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { extractPromptText, removeCodeBlocks, detectKeywordsWithType, hasKeyword, getPrimaryKeyword } from '../hooks/keyword-detector/index.js';
3
+ import { formatTodoStatus, getNextPendingTodo } from '../hooks/todo-continuation/index.js';
4
+ describe('Keyword Detector', () => {
5
+ describe('extractPromptText', () => {
6
+ it('should extract text from text parts', () => {
7
+ const parts = [
8
+ { type: 'text', text: 'Hello world' },
9
+ { type: 'text', text: 'How are you?' }
10
+ ];
11
+ expect(extractPromptText(parts)).toBe('Hello world How are you?');
12
+ });
13
+ it('should filter out non-text parts', () => {
14
+ const parts = [
15
+ { type: 'text', text: 'Hello' },
16
+ { type: 'image', url: 'test.jpg' },
17
+ { type: 'text', text: 'world' }
18
+ ];
19
+ expect(extractPromptText(parts)).toBe('Hello world');
20
+ });
21
+ it('should handle empty parts array', () => {
22
+ expect(extractPromptText([])).toBe('');
23
+ });
24
+ it('should handle parts without text', () => {
25
+ const parts = [
26
+ { type: 'text' },
27
+ { type: 'text', text: undefined }
28
+ ];
29
+ expect(extractPromptText(parts)).toBe('');
30
+ });
31
+ it('should join multiple text parts with space', () => {
32
+ const parts = [
33
+ { type: 'text', text: 'analyze' },
34
+ { type: 'text', text: 'this' },
35
+ { type: 'text', text: 'code' }
36
+ ];
37
+ expect(extractPromptText(parts)).toBe('analyze this code');
38
+ });
39
+ });
40
+ describe('removeCodeBlocks', () => {
41
+ it('should remove triple backtick fenced code blocks', () => {
42
+ const text = 'Some text\n```javascript\nconst x = 1;\n```\nMore text';
43
+ const result = removeCodeBlocks(text);
44
+ expect(result).not.toContain('const x = 1');
45
+ expect(result).toContain('Some text');
46
+ expect(result).toContain('More text');
47
+ });
48
+ it('should remove tilde fenced code blocks', () => {
49
+ const text = 'Before\n~~~python\nprint("hello")\n~~~\nAfter';
50
+ const result = removeCodeBlocks(text);
51
+ expect(result).not.toContain('print("hello")');
52
+ expect(result).toContain('Before');
53
+ expect(result).toContain('After');
54
+ });
55
+ it('should remove inline code with single backticks', () => {
56
+ const text = 'Use `analyze` command here';
57
+ const result = removeCodeBlocks(text);
58
+ expect(result).not.toContain('`analyze`');
59
+ expect(result).toContain('Use');
60
+ expect(result).toContain('command here');
61
+ });
62
+ it('should handle multiple code blocks', () => {
63
+ const text = '```js\ncode1\n```\ntext\n```ts\ncode2\n```';
64
+ const result = removeCodeBlocks(text);
65
+ expect(result).not.toContain('code1');
66
+ expect(result).not.toContain('code2');
67
+ expect(result).toContain('text');
68
+ });
69
+ it('should handle text without code blocks', () => {
70
+ const text = 'Just plain text here';
71
+ expect(removeCodeBlocks(text)).toBe(text);
72
+ });
73
+ it('should handle empty string', () => {
74
+ expect(removeCodeBlocks('')).toBe('');
75
+ });
76
+ it('should handle nested inline code', () => {
77
+ const text = 'Text with `inline` and `another` code';
78
+ const result = removeCodeBlocks(text);
79
+ expect(result).not.toContain('`');
80
+ expect(result).toContain('Text with');
81
+ expect(result).toContain('and');
82
+ expect(result).toContain('code');
83
+ });
84
+ });
85
+ describe('detectKeywordsWithType', () => {
86
+ it('should detect ultrawork keyword', () => {
87
+ const detected = detectKeywordsWithType('I need ultrawork mode');
88
+ expect(detected).toHaveLength(1);
89
+ expect(detected[0].type).toBe('ultrawork');
90
+ expect(detected[0].keyword).toBe('ultrawork');
91
+ });
92
+ it('should detect ulw abbreviation', () => {
93
+ const detected = detectKeywordsWithType('Use ulw for this task');
94
+ expect(detected).toHaveLength(1);
95
+ expect(detected[0].type).toBe('ultrawork');
96
+ expect(detected[0].keyword).toBe('ulw');
97
+ });
98
+ it('should detect ultrathink keyword', () => {
99
+ const detected = detectKeywordsWithType('I need to ultrathink this');
100
+ expect(detected).toHaveLength(1);
101
+ expect(detected[0].type).toBe('ultrathink');
102
+ expect(detected[0].keyword).toBe('ultrathink');
103
+ });
104
+ it('should detect think keyword', () => {
105
+ const detected = detectKeywordsWithType('Let me think about it');
106
+ expect(detected).toHaveLength(1);
107
+ expect(detected[0].type).toBe('ultrathink');
108
+ expect(detected[0].keyword).toBe('think');
109
+ });
110
+ it('should detect search keywords', () => {
111
+ const searchTerms = ['search', 'find', 'locate', 'lookup', 'explore'];
112
+ for (const term of searchTerms) {
113
+ const detected = detectKeywordsWithType(`Please ${term} this file`);
114
+ expect(detected).toHaveLength(1);
115
+ expect(detected[0].type).toBe('search');
116
+ expect(detected[0].keyword).toBe(term);
117
+ }
118
+ });
119
+ it('should detect search patterns', () => {
120
+ const patterns = [
121
+ 'where is the config',
122
+ 'show me all files',
123
+ 'list all functions'
124
+ ];
125
+ for (const pattern of patterns) {
126
+ const detected = detectKeywordsWithType(pattern);
127
+ expect(detected.length).toBeGreaterThan(0);
128
+ const hasSearchType = detected.some(d => d.type === 'search');
129
+ expect(hasSearchType).toBe(true);
130
+ }
131
+ });
132
+ it('should detect analyze keywords', () => {
133
+ const analyzeTerms = ['analyze', 'investigate', 'examine', 'debug'];
134
+ for (const term of analyzeTerms) {
135
+ const detected = detectKeywordsWithType(`Please ${term} this code`);
136
+ expect(detected).toHaveLength(1);
137
+ expect(detected[0].type).toBe('analyze');
138
+ expect(detected[0].keyword).toBe(term);
139
+ }
140
+ });
141
+ it('should detect analyze patterns', () => {
142
+ const patterns = [
143
+ 'why is this failing',
144
+ 'how does this work',
145
+ 'how to implement this'
146
+ ];
147
+ for (const pattern of patterns) {
148
+ const detected = detectKeywordsWithType(pattern);
149
+ expect(detected.length).toBeGreaterThan(0);
150
+ const hasAnalyzeType = detected.some(d => d.type === 'analyze');
151
+ expect(hasAnalyzeType).toBe(true);
152
+ }
153
+ });
154
+ it('should be case insensitive', () => {
155
+ const variants = ['ULTRAWORK', 'UltraWork', 'uLtRaWoRk'];
156
+ for (const variant of variants) {
157
+ const detected = detectKeywordsWithType(variant);
158
+ expect(detected).toHaveLength(1);
159
+ expect(detected[0].type).toBe('ultrawork');
160
+ }
161
+ });
162
+ it('should respect word boundaries', () => {
163
+ // Should not match partial words
164
+ const text = 'multiwork is not ultrawork';
165
+ const detected = detectKeywordsWithType(text);
166
+ expect(detected).toHaveLength(1);
167
+ expect(detected[0].keyword).toBe('ultrawork');
168
+ });
169
+ it('should include position information', () => {
170
+ const detected = detectKeywordsWithType('Start search here');
171
+ expect(detected[0].position).toBe(6); // Position of 'search'
172
+ });
173
+ it('should return empty array for no matches', () => {
174
+ const detected = detectKeywordsWithType('Just plain text');
175
+ expect(detected).toEqual([]);
176
+ });
177
+ it('should detect multiple different keyword types', () => {
178
+ const text = 'search and analyze this code';
179
+ const detected = detectKeywordsWithType(text);
180
+ expect(detected.length).toBeGreaterThanOrEqual(2);
181
+ const types = detected.map(d => d.type);
182
+ expect(types).toContain('search');
183
+ expect(types).toContain('analyze');
184
+ });
185
+ });
186
+ describe('hasKeyword', () => {
187
+ it('should return true when keyword exists', () => {
188
+ expect(hasKeyword('use ultrawork mode')).toBe(true);
189
+ expect(hasKeyword('search for files')).toBe(true);
190
+ expect(hasKeyword('analyze this')).toBe(true);
191
+ });
192
+ it('should return false when no keyword exists', () => {
193
+ expect(hasKeyword('just normal text')).toBe(false);
194
+ expect(hasKeyword('hello world')).toBe(false);
195
+ });
196
+ it('should ignore keywords in code blocks', () => {
197
+ const text = 'Normal text\n```\nsearch in code\n```\nMore text';
198
+ expect(hasKeyword(text)).toBe(false);
199
+ });
200
+ it('should detect keywords outside code blocks', () => {
201
+ const text = 'Please search\n```\nsome code\n```\nfor this';
202
+ expect(hasKeyword(text)).toBe(true);
203
+ });
204
+ it('should handle empty string', () => {
205
+ expect(hasKeyword('')).toBe(false);
206
+ });
207
+ });
208
+ describe('getPrimaryKeyword', () => {
209
+ it('should return highest priority keyword', () => {
210
+ // ultrawork has highest priority
211
+ const text = 'search and analyze with ultrawork';
212
+ const primary = getPrimaryKeyword(text);
213
+ expect(primary).not.toBeNull();
214
+ expect(primary.type).toBe('ultrawork');
215
+ });
216
+ it('should return ultrathink when no ultrawork', () => {
217
+ const text = 'search and think about this';
218
+ const primary = getPrimaryKeyword(text);
219
+ expect(primary).not.toBeNull();
220
+ expect(primary.type).toBe('ultrathink');
221
+ });
222
+ it('should return search when only search keyword', () => {
223
+ const text = 'find all files';
224
+ const primary = getPrimaryKeyword(text);
225
+ expect(primary).not.toBeNull();
226
+ expect(primary.type).toBe('search');
227
+ });
228
+ it('should return analyze when only analyze keyword', () => {
229
+ const text = 'investigate this issue';
230
+ const primary = getPrimaryKeyword(text);
231
+ expect(primary).not.toBeNull();
232
+ expect(primary.type).toBe('analyze');
233
+ });
234
+ it('should return null when no keywords', () => {
235
+ const primary = getPrimaryKeyword('just normal text');
236
+ expect(primary).toBeNull();
237
+ });
238
+ it('should ignore code blocks', () => {
239
+ const text = '```\nultrawork code\n```\nsearch this';
240
+ const primary = getPrimaryKeyword(text);
241
+ expect(primary).not.toBeNull();
242
+ expect(primary.type).toBe('search');
243
+ });
244
+ it('should return first detected when same priority', () => {
245
+ // Both search and analyze have same priority
246
+ const text = 'search and analyze';
247
+ const primary = getPrimaryKeyword(text);
248
+ expect(primary).not.toBeNull();
249
+ // Should return search as it comes first in priority list
250
+ expect(primary.type).toBe('search');
251
+ });
252
+ });
253
+ });
254
+ describe('Todo Continuation', () => {
255
+ describe('formatTodoStatus', () => {
256
+ it('should format when all tasks complete', () => {
257
+ const result = {
258
+ count: 0,
259
+ todos: [],
260
+ total: 5
261
+ };
262
+ expect(formatTodoStatus(result)).toBe('All tasks complete (5 total)');
263
+ });
264
+ it('should format with incomplete tasks', () => {
265
+ const result = {
266
+ count: 3,
267
+ todos: [],
268
+ total: 10
269
+ };
270
+ expect(formatTodoStatus(result)).toBe('7/10 completed, 3 remaining');
271
+ });
272
+ it('should handle zero total tasks', () => {
273
+ const result = {
274
+ count: 0,
275
+ todos: [],
276
+ total: 0
277
+ };
278
+ expect(formatTodoStatus(result)).toBe('All tasks complete (0 total)');
279
+ });
280
+ it('should handle all tasks incomplete', () => {
281
+ const result = {
282
+ count: 5,
283
+ todos: [],
284
+ total: 5
285
+ };
286
+ expect(formatTodoStatus(result)).toBe('0/5 completed, 5 remaining');
287
+ });
288
+ it('should handle single task remaining', () => {
289
+ const result = {
290
+ count: 1,
291
+ todos: [],
292
+ total: 10
293
+ };
294
+ expect(formatTodoStatus(result)).toBe('9/10 completed, 1 remaining');
295
+ });
296
+ });
297
+ describe('getNextPendingTodo', () => {
298
+ it('should return in_progress todo first', () => {
299
+ const todos = [
300
+ { content: 'Task 1', status: 'pending' },
301
+ { content: 'Task 2', status: 'in_progress' },
302
+ { content: 'Task 3', status: 'pending' }
303
+ ];
304
+ const result = {
305
+ count: 3,
306
+ todos,
307
+ total: 3
308
+ };
309
+ const next = getNextPendingTodo(result);
310
+ expect(next).not.toBeNull();
311
+ expect(next.content).toBe('Task 2');
312
+ expect(next.status).toBe('in_progress');
313
+ });
314
+ it('should return first pending when no in_progress', () => {
315
+ const todos = [
316
+ { content: 'Task 1', status: 'pending' },
317
+ { content: 'Task 2', status: 'pending' },
318
+ { content: 'Task 3', status: 'completed' }
319
+ ];
320
+ const result = {
321
+ count: 2,
322
+ todos: todos.filter(t => t.status !== 'completed'),
323
+ total: 3
324
+ };
325
+ const next = getNextPendingTodo(result);
326
+ expect(next).not.toBeNull();
327
+ expect(next.content).toBe('Task 1');
328
+ expect(next.status).toBe('pending');
329
+ });
330
+ it('should return null when no todos', () => {
331
+ const result = {
332
+ count: 0,
333
+ todos: [],
334
+ total: 0
335
+ };
336
+ const next = getNextPendingTodo(result);
337
+ expect(next).toBeNull();
338
+ });
339
+ it('should return null when all completed', () => {
340
+ const result = {
341
+ count: 0,
342
+ todos: [],
343
+ total: 3
344
+ };
345
+ const next = getNextPendingTodo(result);
346
+ expect(next).toBeNull();
347
+ });
348
+ it('should handle todos with priority field', () => {
349
+ const todos = [
350
+ { content: 'Task 1', status: 'pending', priority: 'low' },
351
+ { content: 'Task 2', status: 'in_progress', priority: 'high' }
352
+ ];
353
+ const result = {
354
+ count: 2,
355
+ todos,
356
+ total: 2
357
+ };
358
+ const next = getNextPendingTodo(result);
359
+ expect(next).not.toBeNull();
360
+ expect(next.content).toBe('Task 2');
361
+ });
362
+ it('should handle todos with id field', () => {
363
+ const todos = [
364
+ { content: 'Task 1', status: 'pending', id: 'todo-1' },
365
+ { content: 'Task 2', status: 'pending', id: 'todo-2' }
366
+ ];
367
+ const result = {
368
+ count: 2,
369
+ todos,
370
+ total: 2
371
+ };
372
+ const next = getNextPendingTodo(result);
373
+ expect(next).not.toBeNull();
374
+ expect(next.id).toBe('todo-1');
375
+ });
376
+ it('should ignore cancelled todos', () => {
377
+ const todos = [
378
+ { content: 'Task 1', status: 'cancelled' },
379
+ { content: 'Task 2', status: 'pending' }
380
+ ];
381
+ const result = {
382
+ count: 1,
383
+ todos: [todos[1]],
384
+ total: 2
385
+ };
386
+ const next = getNextPendingTodo(result);
387
+ expect(next).not.toBeNull();
388
+ expect(next.content).toBe('Task 2');
389
+ });
390
+ it('should prefer in_progress over multiple pending', () => {
391
+ const todos = [
392
+ { content: 'Task 1', status: 'pending' },
393
+ { content: 'Task 2', status: 'pending' },
394
+ { content: 'Task 3', status: 'pending' },
395
+ { content: 'Task 4', status: 'in_progress' }
396
+ ];
397
+ const result = {
398
+ count: 4,
399
+ todos,
400
+ total: 4
401
+ };
402
+ const next = getNextPendingTodo(result);
403
+ expect(next).not.toBeNull();
404
+ expect(next.content).toBe('Task 4');
405
+ expect(next.status).toBe('in_progress');
406
+ });
407
+ });
408
+ describe('Todo type validation', () => {
409
+ it('should handle all valid status values', () => {
410
+ const statuses = ['pending', 'in_progress', 'completed', 'cancelled'];
411
+ const todos = statuses.map((status, i) => ({
412
+ content: `Task ${i + 1}`,
413
+ status
414
+ }));
415
+ expect(todos).toHaveLength(4);
416
+ todos.forEach(todo => {
417
+ expect(todo.content).toBeTruthy();
418
+ expect(statuses).toContain(todo.status);
419
+ });
420
+ });
421
+ it('should handle optional fields', () => {
422
+ const todo = {
423
+ content: 'Test task',
424
+ status: 'pending',
425
+ priority: 'high',
426
+ id: 'test-123'
427
+ };
428
+ expect(todo.content).toBe('Test task');
429
+ expect(todo.status).toBe('pending');
430
+ expect(todo.priority).toBe('high');
431
+ expect(todo.id).toBe('test-123');
432
+ });
433
+ it('should handle minimal todo object', () => {
434
+ const todo = {
435
+ content: 'Minimal task',
436
+ status: 'pending'
437
+ };
438
+ expect(todo.content).toBe('Minimal task');
439
+ expect(todo.status).toBe('pending');
440
+ expect(todo.priority).toBeUndefined();
441
+ expect(todo.id).toBeUndefined();
442
+ });
443
+ });
444
+ describe('IncompleteTodosResult validation', () => {
445
+ it('should maintain consistency between count and todos length', () => {
446
+ const todos = [
447
+ { content: 'Task 1', status: 'pending' },
448
+ { content: 'Task 2', status: 'in_progress' }
449
+ ];
450
+ const result = {
451
+ count: todos.length,
452
+ todos,
453
+ total: 5
454
+ };
455
+ expect(result.count).toBe(result.todos.length);
456
+ expect(result.total).toBeGreaterThanOrEqual(result.count);
457
+ });
458
+ it('should handle edge case of more completed than total', () => {
459
+ // This shouldn't happen in practice, but test the type structure
460
+ const result = {
461
+ count: 0,
462
+ todos: [],
463
+ total: 3
464
+ };
465
+ expect(result.count).toBeLessThanOrEqual(result.total);
466
+ });
467
+ });
468
+ });
469
+ describe('Hook Output Structure', () => {
470
+ describe('JSON output format', () => {
471
+ it('should create valid hook output with continue flag', () => {
472
+ const output = {
473
+ continue: true,
474
+ message: 'Test message'
475
+ };
476
+ expect(output).toHaveProperty('continue');
477
+ expect(output).toHaveProperty('message');
478
+ expect(typeof output.continue).toBe('boolean');
479
+ expect(typeof output.message).toBe('string');
480
+ });
481
+ it('should create valid hook output without message', () => {
482
+ const output = {
483
+ continue: false
484
+ };
485
+ expect(output).toHaveProperty('continue');
486
+ expect(output.continue).toBe(false);
487
+ });
488
+ it('should serialize to valid JSON', () => {
489
+ const output = {
490
+ continue: true,
491
+ message: 'ULTRAWORK MODE ACTIVATED'
492
+ };
493
+ const json = JSON.stringify(output);
494
+ const parsed = JSON.parse(json);
495
+ expect(parsed.continue).toBe(true);
496
+ expect(parsed.message).toBe('ULTRAWORK MODE ACTIVATED');
497
+ });
498
+ it('should handle multiline messages', () => {
499
+ const output = {
500
+ continue: true,
501
+ message: 'Line 1\nLine 2\nLine 3'
502
+ };
503
+ const json = JSON.stringify(output);
504
+ const parsed = JSON.parse(json);
505
+ expect(parsed.message).toContain('\n');
506
+ expect(parsed.message.split('\n')).toHaveLength(3);
507
+ });
508
+ it('should handle empty message', () => {
509
+ const output = {
510
+ continue: true,
511
+ message: ''
512
+ };
513
+ expect(output.message).toBe('');
514
+ });
515
+ it('should handle special characters in message', () => {
516
+ const output = {
517
+ continue: true,
518
+ message: 'Message with "quotes" and \'apostrophes\' and \\ backslashes'
519
+ };
520
+ const json = JSON.stringify(output);
521
+ const parsed = JSON.parse(json);
522
+ expect(parsed.message).toBe(output.message);
523
+ });
524
+ });
525
+ describe('Hook message formatting', () => {
526
+ it('should format continuation message', () => {
527
+ const message = '[SYSTEM REMINDER - TODO CONTINUATION] Incomplete tasks remain. Continue working.';
528
+ expect(message).toContain('[SYSTEM REMINDER');
529
+ expect(message).toContain('TODO CONTINUATION');
530
+ expect(message).toContain('Continue working');
531
+ });
532
+ it('should format keyword detection message', () => {
533
+ const keyword = {
534
+ type: 'ultrawork',
535
+ keyword: 'ultrawork',
536
+ position: 0
537
+ };
538
+ const message = `ULTRAWORK MODE ACTIVATED - Detected keyword: ${keyword.keyword}`;
539
+ expect(message).toContain('ULTRAWORK MODE');
540
+ expect(message).toContain(keyword.keyword);
541
+ });
542
+ it('should format todo status message', () => {
543
+ const result = {
544
+ count: 2,
545
+ todos: [],
546
+ total: 5
547
+ };
548
+ const status = formatTodoStatus(result);
549
+ const message = `Todo Status: ${status}`;
550
+ expect(message).toContain('3/5 completed');
551
+ expect(message).toContain('2 remaining');
552
+ });
553
+ });
554
+ });
555
+ describe('Integration: Keyword Detection with Code Blocks', () => {
556
+ it('should detect keywords outside code and ignore inside', () => {
557
+ const text = `
558
+ Please search for files
559
+
560
+ \`\`\`javascript
561
+ // This search should be ignored
562
+ function search() {
563
+ return analyze();
564
+ }
565
+ \`\`\`
566
+
567
+ Now analyze the results
568
+ `;
569
+ const detected = detectKeywordsWithType(removeCodeBlocks(text));
570
+ const types = detected.map(d => d.type);
571
+ expect(types).toContain('search');
572
+ expect(types).toContain('analyze');
573
+ // Should only detect the ones outside code blocks
574
+ expect(detected.filter(d => d.type === 'search')).toHaveLength(1);
575
+ expect(detected.filter(d => d.type === 'analyze')).toHaveLength(1);
576
+ });
577
+ it('should handle inline code with keywords', () => {
578
+ const text = 'Use the `search` command to find files';
579
+ const cleanText = removeCodeBlocks(text);
580
+ const detected = detectKeywordsWithType(cleanText);
581
+ // The word 'find' should still be detected
582
+ expect(detected.some(d => d.type === 'search')).toBe(true);
583
+ });
584
+ it('should prioritize ultrawork even with other keywords', () => {
585
+ const text = 'search, analyze, and use ultrawork mode';
586
+ const primary = getPrimaryKeyword(text);
587
+ expect(primary).not.toBeNull();
588
+ expect(primary.type).toBe('ultrawork');
589
+ expect(primary.keyword).toBe('ultrawork');
590
+ });
591
+ });
592
+ describe('Edge Cases', () => {
593
+ describe('Empty and null inputs', () => {
594
+ it('should handle empty prompt parts', () => {
595
+ expect(extractPromptText([])).toBe('');
596
+ });
597
+ it('should handle empty text in removeCodeBlocks', () => {
598
+ expect(removeCodeBlocks('')).toBe('');
599
+ });
600
+ it('should handle empty text in detectKeywordsWithType', () => {
601
+ expect(detectKeywordsWithType('')).toEqual([]);
602
+ });
603
+ it('should handle empty text in hasKeyword', () => {
604
+ expect(hasKeyword('')).toBe(false);
605
+ });
606
+ it('should handle empty text in getPrimaryKeyword', () => {
607
+ expect(getPrimaryKeyword('')).toBeNull();
608
+ });
609
+ });
610
+ describe('Whitespace handling', () => {
611
+ it('should detect keywords with extra whitespace', () => {
612
+ const text = ' search for files ';
613
+ expect(hasKeyword(text)).toBe(true);
614
+ });
615
+ it('should handle newlines and tabs', () => {
616
+ const text = 'search\n\tfor\r\nfiles';
617
+ const detected = detectKeywordsWithType(text);
618
+ expect(detected.some(d => d.type === 'search')).toBe(true);
619
+ });
620
+ });
621
+ describe('Unicode and special characters', () => {
622
+ it('should handle unicode characters', () => {
623
+ const text = 'search for files with émojis 🔍';
624
+ expect(hasKeyword(text)).toBe(true);
625
+ });
626
+ it('should handle mixed scripts', () => {
627
+ const text = 'Please search 搜索 искать';
628
+ const detected = detectKeywordsWithType(text);
629
+ expect(detected.some(d => d.keyword === 'search')).toBe(true);
630
+ });
631
+ });
632
+ describe('Very long inputs', () => {
633
+ it('should handle long text efficiently', () => {
634
+ const longText = 'plain text '.repeat(1000) + ' search here';
635
+ expect(hasKeyword(longText)).toBe(true);
636
+ });
637
+ it('should handle many code blocks', () => {
638
+ const manyBlocks = '```code```\n'.repeat(100) + 'search here';
639
+ const cleaned = removeCodeBlocks(manyBlocks);
640
+ expect(hasKeyword(cleaned)).toBe(true);
641
+ });
642
+ });
643
+ });
644
+ //# sourceMappingURL=hooks.test.js.map