heraspec 0.1.12 → 0.1.14

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 (129) hide show
  1. package/LICENSE +22 -22
  2. package/README.md +188 -103
  3. package/bin/heraspec.js +4805 -1122
  4. package/bin/heraspec.js.map +4 -4
  5. package/dist/core/templates/skills/CHANGELOG.md +117 -117
  6. package/dist/core/templates/skills/README-template.md +58 -58
  7. package/dist/core/templates/skills/README.md +38 -38
  8. package/dist/core/templates/skills/content-optimization-skill.md +104 -104
  9. package/dist/core/templates/skills/data/design-systems.csv +54 -0
  10. package/dist/core/templates/skills/data/pages-proposed.csv +21 -21
  11. package/dist/core/templates/skills/data/pages.csv +9 -9
  12. package/dist/core/templates/skills/data/typography.csv +57 -57
  13. package/dist/core/templates/skills/deploy-documentation-skill.md +408 -0
  14. package/dist/core/templates/skills/design-system-skill.md +176 -0
  15. package/dist/core/templates/skills/documents/templates/documentation-landing-page.html +63 -63
  16. package/dist/core/templates/skills/documents/templates/documentation.html +49 -49
  17. package/dist/core/templates/skills/documents/templates/landing-script.js +38 -38
  18. package/dist/core/templates/skills/documents/templates/landing-style.css +158 -158
  19. package/dist/core/templates/skills/documents/templates/script.js +56 -56
  20. package/dist/core/templates/skills/documents/templates/style.css +155 -155
  21. package/dist/core/templates/skills/documents/templates/technical-doc-template.md +16 -16
  22. package/dist/core/templates/skills/documents/templates/user-guide-template.md +16 -16
  23. package/dist/core/templates/skills/documents-skill.md +104 -104
  24. package/dist/core/templates/skills/e2e-test-skill.md +119 -119
  25. package/dist/core/templates/skills/git-embed-skill.md +57 -0
  26. package/dist/core/templates/skills/integration-test-skill.md +118 -118
  27. package/dist/core/templates/skills/knowledge/README.md +63 -0
  28. package/dist/core/templates/skills/knowledge/design-systems/airbnb/DESIGN.md +246 -0
  29. package/dist/core/templates/skills/knowledge/design-systems/airtable/DESIGN.md +89 -0
  30. package/dist/core/templates/skills/knowledge/design-systems/apple/DESIGN.md +313 -0
  31. package/dist/core/templates/skills/knowledge/design-systems/bmw/DESIGN.md +180 -0
  32. package/dist/core/templates/skills/knowledge/design-systems/cal/DESIGN.md +259 -0
  33. package/dist/core/templates/skills/knowledge/design-systems/claude/DESIGN.md +312 -0
  34. package/dist/core/templates/skills/knowledge/design-systems/clay/DESIGN.md +304 -0
  35. package/dist/core/templates/skills/knowledge/design-systems/clickhouse/DESIGN.md +281 -0
  36. package/dist/core/templates/skills/knowledge/design-systems/cohere/DESIGN.md +266 -0
  37. package/dist/core/templates/skills/knowledge/design-systems/coinbase/DESIGN.md +129 -0
  38. package/dist/core/templates/skills/knowledge/design-systems/composio/DESIGN.md +307 -0
  39. package/dist/core/templates/skills/knowledge/design-systems/cursor/DESIGN.md +309 -0
  40. package/dist/core/templates/skills/knowledge/design-systems/elevenlabs/DESIGN.md +265 -0
  41. package/dist/core/templates/skills/knowledge/design-systems/expo/DESIGN.md +281 -0
  42. package/dist/core/templates/skills/knowledge/design-systems/figma/DESIGN.md +220 -0
  43. package/dist/core/templates/skills/knowledge/design-systems/framer/DESIGN.md +246 -0
  44. package/dist/core/templates/skills/knowledge/design-systems/hashicorp/DESIGN.md +278 -0
  45. package/dist/core/templates/skills/knowledge/design-systems/ibm/DESIGN.md +332 -0
  46. package/dist/core/templates/skills/knowledge/design-systems/index.json +72 -0
  47. package/dist/core/templates/skills/knowledge/design-systems/intercom/DESIGN.md +146 -0
  48. package/dist/core/templates/skills/knowledge/design-systems/kraken/DESIGN.md +125 -0
  49. package/dist/core/templates/skills/knowledge/design-systems/linear.app/DESIGN.md +367 -0
  50. package/dist/core/templates/skills/knowledge/design-systems/lovable/DESIGN.md +298 -0
  51. package/dist/core/templates/skills/knowledge/design-systems/minimax/DESIGN.md +257 -0
  52. package/dist/core/templates/skills/knowledge/design-systems/mintlify/DESIGN.md +326 -0
  53. package/dist/core/templates/skills/knowledge/design-systems/miro/DESIGN.md +108 -0
  54. package/dist/core/templates/skills/knowledge/design-systems/mistral.ai/DESIGN.md +261 -0
  55. package/dist/core/templates/skills/knowledge/design-systems/mongodb/DESIGN.md +266 -0
  56. package/dist/core/templates/skills/knowledge/design-systems/notion/DESIGN.md +309 -0
  57. package/dist/core/templates/skills/knowledge/design-systems/nvidia/DESIGN.md +293 -0
  58. package/dist/core/templates/skills/knowledge/design-systems/ollama/DESIGN.md +267 -0
  59. package/dist/core/templates/skills/knowledge/design-systems/opencode.ai/DESIGN.md +281 -0
  60. package/dist/core/templates/skills/knowledge/design-systems/pinterest/DESIGN.md +230 -0
  61. package/dist/core/templates/skills/knowledge/design-systems/posthog/DESIGN.md +256 -0
  62. package/dist/core/templates/skills/knowledge/design-systems/raycast/DESIGN.md +268 -0
  63. package/dist/core/templates/skills/knowledge/design-systems/replicate/DESIGN.md +261 -0
  64. package/dist/core/templates/skills/knowledge/design-systems/resend/DESIGN.md +303 -0
  65. package/dist/core/templates/skills/knowledge/design-systems/revolut/DESIGN.md +185 -0
  66. package/dist/core/templates/skills/knowledge/design-systems/runwayml/DESIGN.md +244 -0
  67. package/dist/core/templates/skills/knowledge/design-systems/sanity/DESIGN.md +357 -0
  68. package/dist/core/templates/skills/knowledge/design-systems/sentry/DESIGN.md +262 -0
  69. package/dist/core/templates/skills/knowledge/design-systems/spacex/DESIGN.md +194 -0
  70. package/dist/core/templates/skills/knowledge/design-systems/spotify/DESIGN.md +246 -0
  71. package/dist/core/templates/skills/knowledge/design-systems/stripe/DESIGN.md +322 -0
  72. package/dist/core/templates/skills/knowledge/design-systems/supabase/DESIGN.md +255 -0
  73. package/dist/core/templates/skills/knowledge/design-systems/superhuman/DESIGN.md +252 -0
  74. package/dist/core/templates/skills/knowledge/design-systems/together.ai/DESIGN.md +263 -0
  75. package/dist/core/templates/skills/knowledge/design-systems/uber/DESIGN.md +295 -0
  76. package/dist/core/templates/skills/knowledge/design-systems/vercel/DESIGN.md +310 -0
  77. package/dist/core/templates/skills/knowledge/design-systems/voltagent/DESIGN.md +323 -0
  78. package/dist/core/templates/skills/knowledge/design-systems/warp/DESIGN.md +253 -0
  79. package/dist/core/templates/skills/knowledge/design-systems/webflow/DESIGN.md +92 -0
  80. package/dist/core/templates/skills/knowledge/design-systems/wise/DESIGN.md +173 -0
  81. package/dist/core/templates/skills/knowledge/design-systems/x.ai/DESIGN.md +257 -0
  82. package/dist/core/templates/skills/knowledge/design-systems/zapier/DESIGN.md +328 -0
  83. package/dist/core/templates/skills/knowledge/frameworks/php/codeigniter/rise-cms/profile.json +27 -0
  84. package/dist/core/templates/skills/knowledge/frameworks/php/codeigniter/rise-cms/structure.md +137 -0
  85. package/dist/core/templates/skills/knowledge/frameworks/php/laravel/botble/profile.json +39 -0
  86. package/dist/core/templates/skills/knowledge/frameworks/php/laravel/botble/structure.md +208 -0
  87. package/dist/core/templates/skills/knowledge/frameworks/php/wordpress/core/profile.json +51 -0
  88. package/dist/core/templates/skills/knowledge/frameworks/php/wordpress/core/structure.md +369 -0
  89. package/dist/core/templates/skills/knowledge/index.json +65 -0
  90. package/dist/core/templates/skills/module-codebase-skill.md +110 -110
  91. package/dist/core/templates/skills/plugin-directory-skill.md +396 -396
  92. package/dist/core/templates/skills/project-memory-skill.md +222 -0
  93. package/dist/core/templates/skills/project-memory-skill.vi.md +223 -0
  94. package/dist/core/templates/skills/scripts/CODE_EXPLANATION.md +394 -394
  95. package/dist/core/templates/skills/scripts/SEARCH_ALGORITHMS_COMPARISON.md +421 -421
  96. package/dist/core/templates/skills/scripts/SEARCH_MODES_GUIDE.md +238 -238
  97. package/dist/core/templates/skills/scripts/__pycache__/core.cpython-311.pyc +0 -0
  98. package/dist/core/templates/skills/scripts/core.py +391 -385
  99. package/dist/core/templates/skills/scripts/search.py +1 -1
  100. package/dist/core/templates/skills/smart-explore-skill.md +141 -0
  101. package/dist/core/templates/skills/sourcecode-analyzer-skill.md +210 -0
  102. package/dist/core/templates/skills/sourcecode-analyzer-skill.vi.md +210 -0
  103. package/dist/core/templates/skills/suggestion-skill.md +118 -118
  104. package/dist/core/templates/skills/templates/accessibility-checklist.md +40 -40
  105. package/dist/core/templates/skills/templates/example-prompt-full-theme.md +333 -333
  106. package/dist/core/templates/skills/templates/page-types-guide.md +338 -338
  107. package/dist/core/templates/skills/templates/pages-proposed-summary.md +273 -273
  108. package/dist/core/templates/skills/templates/pre-delivery-checklist.md +42 -42
  109. package/dist/core/templates/skills/templates/prompt-template-full-theme.md +313 -313
  110. package/dist/core/templates/skills/templates/responsive-design.md +40 -40
  111. package/dist/core/templates/skills/ui-ux-skill.md +595 -584
  112. package/dist/core/templates/skills/unit-test-skill.md +111 -111
  113. package/dist/core/templates/skills/ux-element/templates/Controller.php +50 -50
  114. package/dist/core/templates/skills/ux-element/templates/Shortcode.php +23 -23
  115. package/dist/core/templates/skills/ux-element/templates/Template.html +20 -20
  116. package/dist/core/templates/skills/ux-element/templates/Thumbnail.svg +8 -8
  117. package/dist/core/templates/skills/ux-element/templates/View.php +21 -21
  118. package/dist/core/templates/skills/ux-element-skill.md +83 -83
  119. package/dist/core/templates/skills/wordpress-plugin-check-skill.md +151 -76
  120. package/dist/core/templates/skills/wordpress-plugin-standard/templates/admin-dashboard.php +47 -47
  121. package/dist/core/templates/skills/wordpress-plugin-standard/templates/admin-settings.php +60 -60
  122. package/dist/core/templates/skills/wordpress-plugin-standard/templates/assets/admin-css.css +22 -22
  123. package/dist/core/templates/skills/wordpress-plugin-standard/templates/assets/admin-js.js +15 -15
  124. package/dist/core/templates/skills/wordpress-plugin-standard/templates/plugin-main.php +169 -169
  125. package/dist/core/templates/skills/wordpress-plugin-standard/templates/readme.txt +41 -41
  126. package/dist/core/templates/skills/wordpress-plugin-standard/templates/uninstall.php +21 -21
  127. package/dist/core/templates/skills/wordpress-plugin-standard-skill.md +100 -100
  128. package/dist/index.js +4068 -278
  129. package/package.json +75 -72
package/dist/index.js CHANGED
@@ -1,112 +1,3196 @@
1
1
  import { createRequire } from 'module'; const require = createRequire(import.meta.url);
2
-
3
- // src/core/config.ts
4
- var HERASPEC_DIR_NAME = "heraspec";
5
- var SPECS_DIR_NAME = "specs";
6
- var CHANGES_DIR_NAME = "changes";
7
- var ARCHIVES_DIR_NAME = "archives";
8
- var SKILLS_DIR_NAME = "skills";
9
- var PROJECT_TYPES = [
10
- "wordpress-plugin",
11
- "wordpress-theme",
12
- "perfex-module",
13
- "laravel-package",
14
- "node-service",
15
- "generic-webapp",
16
- "backend-api",
17
- "frontend-app",
18
- "multi-stack"
19
- ];
20
- var SKILLS = {
21
- "wordpress-plugin": [
22
- "admin-settings-page",
23
- "custom-post-type",
24
- "shortcode",
25
- "rest-endpoint",
26
- "ajax-handler",
27
- "activation-hook",
28
- "deactivation-hook",
29
- "admin-menu-item",
30
- "meta-box",
31
- "taxonomy",
32
- "plugin-check",
33
- "plugin-directory"
34
- ],
35
- "wordpress-theme": [
36
- "theme-setup",
37
- "custom-post-type",
38
- "template-part",
39
- "widget-area",
40
- "customizer-setting",
41
- "theme-option"
42
- ],
43
- "perfex-module": [
44
- "module-codebase",
45
- "module-registration",
46
- "permission-group",
47
- "admin-menu-item",
48
- "login-hook",
49
- "database-table",
50
- "api-endpoint"
51
- ],
52
- "laravel-package": [
53
- "service-provider",
54
- "config-file",
55
- "artisan-command",
56
- "migration",
57
- "model",
58
- "controller",
59
- "middleware",
60
- "route"
61
- ],
62
- "node-service": [
63
- "express-route",
64
- "middleware",
65
- "database-model",
66
- "service-layer",
67
- "api-endpoint",
68
- "background-job"
69
- ],
70
- "generic-webapp": [
71
- "page",
72
- "component",
73
- "api-endpoint",
74
- "database-table",
75
- "authentication",
76
- "authorization"
77
- ],
78
- "backend-api": [
79
- "endpoint",
80
- "middleware",
81
- "authentication",
82
- "authorization",
83
- "database-model",
84
- "validation"
85
- ],
86
- "frontend-app": [
87
- "page",
88
- "component",
89
- "route",
90
- "store",
91
- "service",
92
- "hook"
93
- ],
94
- "multi-stack": [
95
- "cross-platform-feature",
96
- "api-contract",
97
- "shared-type",
98
- "integration-point"
99
- ]
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
6
+ }) : x)(function(x) {
7
+ if (typeof require !== "undefined") return require.apply(this, arguments);
8
+ throw Error('Dynamic require of "' + x + '" is not supported');
9
+ });
10
+ var __esm = (fn, res) => function __init() {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
100
12
  };
101
- var HERASPEC_MARKERS = {
102
- PROJECT_MD: "project.md",
103
- AGENTS_MD: "AGENTS.heraspec.md",
104
- CONFIG_YAML: "config.yaml",
105
- PROPOSAL_MD: "proposal.md",
106
- TASKS_MD: "tasks.md",
107
- DESIGN_MD: "design.md"
13
+ var __export = (target, all) => {
14
+ for (var name in all)
15
+ __defProp(target, name, { get: all[name], enumerable: true });
108
16
  };
109
17
 
18
+ // src/core/config.ts
19
+ var HERASPEC_DIR_NAME, SPECS_DIR_NAME, CHANGES_DIR_NAME, ARCHIVES_DIR_NAME, SKILLS_DIR_NAME, KNOWLEDGE_DIR_NAME, MEMORY_DIR_NAME, PROJECT_TYPES, SKILLS, HERASPEC_MARKERS;
20
+ var init_config = __esm({
21
+ "src/core/config.ts"() {
22
+ "use strict";
23
+ HERASPEC_DIR_NAME = "heraspec";
24
+ SPECS_DIR_NAME = "specs";
25
+ CHANGES_DIR_NAME = "changes";
26
+ ARCHIVES_DIR_NAME = "archives";
27
+ SKILLS_DIR_NAME = "skills";
28
+ KNOWLEDGE_DIR_NAME = "knowledge";
29
+ MEMORY_DIR_NAME = "memory";
30
+ PROJECT_TYPES = [
31
+ "wordpress-plugin",
32
+ "wordpress-theme",
33
+ "perfex-module",
34
+ "laravel-package",
35
+ "node-service",
36
+ "generic-webapp",
37
+ "backend-api",
38
+ "frontend-app",
39
+ "multi-stack"
40
+ ];
41
+ SKILLS = {
42
+ "wordpress-plugin": [
43
+ "admin-settings-page",
44
+ "custom-post-type",
45
+ "shortcode",
46
+ "rest-endpoint",
47
+ "ajax-handler",
48
+ "activation-hook",
49
+ "deactivation-hook",
50
+ "admin-menu-item",
51
+ "meta-box",
52
+ "taxonomy",
53
+ "plugin-check",
54
+ "plugin-directory"
55
+ ],
56
+ "wordpress-theme": [
57
+ "theme-setup",
58
+ "custom-post-type",
59
+ "template-part",
60
+ "widget-area",
61
+ "customizer-setting",
62
+ "theme-option"
63
+ ],
64
+ "perfex-module": [
65
+ "module-codebase",
66
+ "module-registration",
67
+ "permission-group",
68
+ "admin-menu-item",
69
+ "login-hook",
70
+ "database-table",
71
+ "api-endpoint"
72
+ ],
73
+ "laravel-package": [
74
+ "service-provider",
75
+ "config-file",
76
+ "artisan-command",
77
+ "migration",
78
+ "model",
79
+ "controller",
80
+ "middleware",
81
+ "route"
82
+ ],
83
+ "node-service": [
84
+ "express-route",
85
+ "middleware",
86
+ "database-model",
87
+ "service-layer",
88
+ "api-endpoint",
89
+ "background-job"
90
+ ],
91
+ "generic-webapp": [
92
+ "page",
93
+ "component",
94
+ "api-endpoint",
95
+ "database-table",
96
+ "authentication",
97
+ "authorization"
98
+ ],
99
+ "backend-api": [
100
+ "endpoint",
101
+ "middleware",
102
+ "authentication",
103
+ "authorization",
104
+ "database-model",
105
+ "validation"
106
+ ],
107
+ "frontend-app": [
108
+ "page",
109
+ "component",
110
+ "route",
111
+ "store",
112
+ "service",
113
+ "hook"
114
+ ],
115
+ "multi-stack": [
116
+ "cross-platform-feature",
117
+ "api-contract",
118
+ "shared-type",
119
+ "integration-point"
120
+ ]
121
+ };
122
+ HERASPEC_MARKERS = {
123
+ PROJECT_MD: "project.md",
124
+ AGENTS_MD: "AGENTS.heraspec.md",
125
+ CONFIG_YAML: "config.yaml",
126
+ PROPOSAL_MD: "proposal.md",
127
+ TASKS_MD: "tasks.md",
128
+ DESIGN_MD: "design.md"
129
+ };
130
+ }
131
+ });
132
+
133
+ // src/utils/file-system.ts
134
+ import { promises as fs } from "fs";
135
+ import path from "path";
136
+ var FileSystemUtils;
137
+ var init_file_system = __esm({
138
+ "src/utils/file-system.ts"() {
139
+ "use strict";
140
+ FileSystemUtils = class {
141
+ static async createDirectory(dirPath) {
142
+ await fs.mkdir(dirPath, { recursive: true });
143
+ }
144
+ static async fileExists(filePath) {
145
+ try {
146
+ await fs.access(filePath);
147
+ return true;
148
+ } catch {
149
+ return false;
150
+ }
151
+ }
152
+ static async readFile(filePath) {
153
+ return await fs.readFile(filePath, "utf-8");
154
+ }
155
+ static async writeFile(filePath, content) {
156
+ await fs.writeFile(filePath, content, "utf-8");
157
+ }
158
+ static async readDirectory(dirPath) {
159
+ return await fs.readdir(dirPath);
160
+ }
161
+ static async stat(filePath) {
162
+ return await fs.stat(filePath);
163
+ }
164
+ static async copyFile(src, dest) {
165
+ await fs.copyFile(src, dest);
166
+ }
167
+ static async copyDirectory(src, dest) {
168
+ await fs.mkdir(dest, { recursive: true });
169
+ const entries = await fs.readdir(src, { withFileTypes: true });
170
+ for (const entry of entries) {
171
+ const srcPath = path.join(src, entry.name);
172
+ const destPath = path.join(dest, entry.name);
173
+ if (entry.isDirectory()) {
174
+ await this.copyDirectory(srcPath, destPath);
175
+ } else {
176
+ await fs.copyFile(srcPath, destPath);
177
+ }
178
+ }
179
+ }
180
+ static async removeFile(filePath) {
181
+ await fs.unlink(filePath);
182
+ }
183
+ static async removeDirectory(dirPath, recursive = true) {
184
+ if (typeof fs.rm === "function") {
185
+ await fs.rm(dirPath, { recursive, force: true });
186
+ } else {
187
+ await fs.rmdir(dirPath, { recursive });
188
+ }
189
+ }
190
+ static async moveFile(src, dest) {
191
+ await fs.rename(src, dest);
192
+ }
193
+ static joinPath(...segments) {
194
+ return path.join(...segments);
195
+ }
196
+ static resolvePath(...segments) {
197
+ return path.resolve(...segments);
198
+ }
199
+ static getDirectoryName(filePath) {
200
+ return path.dirname(filePath);
201
+ }
202
+ static getBaseName(filePath) {
203
+ return path.basename(filePath);
204
+ }
205
+ static async generateTree(dirPath, maxDepth = 3, ignoreDirs = ["node_modules", ".git", "dist", "build", "vendor", ".next", ".nuxt", "heraspec", ".heraspec"], currentDepth = 0, prefix = "") {
206
+ if (currentDepth >= maxDepth) return "";
207
+ let result = "";
208
+ let entries;
209
+ try {
210
+ entries = await fs.readdir(dirPath);
211
+ } catch {
212
+ return result;
213
+ }
214
+ const validEntries = [];
215
+ for (const entry of entries) {
216
+ if (ignoreDirs.includes(entry)) continue;
217
+ if (entry.startsWith(".")) {
218
+ const stat = await this.stat(path.join(dirPath, entry)).catch(() => null);
219
+ if (stat?.isDirectory()) continue;
220
+ }
221
+ validEntries.push(entry);
222
+ }
223
+ const statsCache = /* @__PURE__ */ new Map();
224
+ for (const entry of validEntries) {
225
+ const stat = await this.stat(path.join(dirPath, entry)).catch(() => null);
226
+ statsCache.set(entry, stat);
227
+ }
228
+ validEntries.sort((a, b) => {
229
+ const statA = statsCache.get(a);
230
+ const statB = statsCache.get(b);
231
+ if (statA?.isDirectory() && !statB?.isDirectory()) return -1;
232
+ if (!statA?.isDirectory() && statB?.isDirectory()) return 1;
233
+ return a.localeCompare(b);
234
+ });
235
+ for (let i = 0; i < validEntries.length; i++) {
236
+ const entry = validEntries[i];
237
+ const isLast = i === validEntries.length - 1;
238
+ const marker = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
239
+ const childPrefix = prefix + (isLast ? " " : "\u2502 ");
240
+ const entryPath = path.join(dirPath, entry);
241
+ const stat = statsCache.get(entry);
242
+ if (stat?.isDirectory()) {
243
+ result += `${prefix}${marker}${entry}/
244
+ `;
245
+ result += await this.generateTree(entryPath, maxDepth, ignoreDirs, currentDepth + 1, childPrefix);
246
+ } else {
247
+ result += `${prefix}${marker}${entry}
248
+ `;
249
+ }
250
+ }
251
+ return result;
252
+ }
253
+ };
254
+ }
255
+ });
256
+
257
+ // src/core/templates/skills-template-map.ts
258
+ function getSkillTemplateInfo(skillName, projectType) {
259
+ if (projectType) {
260
+ const key = `${projectType}:${skillName}`;
261
+ if (SKILL_TEMPLATE_MAP[key]) {
262
+ return SKILL_TEMPLATE_MAP[key];
263
+ }
264
+ }
265
+ if (SKILL_TEMPLATE_MAP[skillName]) {
266
+ return SKILL_TEMPLATE_MAP[skillName];
267
+ }
268
+ if (!projectType) {
269
+ for (const key of Object.keys(SKILL_TEMPLATE_MAP)) {
270
+ const parts = key.split(":");
271
+ if (parts.length === 2 && parts[1] === skillName) {
272
+ return SKILL_TEMPLATE_MAP[key];
273
+ }
274
+ }
275
+ }
276
+ return null;
277
+ }
278
+ function getAllSkillTemplates() {
279
+ const result = [];
280
+ for (const [key, info] of Object.entries(SKILL_TEMPLATE_MAP)) {
281
+ if (info.isCrossCutting) {
282
+ result.push({ skillName: key, info });
283
+ } else {
284
+ const [projectType, skillName] = key.split(":");
285
+ result.push({ skillName, projectType, info });
286
+ }
287
+ }
288
+ return result;
289
+ }
290
+ var SKILL_TEMPLATE_MAP;
291
+ var init_skills_template_map = __esm({
292
+ "src/core/templates/skills-template-map.ts"() {
293
+ "use strict";
294
+ SKILL_TEMPLATE_MAP = {
295
+ // Cross-cutting skills
296
+ "ui-ux": {
297
+ templateFileName: "ui-ux-skill.md",
298
+ isCrossCutting: true,
299
+ resourceDirs: ["scripts", "templates", "data"]
300
+ },
301
+ "design-system": {
302
+ templateFileName: "design-system-skill.md",
303
+ isCrossCutting: true,
304
+ resourceDirs: ["scripts", "data", "knowledge/design-systems"]
305
+ },
306
+ "documents": {
307
+ templateFileName: "documents-skill.md",
308
+ isCrossCutting: true,
309
+ resourceDirs: ["documents/templates"]
310
+ },
311
+ "content-optimization": {
312
+ templateFileName: "content-optimization-skill.md",
313
+ isCrossCutting: true
314
+ },
315
+ "unit-test": {
316
+ templateFileName: "unit-test-skill.md",
317
+ isCrossCutting: true
318
+ },
319
+ "integration-test": {
320
+ templateFileName: "integration-test-skill.md",
321
+ isCrossCutting: true
322
+ },
323
+ "e2e-test": {
324
+ templateFileName: "e2e-test-skill.md",
325
+ isCrossCutting: true
326
+ },
327
+ "suggestion": {
328
+ templateFileName: "suggestion-skill.md",
329
+ isCrossCutting: true
330
+ },
331
+ "sourcecode-analyzer": {
332
+ templateFileName: "sourcecode-analyzer-skill.md",
333
+ isCrossCutting: true,
334
+ viFileName: "sourcecode-analyzer-skill.vi.md"
335
+ },
336
+ "project-memory": {
337
+ templateFileName: "project-memory-skill.md",
338
+ viFileName: "project-memory-skill.vi.md",
339
+ isCrossCutting: true
340
+ },
341
+ "smart-explore": {
342
+ templateFileName: "smart-explore-skill.md",
343
+ isCrossCutting: true
344
+ },
345
+ "deploy-documentation": {
346
+ templateFileName: "deploy-documentation-skill.md",
347
+ isCrossCutting: true,
348
+ resourceDirs: ["templates"]
349
+ },
350
+ // Perfex module skills
351
+ "perfex-module:module-codebase": {
352
+ templateFileName: "module-codebase-skill.md",
353
+ isCrossCutting: false,
354
+ projectType: "perfex-module"
355
+ },
356
+ "wordpress:ux-element": {
357
+ templateFileName: "ux-element-skill.md",
358
+ isCrossCutting: false,
359
+ projectType: "wordpress",
360
+ resourceDirs: ["ux-element/templates"]
361
+ },
362
+ "wordpress:plugin-standard": {
363
+ templateFileName: "wordpress-plugin-standard-skill.md",
364
+ isCrossCutting: false,
365
+ projectType: "wordpress",
366
+ resourceDirs: ["wordpress-plugin-standard/templates"]
367
+ },
368
+ "wordpress:plugin-check": {
369
+ templateFileName: "wordpress-plugin-check-skill.md",
370
+ isCrossCutting: false,
371
+ projectType: "wordpress"
372
+ },
373
+ "wordpress:plugin-directory": {
374
+ templateFileName: "plugin-directory-skill.md",
375
+ isCrossCutting: false,
376
+ projectType: "wordpress"
377
+ },
378
+ // WordPress Plugin specific (matches PROJECT_TYPES)
379
+ "wordpress-plugin:plugin-check": {
380
+ templateFileName: "wordpress-plugin-check-skill.md",
381
+ isCrossCutting: false,
382
+ projectType: "wordpress-plugin"
383
+ },
384
+ "wordpress-plugin:plugin-directory": {
385
+ templateFileName: "plugin-directory-skill.md",
386
+ isCrossCutting: false,
387
+ projectType: "wordpress-plugin"
388
+ }
389
+ };
390
+ }
391
+ });
392
+
393
+ // src/core/memory/memory-schema.ts
394
+ function initializeSchema(db) {
395
+ db.pragma("journal_mode = WAL");
396
+ db.pragma("foreign_keys = ON");
397
+ db.exec(CREATE_TABLES);
398
+ db.exec(CREATE_INDEXES);
399
+ db.exec(CREATE_FTS5);
400
+ db.exec(CREATE_FTS_TRIGGERS);
401
+ db.pragma(`user_version = ${SCHEMA_VERSION}`);
402
+ }
403
+ function needsMigration(db) {
404
+ const currentVersion = db.pragma("user_version", { simple: true });
405
+ return currentVersion < SCHEMA_VERSION;
406
+ }
407
+ function runMigrations(db) {
408
+ const currentVersion = db.pragma("user_version", { simple: true });
409
+ if (currentVersion === 0) {
410
+ initializeSchema(db);
411
+ return;
412
+ }
413
+ if (currentVersion < 2) {
414
+ db.exec(`
415
+ CREATE TABLE IF NOT EXISTS db_history (
416
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
417
+ project TEXT NOT NULL,
418
+ db_size_bytes INTEGER NOT NULL,
419
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
420
+ created_at_epoch INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)
421
+ );
422
+ CREATE INDEX IF NOT EXISTS idx_history_project ON db_history(project);
423
+ `);
424
+ }
425
+ if (currentVersion < 3) {
426
+ const columnExists = db.prepare("PRAGMA table_info(observations)").all().some((col) => col.name === "embedding");
427
+ if (!columnExists) {
428
+ db.exec(`ALTER TABLE observations ADD COLUMN embedding TEXT;`);
429
+ }
430
+ }
431
+ db.pragma(`user_version = ${SCHEMA_VERSION}`);
432
+ }
433
+ var SCHEMA_VERSION, CREATE_TABLES, CREATE_INDEXES, CREATE_FTS5, CREATE_FTS_TRIGGERS;
434
+ var init_memory_schema = __esm({
435
+ "src/core/memory/memory-schema.ts"() {
436
+ "use strict";
437
+ SCHEMA_VERSION = 3;
438
+ CREATE_TABLES = `
439
+ -- Sessions tracking
440
+ CREATE TABLE IF NOT EXISTS sessions (
441
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
442
+ session_id TEXT NOT NULL UNIQUE,
443
+ project TEXT NOT NULL,
444
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
445
+ started_at_epoch INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000),
446
+ completed_at TEXT,
447
+ completed_at_epoch INTEGER,
448
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed'))
449
+ );
450
+
451
+ -- Observation records
452
+ CREATE TABLE IF NOT EXISTS observations (
453
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
454
+ session_id TEXT NOT NULL DEFAULT '',
455
+ project TEXT NOT NULL DEFAULT '',
456
+ type TEXT NOT NULL DEFAULT 'change' CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
457
+ title TEXT NOT NULL DEFAULT '',
458
+ narrative TEXT DEFAULT '',
459
+ concepts TEXT DEFAULT '[]',
460
+ files_read TEXT DEFAULT '[]',
461
+ files_modified TEXT DEFAULT '[]',
462
+ discovery_tokens INTEGER DEFAULT 0,
463
+ embedding TEXT,
464
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
465
+ created_at_epoch INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)
466
+ );
467
+
468
+ -- Session summaries
469
+ CREATE TABLE IF NOT EXISTS session_summaries (
470
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
471
+ session_id TEXT NOT NULL DEFAULT '',
472
+ project TEXT NOT NULL DEFAULT '',
473
+ request TEXT DEFAULT '',
474
+ investigated TEXT DEFAULT '',
475
+ learned TEXT DEFAULT '',
476
+ completed TEXT DEFAULT '',
477
+ next_steps TEXT DEFAULT '',
478
+ files_read TEXT DEFAULT '[]',
479
+ files_edited TEXT DEFAULT '[]',
480
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
481
+ created_at_epoch INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)
482
+ );
483
+
484
+ -- Database size history tracking
485
+ CREATE TABLE IF NOT EXISTS db_history (
486
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
487
+ project TEXT NOT NULL,
488
+ db_size_bytes INTEGER NOT NULL,
489
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
490
+ created_at_epoch INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)
491
+ );
492
+ `;
493
+ CREATE_INDEXES = `
494
+ CREATE INDEX IF NOT EXISTS idx_obs_project ON observations(project);
495
+ CREATE INDEX IF NOT EXISTS idx_obs_type ON observations(type);
496
+ CREATE INDEX IF NOT EXISTS idx_obs_created ON observations(created_at_epoch DESC);
497
+ CREATE INDEX IF NOT EXISTS idx_obs_session ON observations(session_id);
498
+ CREATE INDEX IF NOT EXISTS idx_sum_project ON session_summaries(project);
499
+ CREATE INDEX IF NOT EXISTS idx_sum_created ON session_summaries(created_at_epoch DESC);
500
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project);
501
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
502
+ CREATE INDEX IF NOT EXISTS idx_history_project ON db_history(project);
503
+ `;
504
+ CREATE_FTS5 = `
505
+ CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
506
+ title,
507
+ narrative,
508
+ concepts,
509
+ content='observations',
510
+ content_rowid='id',
511
+ tokenize='porter unicode61'
512
+ );
513
+
514
+ CREATE VIRTUAL TABLE IF NOT EXISTS summaries_fts USING fts5(
515
+ request,
516
+ learned,
517
+ completed,
518
+ content='session_summaries',
519
+ content_rowid='id',
520
+ tokenize='porter unicode61'
521
+ );
522
+ `;
523
+ CREATE_FTS_TRIGGERS = `
524
+ -- Observations FTS triggers
525
+ CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
526
+ INSERT INTO observations_fts(rowid, title, narrative, concepts)
527
+ VALUES (new.id, new.title, new.narrative, new.concepts);
528
+ END;
529
+
530
+ CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
531
+ INSERT INTO observations_fts(observations_fts, rowid, title, narrative, concepts)
532
+ VALUES ('delete', old.id, old.title, old.narrative, old.concepts);
533
+ END;
534
+
535
+ CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
536
+ INSERT INTO observations_fts(observations_fts, rowid, title, narrative, concepts)
537
+ VALUES ('delete', old.id, old.title, old.narrative, old.concepts);
538
+ INSERT INTO observations_fts(rowid, title, narrative, concepts)
539
+ VALUES (new.id, new.title, new.narrative, new.concepts);
540
+ END;
541
+
542
+ -- Summaries FTS triggers
543
+ CREATE TRIGGER IF NOT EXISTS summaries_ai AFTER INSERT ON session_summaries BEGIN
544
+ INSERT INTO summaries_fts(rowid, request, learned, completed)
545
+ VALUES (new.id, new.request, new.learned, new.completed);
546
+ END;
547
+
548
+ CREATE TRIGGER IF NOT EXISTS summaries_ad AFTER DELETE ON session_summaries BEGIN
549
+ INSERT INTO summaries_fts(summaries_fts, rowid, request, learned, completed)
550
+ VALUES ('delete', old.id, old.request, old.learned, old.completed);
551
+ END;
552
+
553
+ CREATE TRIGGER IF NOT EXISTS summaries_au AFTER UPDATE ON session_summaries BEGIN
554
+ INSERT INTO summaries_fts(summaries_fts, rowid, request, learned, completed)
555
+ VALUES ('delete', old.id, old.request, old.learned, old.completed);
556
+ INSERT INTO summaries_fts(rowid, request, learned, completed)
557
+ VALUES (new.id, new.request, new.learned, new.completed);
558
+ END;
559
+ `;
560
+ }
561
+ });
562
+
563
+ // src/core/memory/memory-types.ts
564
+ function estimateTokens(text) {
565
+ if (!text) return 0;
566
+ return Math.ceil(text.length / CHARS_PER_TOKEN_ESTIMATE);
567
+ }
568
+ var OBSERVATION_TYPES, OBSERVATION_TYPE_ICONS, CHARS_PER_TOKEN_ESTIMATE;
569
+ var init_memory_types = __esm({
570
+ "src/core/memory/memory-types.ts"() {
571
+ "use strict";
572
+ OBSERVATION_TYPES = [
573
+ "decision",
574
+ "bugfix",
575
+ "feature",
576
+ "refactor",
577
+ "discovery",
578
+ "change"
579
+ ];
580
+ OBSERVATION_TYPE_ICONS = {
581
+ decision: "\u2696\uFE0F",
582
+ bugfix: "\u{1F534}",
583
+ feature: "\u{1F7E2}",
584
+ refactor: "\u{1F504}",
585
+ discovery: "\u{1F535}",
586
+ change: "\u2705"
587
+ };
588
+ CHARS_PER_TOKEN_ESTIMATE = 4;
589
+ }
590
+ });
591
+
592
+ // src/core/memory/memory-store.ts
593
+ import path2 from "path";
594
+ var DB_FILENAME, MemoryStore;
595
+ var init_memory_store = __esm({
596
+ "src/core/memory/memory-store.ts"() {
597
+ "use strict";
598
+ init_config();
599
+ init_memory_schema();
600
+ init_memory_types();
601
+ DB_FILENAME = "heraspec-memory.db";
602
+ MemoryStore = class {
603
+ db;
604
+ dbPath;
605
+ hasChanges = false;
606
+ constructor(projectPath = ".") {
607
+ this.dbPath = path2.join(projectPath, HERASPEC_DIR_NAME, MEMORY_DIR_NAME, DB_FILENAME);
608
+ this.db = null;
609
+ }
610
+ /**
611
+ * Open database connection, init schema if needed
612
+ */
613
+ open() {
614
+ if (this.db) return;
615
+ let Database;
616
+ try {
617
+ Database = __require("better-sqlite3");
618
+ } catch {
619
+ throw new Error(
620
+ "better-sqlite3 is required for HeraSpec memory. Install it with: npm install better-sqlite3"
621
+ );
622
+ }
623
+ const dir = path2.dirname(this.dbPath);
624
+ const fs2 = __require("fs");
625
+ if (!fs2.existsSync(dir)) {
626
+ fs2.mkdirSync(dir, { recursive: true });
627
+ }
628
+ this.db = new Database(this.dbPath);
629
+ if (needsMigration(this.db)) {
630
+ runMigrations(this.db);
631
+ }
632
+ }
633
+ /**
634
+ * Close database connection
635
+ */
636
+ close() {
637
+ if (this.db) {
638
+ if (this.hasChanges) {
639
+ this.logDbSizeChange();
640
+ this.hasChanges = false;
641
+ }
642
+ this.db.close();
643
+ this.db = null;
644
+ }
645
+ }
646
+ logDbSizeChange() {
647
+ try {
648
+ const project = this.detectProjectName();
649
+ const fs2 = __require("fs");
650
+ if (fs2.existsSync(this.dbPath)) {
651
+ let totalSize = fs2.statSync(this.dbPath).size;
652
+ const walPath = this.dbPath + "-wal";
653
+ if (fs2.existsSync(walPath)) {
654
+ totalSize += fs2.statSync(walPath).size;
655
+ }
656
+ const now = /* @__PURE__ */ new Date();
657
+ this.db.prepare(`
658
+ INSERT INTO db_history (project, db_size_bytes, created_at, created_at_epoch)
659
+ VALUES (?, ?, ?, ?)
660
+ `).run(project, totalSize, now.toISOString(), now.getTime());
661
+ }
662
+ } catch (e) {
663
+ }
664
+ }
665
+ /**
666
+ * Get the raw database reference (for advanced queries)
667
+ */
668
+ getDb() {
669
+ this.ensureOpen();
670
+ return this.db;
671
+ }
672
+ ensureOpen() {
673
+ if (!this.db) {
674
+ this.open();
675
+ }
676
+ }
677
+ // ============ Observations ============
678
+ /**
679
+ * Add a new observation
680
+ */
681
+ addObservation(input) {
682
+ this.ensureOpen();
683
+ const now = /* @__PURE__ */ new Date();
684
+ const sessionId = input.sessionId || this.generateSessionId();
685
+ const project = input.project || this.detectProjectName();
686
+ const stmt = this.db.prepare(`
687
+ INSERT INTO observations (session_id, project, type, title, narrative, concepts, files_read, files_modified, discovery_tokens, embedding, created_at, created_at_epoch)
688
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
689
+ `);
690
+ const result = stmt.run(
691
+ sessionId,
692
+ project,
693
+ input.type,
694
+ input.title,
695
+ input.narrative || "",
696
+ JSON.stringify(input.concepts || []),
697
+ JSON.stringify(input.filesRead || []),
698
+ JSON.stringify(input.filesModified || []),
699
+ input.discoveryTokens || 0,
700
+ input.embedding ? JSON.stringify(input.embedding) : null,
701
+ now.toISOString(),
702
+ now.getTime()
703
+ );
704
+ this.hasChanges = true;
705
+ return this.getObservationById(result.lastInsertRowid);
706
+ }
707
+ /**
708
+ * Get observation by ID
709
+ */
710
+ getObservationById(id) {
711
+ this.ensureOpen();
712
+ const row = this.db.prepare("SELECT * FROM observations WHERE id = ?").get(id);
713
+ return row ? this.rowToObservation(row) : null;
714
+ }
715
+ /**
716
+ * Get observations by IDs (batch)
717
+ */
718
+ getObservationsByIds(ids) {
719
+ this.ensureOpen();
720
+ if (ids.length === 0) return [];
721
+ const placeholders = ids.map(() => "?").join(",");
722
+ const rows = this.db.prepare(`SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`).all(...ids);
723
+ return rows.map((row) => this.rowToObservation(row));
724
+ }
725
+ /**
726
+ * Get recent observations for a project
727
+ */
728
+ getRecentObservations(project, limit = 50) {
729
+ this.ensureOpen();
730
+ let sql = "SELECT * FROM observations";
731
+ const params = [];
732
+ if (project) {
733
+ sql += " WHERE project = ?";
734
+ params.push(project);
735
+ }
736
+ sql += " ORDER BY created_at_epoch DESC LIMIT ?";
737
+ params.push(limit);
738
+ const rows = this.db.prepare(sql).all(...params);
739
+ return rows.map((row) => this.rowToObservation(row));
740
+ }
741
+ /**
742
+ * Delete observations older than N days
743
+ */
744
+ pruneObservations(daysOld, project) {
745
+ this.ensureOpen();
746
+ const cutoffEpoch = Date.now() - daysOld * 24 * 60 * 60 * 1e3;
747
+ let sql = "DELETE FROM observations WHERE created_at_epoch < ?";
748
+ const params = [cutoffEpoch];
749
+ if (project) {
750
+ sql += " AND project = ?";
751
+ params.push(project);
752
+ }
753
+ const result = this.db.prepare(sql).run(...params);
754
+ if (result.changes > 0) this.hasChanges = true;
755
+ return result.changes;
756
+ }
757
+ // ============ Session Summaries ============
758
+ /**
759
+ * Add a new session summary
760
+ */
761
+ addSummary(input) {
762
+ this.ensureOpen();
763
+ const now = /* @__PURE__ */ new Date();
764
+ const sessionId = input.sessionId || this.generateSessionId();
765
+ const project = input.project || this.detectProjectName();
766
+ const stmt = this.db.prepare(`
767
+ INSERT INTO session_summaries (session_id, project, request, investigated, learned, completed, next_steps, files_read, files_edited, created_at, created_at_epoch)
768
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
769
+ `);
770
+ const result = stmt.run(
771
+ sessionId,
772
+ project,
773
+ input.request,
774
+ input.investigated || "",
775
+ input.learned || "",
776
+ input.completed || "",
777
+ input.nextSteps || "",
778
+ JSON.stringify(input.filesRead || []),
779
+ JSON.stringify(input.filesEdited || []),
780
+ now.toISOString(),
781
+ now.getTime()
782
+ );
783
+ this.hasChanges = true;
784
+ return this.getSummaryById(result.lastInsertRowid);
785
+ }
786
+ /**
787
+ * Get summary by ID
788
+ */
789
+ getSummaryById(id) {
790
+ this.ensureOpen();
791
+ const row = this.db.prepare("SELECT * FROM session_summaries WHERE id = ?").get(id);
792
+ return row ? this.rowToSummary(row) : null;
793
+ }
794
+ /**
795
+ * Get recent summaries for a project
796
+ */
797
+ getRecentSummaries(project, limit = 20) {
798
+ this.ensureOpen();
799
+ let sql = "SELECT * FROM session_summaries";
800
+ const params = [];
801
+ if (project) {
802
+ sql += " WHERE project = ?";
803
+ params.push(project);
804
+ }
805
+ sql += " ORDER BY created_at_epoch DESC LIMIT ?";
806
+ params.push(limit);
807
+ const rows = this.db.prepare(sql).all(...params);
808
+ return rows.map((row) => this.rowToSummary(row));
809
+ }
810
+ // ============ Status ============
811
+ /**
812
+ * Get memory status statistics
813
+ */
814
+ getStatus(project) {
815
+ this.ensureOpen();
816
+ const projectFilter = project ? " WHERE project = ?" : "";
817
+ const params = project ? [project] : [];
818
+ const obsCount = this.db.prepare(`SELECT COUNT(*) as count FROM observations${projectFilter}`).get(...params)?.count || 0;
819
+ const sumCount = this.db.prepare(`SELECT COUNT(*) as count FROM session_summaries${projectFilter}`).get(...params)?.count || 0;
820
+ const sessCount = this.db.prepare(`SELECT COUNT(*) as count FROM sessions${projectFilter}`).get(...params)?.count || 0;
821
+ const oldest = this.db.prepare(`SELECT created_at FROM observations${projectFilter} ORDER BY created_at_epoch ASC LIMIT 1`).get(...params);
822
+ const newest = this.db.prepare(`SELECT created_at FROM observations${projectFilter} ORDER BY created_at_epoch DESC LIMIT 1`).get(...params);
823
+ const allObs = this.db.prepare(`SELECT concepts FROM observations${projectFilter}`).all(...params);
824
+ const conceptCounts = {};
825
+ for (const row of allObs) {
826
+ try {
827
+ const concepts = JSON.parse(row.concepts || "[]");
828
+ for (const c of concepts) {
829
+ conceptCounts[c] = (conceptCounts[c] || 0) + 1;
830
+ }
831
+ } catch {
832
+ }
833
+ }
834
+ const topConcepts = Object.entries(conceptCounts).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([concept, count]) => ({ concept, count }));
835
+ const fileCounts = {};
836
+ for (const row of allObs) {
837
+ try {
838
+ const files = JSON.parse(row.files_modified || "[]");
839
+ for (const f of files) {
840
+ fileCounts[f] = (fileCounts[f] || 0) + 1;
841
+ }
842
+ } catch {
843
+ }
844
+ }
845
+ const topFiles = Object.entries(fileCounts).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([file, count]) => ({ file, count }));
846
+ const allNarratives = this.db.prepare(`SELECT narrative FROM observations${projectFilter}`).all(...params);
847
+ let totalTokens = 0;
848
+ for (const row of allNarratives) {
849
+ totalTokens += estimateTokens(row.narrative);
850
+ }
851
+ let dbSizeBytes = 0;
852
+ try {
853
+ const fs2 = __require("fs");
854
+ const stats = fs2.statSync(this.dbPath);
855
+ dbSizeBytes = stats.size;
856
+ } catch {
857
+ }
858
+ return {
859
+ observationCount: obsCount,
860
+ summaryCount: sumCount,
861
+ sessionCount: sessCount,
862
+ oldestObservation: oldest?.created_at || null,
863
+ newestObservation: newest?.created_at || null,
864
+ topConcepts,
865
+ topFiles,
866
+ estimatedTotalTokens: totalTokens,
867
+ dbSizeBytes
868
+ };
869
+ }
870
+ // ============ Analytics ============
871
+ /**
872
+ * Get token saving statistics for all known projects in memory
873
+ */
874
+ getAnalytics() {
875
+ this.ensureOpen();
876
+ const TOKENS_WITHOUT_MEMORY_PER_OP = 5e4;
877
+ const CONTEXT_GENERATION_OVERHEAD = 3e3;
878
+ const obsQuery = this.db.prepare(`
879
+ SELECT
880
+ project,
881
+ COUNT(*) as obs_count,
882
+ SUM(discovery_tokens) as total_discovery,
883
+ SUM(length(narrative)) as total_chars
884
+ FROM observations
885
+ GROUP BY project
886
+ `).all();
887
+ const sumQuery = this.db.prepare(`
888
+ SELECT
889
+ project,
890
+ COUNT(*) as sum_count,
891
+ SUM(length(learned) + length(completed)) as total_chars
892
+ FROM session_summaries
893
+ GROUP BY project
894
+ `).all();
895
+ const projectData = /* @__PURE__ */ new Map();
896
+ for (const row of obsQuery) {
897
+ if (!row.project) continue;
898
+ projectData.set(row.project, {
899
+ ops: row.obs_count,
900
+ discovery: row.total_discovery || 0,
901
+ textChars: row.total_chars || 0
902
+ });
903
+ }
904
+ for (const row of sumQuery) {
905
+ if (!row.project) continue;
906
+ const data = projectData.get(row.project) || { ops: 0, discovery: 0, textChars: 0 };
907
+ data.ops += row.sum_count;
908
+ data.textChars += row.total_chars || 0;
909
+ projectData.set(row.project, data);
910
+ }
911
+ const results = [];
912
+ for (const [project, data] of projectData.entries()) {
913
+ const contentTokens = data.discovery > 0 ? data.discovery : Math.ceil(data.textChars / 4);
914
+ let tokensWithMemory = contentTokens + data.ops * CONTEXT_GENERATION_OVERHEAD;
915
+ let tokensWithoutMemory = data.ops * TOKENS_WITHOUT_MEMORY_PER_OP;
916
+ if (tokensWithoutMemory < tokensWithMemory) {
917
+ tokensWithoutMemory = tokensWithMemory;
918
+ }
919
+ let savingsTokens = tokensWithoutMemory - tokensWithMemory;
920
+ let savingsPercent = tokensWithoutMemory > 0 ? savingsTokens / tokensWithoutMemory * 100 : 0;
921
+ let dbSizeBytes = 0;
922
+ try {
923
+ const fs2 = __require("fs");
924
+ if (fs2.existsSync(this.dbPath)) {
925
+ dbSizeBytes = fs2.statSync(this.dbPath).size;
926
+ const walPath = this.dbPath + "-wal";
927
+ if (fs2.existsSync(walPath)) {
928
+ dbSizeBytes += fs2.statSync(walPath).size;
929
+ }
930
+ }
931
+ } catch {
932
+ }
933
+ results.push({
934
+ project,
935
+ totalOps: data.ops,
936
+ tokensWithMemory,
937
+ tokensWithoutMemory,
938
+ savingsTokens,
939
+ savingsPercent,
940
+ dbSizeBytes
941
+ });
942
+ }
943
+ return results.sort((a, b) => b.savingsTokens - a.savingsTokens);
944
+ }
945
+ /**
946
+ * Get database size history updates
947
+ */
948
+ getDbHistory(project, limit = 13) {
949
+ this.ensureOpen();
950
+ const rows = this.db.prepare(`
951
+ SELECT * FROM db_history
952
+ WHERE project = ?
953
+ ORDER BY created_at_epoch DESC
954
+ LIMIT ?
955
+ `).all(project, limit);
956
+ return rows.map((row) => ({
957
+ id: row.id,
958
+ project: row.project,
959
+ dbSizeBytes: row.db_size_bytes,
960
+ createdAt: row.created_at,
961
+ createdAtEpoch: row.created_at_epoch
962
+ }));
963
+ }
964
+ // ============ Helpers ============
965
+ rowToObservation(row) {
966
+ return {
967
+ id: row.id,
968
+ sessionId: row.session_id,
969
+ project: row.project,
970
+ type: row.type,
971
+ title: row.title,
972
+ narrative: row.narrative || "",
973
+ concepts: this.parseJsonArray(row.concepts),
974
+ filesRead: this.parseJsonArray(row.files_read),
975
+ filesModified: this.parseJsonArray(row.files_modified),
976
+ discoveryTokens: row.discovery_tokens || 0,
977
+ embedding: row.embedding ? JSON.parse(row.embedding) : void 0,
978
+ createdAt: row.created_at,
979
+ createdAtEpoch: row.created_at_epoch
980
+ };
981
+ }
982
+ rowToSummary(row) {
983
+ return {
984
+ id: row.id,
985
+ sessionId: row.session_id,
986
+ project: row.project,
987
+ request: row.request || "",
988
+ investigated: row.investigated || "",
989
+ learned: row.learned || "",
990
+ completed: row.completed || "",
991
+ nextSteps: row.next_steps || "",
992
+ filesRead: this.parseJsonArray(row.files_read),
993
+ filesEdited: this.parseJsonArray(row.files_edited),
994
+ createdAt: row.created_at,
995
+ createdAtEpoch: row.created_at_epoch
996
+ };
997
+ }
998
+ parseJsonArray(json) {
999
+ if (!json) return [];
1000
+ try {
1001
+ const parsed = JSON.parse(json);
1002
+ return Array.isArray(parsed) ? parsed : [];
1003
+ } catch {
1004
+ return [];
1005
+ }
1006
+ }
1007
+ generateSessionId() {
1008
+ const now = /* @__PURE__ */ new Date();
1009
+ const random = Math.random().toString(36).substring(2, 8);
1010
+ return `${now.toISOString().replace(/[:.]/g, "-")}-${random}`;
1011
+ }
1012
+ detectProjectName() {
1013
+ try {
1014
+ const fs2 = __require("fs");
1015
+ const pkgPath = path2.join(process.cwd(), "package.json");
1016
+ if (fs2.existsSync(pkgPath)) {
1017
+ const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
1018
+ return pkg.name || path2.basename(process.cwd());
1019
+ }
1020
+ } catch {
1021
+ }
1022
+ return path2.basename(process.cwd());
1023
+ }
1024
+ };
1025
+ }
1026
+ });
1027
+
1028
+ // src/core/memory/memory-search.ts
1029
+ var MemorySearch;
1030
+ var init_memory_search = __esm({
1031
+ "src/core/memory/memory-search.ts"() {
1032
+ "use strict";
1033
+ init_memory_types();
1034
+ MemorySearch = class {
1035
+ store;
1036
+ constructor(store) {
1037
+ this.store = store;
1038
+ }
1039
+ /**
1040
+ * Search observations using FTS5 full-text search
1041
+ * Returns compact index results (Layer 1 of progressive disclosure)
1042
+ */
1043
+ searchObservations(options) {
1044
+ const db = this.store.getDb();
1045
+ const params = [];
1046
+ let sql;
1047
+ if (options.query) {
1048
+ sql = `
1049
+ SELECT o.*, bm25(observations_fts) as rank
1050
+ FROM observations o
1051
+ JOIN observations_fts fts ON o.id = fts.rowid
1052
+ WHERE observations_fts MATCH ?
1053
+ `;
1054
+ params.push(this.sanitizeFtsQuery(options.query));
1055
+ } else {
1056
+ sql = "SELECT *, 0 as rank FROM observations WHERE 1=1";
1057
+ }
1058
+ if (options.project) {
1059
+ sql += " AND o.project = ?";
1060
+ params.push(options.project);
1061
+ }
1062
+ if (options.type) {
1063
+ const types = Array.isArray(options.type) ? options.type : [options.type];
1064
+ const placeholders = types.map(() => "?").join(",");
1065
+ sql += ` AND o.type IN (${placeholders})`;
1066
+ params.push(...types);
1067
+ }
1068
+ if (options.concepts && options.concepts.length > 0) {
1069
+ const conceptConditions = options.concepts.map(() => `o.concepts LIKE ?`).join(" OR ");
1070
+ sql += ` AND (${conceptConditions})`;
1071
+ params.push(...options.concepts.map((c) => `%"${c}"%`));
1072
+ }
1073
+ if (options.files && options.files.length > 0) {
1074
+ const fileConditions = options.files.map(() => `(o.files_modified LIKE ? OR o.files_read LIKE ?)`).join(" OR ");
1075
+ sql += ` AND (${fileConditions})`;
1076
+ for (const f of options.files) {
1077
+ params.push(`%${f}%`, `%${f}%`);
1078
+ }
1079
+ }
1080
+ if (options.dateStart) {
1081
+ const epoch = this.parseDate(options.dateStart);
1082
+ if (epoch) {
1083
+ sql += " AND o.created_at_epoch >= ?";
1084
+ params.push(epoch);
1085
+ }
1086
+ }
1087
+ if (options.dateEnd) {
1088
+ const epoch = this.parseDate(options.dateEnd);
1089
+ if (epoch) {
1090
+ sql += " AND o.created_at_epoch <= ?";
1091
+ params.push(epoch);
1092
+ }
1093
+ }
1094
+ if (options.query) {
1095
+ sql += " ORDER BY rank";
1096
+ } else if (options.orderBy === "date_asc") {
1097
+ sql += " ORDER BY o.created_at_epoch ASC";
1098
+ } else {
1099
+ sql += " ORDER BY o.created_at_epoch DESC";
1100
+ }
1101
+ sql += " LIMIT ?";
1102
+ params.push(options.limit || 20);
1103
+ if (options.offset) {
1104
+ sql += " OFFSET ?";
1105
+ params.push(options.offset);
1106
+ }
1107
+ const rows = db.prepare(sql).all(...params);
1108
+ return rows.map((row) => ({
1109
+ id: row.id,
1110
+ type: row.type,
1111
+ title: row.title,
1112
+ narrative: row.narrative || "",
1113
+ concepts: this.parseJsonSafe(row.concepts),
1114
+ filesModified: this.parseJsonSafe(row.files_modified),
1115
+ createdAt: row.created_at,
1116
+ createdAtEpoch: row.created_at_epoch,
1117
+ rank: row.rank,
1118
+ estimatedTokens: estimateTokens(row.narrative)
1119
+ }));
1120
+ }
1121
+ /**
1122
+ * Search session summaries using FTS5
1123
+ */
1124
+ searchSummaries(options) {
1125
+ const db = this.store.getDb();
1126
+ const params = [];
1127
+ let sql;
1128
+ if (options.query) {
1129
+ sql = `
1130
+ SELECT s.*, bm25(summaries_fts) as rank
1131
+ FROM session_summaries s
1132
+ JOIN summaries_fts fts ON s.id = fts.rowid
1133
+ WHERE summaries_fts MATCH ?
1134
+ `;
1135
+ params.push(this.sanitizeFtsQuery(options.query));
1136
+ } else {
1137
+ sql = "SELECT *, 0 as rank FROM session_summaries WHERE 1=1";
1138
+ }
1139
+ if (options.project) {
1140
+ sql += " AND s.project = ?";
1141
+ params.push(options.project);
1142
+ }
1143
+ if (options.dateStart) {
1144
+ const epoch = this.parseDate(options.dateStart);
1145
+ if (epoch) {
1146
+ sql += " AND s.created_at_epoch >= ?";
1147
+ params.push(epoch);
1148
+ }
1149
+ }
1150
+ if (options.dateEnd) {
1151
+ const epoch = this.parseDate(options.dateEnd);
1152
+ if (epoch) {
1153
+ sql += " AND s.created_at_epoch <= ?";
1154
+ params.push(epoch);
1155
+ }
1156
+ }
1157
+ if (options.query) {
1158
+ sql += " ORDER BY rank";
1159
+ } else {
1160
+ sql += " ORDER BY s.created_at_epoch DESC";
1161
+ }
1162
+ sql += " LIMIT ?";
1163
+ params.push(options.limit || 10);
1164
+ const rows = db.prepare(sql).all(...params);
1165
+ return rows.map((row) => ({
1166
+ id: row.id,
1167
+ sessionId: row.session_id,
1168
+ request: row.request || "",
1169
+ completed: row.completed || "",
1170
+ learned: row.learned || "",
1171
+ createdAt: row.created_at,
1172
+ createdAtEpoch: row.created_at_epoch,
1173
+ rank: row.rank,
1174
+ estimatedTokens: estimateTokens(row.request) + estimateTokens(row.completed) + estimateTokens(row.learned)
1175
+ }));
1176
+ }
1177
+ /**
1178
+ * Get timeline of observations around a specific point
1179
+ */
1180
+ getTimeline(options) {
1181
+ const db = this.store.getDb();
1182
+ const depthBefore = options.depthBefore ?? 5;
1183
+ const depthAfter = options.depthAfter ?? 5;
1184
+ const items = [];
1185
+ const params = [];
1186
+ let anchorEpoch = options.anchorEpoch;
1187
+ if (options.anchorId && !anchorEpoch) {
1188
+ const obs = db.prepare("SELECT created_at_epoch FROM observations WHERE id = ?").get(options.anchorId);
1189
+ if (obs) anchorEpoch = obs.created_at_epoch;
1190
+ }
1191
+ if (!anchorEpoch) {
1192
+ anchorEpoch = Date.now();
1193
+ }
1194
+ let sqlBefore = `SELECT * FROM observations WHERE created_at_epoch <= ?`;
1195
+ params.length = 0;
1196
+ params.push(anchorEpoch);
1197
+ if (options.project) {
1198
+ sqlBefore += " AND project = ?";
1199
+ params.push(options.project);
1200
+ }
1201
+ sqlBefore += ` ORDER BY created_at_epoch DESC LIMIT ?`;
1202
+ params.push(depthBefore + 1);
1203
+ const beforeRows = db.prepare(sqlBefore).all(...params);
1204
+ params.length = 0;
1205
+ let sqlAfter = `SELECT * FROM observations WHERE created_at_epoch > ?`;
1206
+ params.push(anchorEpoch);
1207
+ if (options.project) {
1208
+ sqlAfter += " AND project = ?";
1209
+ params.push(options.project);
1210
+ }
1211
+ sqlAfter += ` ORDER BY created_at_epoch ASC LIMIT ?`;
1212
+ params.push(depthAfter);
1213
+ const afterRows = db.prepare(sqlAfter).all(...params);
1214
+ const allRows = [...beforeRows.reverse(), ...afterRows];
1215
+ for (const row of allRows) {
1216
+ items.push({
1217
+ type: "observation",
1218
+ id: row.id,
1219
+ title: row.title,
1220
+ timestamp: row.created_at,
1221
+ epoch: row.created_at_epoch,
1222
+ icon: OBSERVATION_TYPE_ICONS[row.type] || "\u{1F4CC}",
1223
+ estimatedTokens: estimateTokens(row.narrative)
1224
+ });
1225
+ }
1226
+ if (allRows.length > 0) {
1227
+ const minEpoch = allRows[0].created_at_epoch;
1228
+ const maxEpoch = allRows[allRows.length - 1].created_at_epoch;
1229
+ params.length = 0;
1230
+ let sqlSum = `SELECT * FROM session_summaries WHERE created_at_epoch >= ? AND created_at_epoch <= ?`;
1231
+ params.push(minEpoch, maxEpoch);
1232
+ if (options.project) {
1233
+ sqlSum += " AND project = ?";
1234
+ params.push(options.project);
1235
+ }
1236
+ const sumRows = db.prepare(sqlSum).all(...params);
1237
+ for (const row of sumRows) {
1238
+ items.push({
1239
+ type: "summary",
1240
+ id: row.id,
1241
+ title: `Session: ${row.request || "Untitled"}`,
1242
+ timestamp: row.created_at,
1243
+ epoch: row.created_at_epoch,
1244
+ icon: "\u{1F3AF}",
1245
+ estimatedTokens: estimateTokens(row.request) + estimateTokens(row.completed)
1246
+ });
1247
+ }
1248
+ }
1249
+ items.sort((a, b) => a.epoch - b.epoch);
1250
+ return items;
1251
+ }
1252
+ /**
1253
+ * Format search results as markdown (compact index view)
1254
+ */
1255
+ formatResultsAsIndex(results, query) {
1256
+ if (results.length === 0) {
1257
+ return query ? `No results found matching "${query}"` : "No observations found.";
1258
+ }
1259
+ const lines = [];
1260
+ lines.push(`Found ${results.length} observation(s)${query ? ` matching "${query}"` : ""}:
1261
+ `);
1262
+ lines.push("| ID | Time | Type | Title | Tokens |");
1263
+ lines.push("|----|------|------|-------|--------|");
1264
+ for (const r of results) {
1265
+ const time = this.formatTime(r.createdAtEpoch);
1266
+ const icon = OBSERVATION_TYPE_ICONS[r.type] || "\u{1F4CC}";
1267
+ lines.push(`| #${r.id} | ${time} | ${icon} | ${r.title} | ~${r.estimatedTokens} |`);
1268
+ }
1269
+ lines.push("");
1270
+ lines.push("Use `heraspec memory search --id <ID>` to see full details.");
1271
+ return lines.join("\n");
1272
+ }
1273
+ /**
1274
+ * Format timeline as markdown
1275
+ */
1276
+ formatTimeline(items) {
1277
+ if (items.length === 0) return "No timeline data found.";
1278
+ const lines = [];
1279
+ lines.push(`# Timeline (${items.length} items)
1280
+ `);
1281
+ const byDay = /* @__PURE__ */ new Map();
1282
+ for (const item of items) {
1283
+ const day = new Date(item.epoch).toLocaleDateString("en-US", {
1284
+ year: "numeric",
1285
+ month: "short",
1286
+ day: "numeric"
1287
+ });
1288
+ if (!byDay.has(day)) byDay.set(day, []);
1289
+ byDay.get(day).push(item);
1290
+ }
1291
+ for (const [day, dayItems] of byDay) {
1292
+ lines.push(`### ${day}
1293
+ `);
1294
+ lines.push("| ID | Time | Type | Title | Tokens |");
1295
+ lines.push("|----|------|------|-------|--------|");
1296
+ for (const item of dayItems) {
1297
+ const time = this.formatTime(item.epoch);
1298
+ lines.push(`| #${item.id} | ${time} | ${item.icon} | ${item.title} | ~${item.estimatedTokens} |`);
1299
+ }
1300
+ lines.push("");
1301
+ }
1302
+ return lines.join("\n");
1303
+ }
1304
+ // ============ Helpers ============
1305
+ /**
1306
+ * Sanitize FTS5 query - escape special characters
1307
+ */
1308
+ sanitizeFtsQuery(query) {
1309
+ return query.replace(/[*"(){}[\]^~:]/g, " ").replace(/\b(AND|OR|NOT|NEAR)\b/gi, "").trim().split(/\s+/).filter((w) => w.length > 0).map((w) => `"${w}"`).join(" OR ");
1310
+ }
1311
+ parseJsonSafe(json) {
1312
+ if (!json) return [];
1313
+ try {
1314
+ const parsed = JSON.parse(json);
1315
+ return Array.isArray(parsed) ? parsed : [];
1316
+ } catch {
1317
+ return [];
1318
+ }
1319
+ }
1320
+ parseDate(input) {
1321
+ const num = Number(input);
1322
+ if (!isNaN(num) && num > 1e12) return num;
1323
+ if (!isNaN(num) && num > 1e9) return num * 1e3;
1324
+ const d = new Date(input);
1325
+ return isNaN(d.getTime()) ? null : d.getTime();
1326
+ }
1327
+ formatTime(epoch) {
1328
+ return new Date(epoch).toLocaleTimeString("en-US", {
1329
+ hour: "2-digit",
1330
+ minute: "2-digit",
1331
+ hour12: true
1332
+ });
1333
+ }
1334
+ };
1335
+ }
1336
+ });
1337
+
1338
+ // src/core/memory/memory-vector.ts
1339
+ import { pipeline } from "@xenova/transformers";
1340
+ var MemoryVector;
1341
+ var init_memory_vector = __esm({
1342
+ "src/core/memory/memory-vector.ts"() {
1343
+ "use strict";
1344
+ init_memory_types();
1345
+ MemoryVector = class {
1346
+ static extractor = null;
1347
+ /**
1348
+ * Initialize the embedding model.
1349
+ * This downloads the model on first run (cached in node_modules or system cache).
1350
+ */
1351
+ static async initModel() {
1352
+ if (!this.extractor) {
1353
+ this.extractor = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
1354
+ }
1355
+ return this.extractor;
1356
+ }
1357
+ /**
1358
+ * Generate an embedding vector for a given text.
1359
+ */
1360
+ static async generateEmbedding(text) {
1361
+ try {
1362
+ const extractor = await this.initModel();
1363
+ const output = await extractor(text, { pooling: "mean", normalize: true });
1364
+ return Array.from(output.data);
1365
+ } catch (error) {
1366
+ return [];
1367
+ }
1368
+ }
1369
+ /**
1370
+ * Calculate Cosine Similarity between two vectors.
1371
+ * Returns a value between -1 and 1. Higher is more similar.
1372
+ */
1373
+ static cosineSimilarity(vecA, vecB) {
1374
+ if (!vecA || !vecB || vecA.length === 0 || vecA.length !== vecB.length) {
1375
+ return 0;
1376
+ }
1377
+ let dotProduct = 0;
1378
+ let normA = 0;
1379
+ let normB = 0;
1380
+ for (let i = 0; i < vecA.length; i++) {
1381
+ dotProduct += vecA[i] * vecB[i];
1382
+ normA += vecA[i] * vecA[i];
1383
+ normB += vecB[i] * vecB[i];
1384
+ }
1385
+ if (normA === 0 || normB === 0) return 0;
1386
+ return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
1387
+ }
1388
+ /**
1389
+ * Search through observations using semantic vector search.
1390
+ */
1391
+ static async search(query, observations, limit = 10) {
1392
+ if (observations.length === 0) return [];
1393
+ const queryEmbedding = await this.generateEmbedding(query);
1394
+ if (queryEmbedding.length === 0) {
1395
+ return [];
1396
+ }
1397
+ const results = [];
1398
+ for (const obs of observations) {
1399
+ if (obs.embedding && obs.embedding.length > 0) {
1400
+ const score = this.cosineSimilarity(queryEmbedding, obs.embedding);
1401
+ results.push({ obs, score });
1402
+ }
1403
+ }
1404
+ results.sort((a, b) => b.score - a.score);
1405
+ return results.slice(0, limit).map(({ obs, score }) => {
1406
+ return {
1407
+ id: obs.id,
1408
+ type: obs.type,
1409
+ title: obs.title,
1410
+ narrative: obs.narrative,
1411
+ concepts: obs.concepts || [],
1412
+ filesModified: obs.filesModified || [],
1413
+ createdAt: obs.createdAt,
1414
+ createdAtEpoch: obs.createdAtEpoch,
1415
+ rank: score,
1416
+ // Use score as rank for sorting/display
1417
+ estimatedTokens: estimateTokens(obs.narrative)
1418
+ };
1419
+ });
1420
+ }
1421
+ };
1422
+ }
1423
+ });
1424
+
1425
+ // src/core/memory/context-config.ts
1426
+ import path3 from "path";
1427
+ function loadContextConfig(projectPath = ".") {
1428
+ const configPath = path3.join(projectPath, HERASPEC_DIR_NAME, MEMORY_DIR_NAME, CONFIG_FILENAME);
1429
+ try {
1430
+ const fs2 = __require("fs");
1431
+ if (!fs2.existsSync(configPath)) {
1432
+ return { ...DEFAULT_CONTEXT_CONFIG };
1433
+ }
1434
+ const raw = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
1435
+ return mergeConfig(raw);
1436
+ } catch {
1437
+ return { ...DEFAULT_CONTEXT_CONFIG };
1438
+ }
1439
+ }
1440
+ function saveContextConfig(config, projectPath = ".") {
1441
+ const configDir = path3.join(projectPath, HERASPEC_DIR_NAME, MEMORY_DIR_NAME);
1442
+ const configPath = path3.join(configDir, CONFIG_FILENAME);
1443
+ const fs2 = __require("fs");
1444
+ if (!fs2.existsSync(configDir)) {
1445
+ fs2.mkdirSync(configDir, { recursive: true });
1446
+ }
1447
+ const serializable = {
1448
+ totalObservationCount: config.totalObservationCount,
1449
+ fullObservationCount: config.fullObservationCount,
1450
+ sessionCount: config.sessionCount,
1451
+ observationTypes: Array.from(config.observationTypes),
1452
+ observationConcepts: Array.from(config.observationConcepts),
1453
+ maxTokens: config.maxTokens,
1454
+ showLastSummary: config.showLastSummary
1455
+ };
1456
+ fs2.writeFileSync(configPath, JSON.stringify(serializable, null, 2), "utf-8");
1457
+ }
1458
+ function mergeConfig(raw) {
1459
+ const defaults = DEFAULT_CONTEXT_CONFIG;
1460
+ return {
1461
+ totalObservationCount: raw.totalObservationCount ?? defaults.totalObservationCount,
1462
+ fullObservationCount: raw.fullObservationCount ?? defaults.fullObservationCount,
1463
+ sessionCount: raw.sessionCount ?? defaults.sessionCount,
1464
+ observationTypes: raw.observationTypes ? new Set(raw.observationTypes) : new Set(defaults.observationTypes),
1465
+ observationConcepts: raw.observationConcepts ? new Set(raw.observationConcepts) : new Set(defaults.observationConcepts),
1466
+ maxTokens: raw.maxTokens ?? defaults.maxTokens,
1467
+ showLastSummary: raw.showLastSummary ?? defaults.showLastSummary
1468
+ };
1469
+ }
1470
+ var CONFIG_FILENAME, DEFAULT_CONTEXT_CONFIG;
1471
+ var init_context_config = __esm({
1472
+ "src/core/memory/context-config.ts"() {
1473
+ "use strict";
1474
+ init_config();
1475
+ CONFIG_FILENAME = "config.json";
1476
+ DEFAULT_CONTEXT_CONFIG = {
1477
+ totalObservationCount: 50,
1478
+ fullObservationCount: 5,
1479
+ sessionCount: 5,
1480
+ observationTypes: /* @__PURE__ */ new Set(["decision", "bugfix", "feature", "refactor", "discovery", "change"]),
1481
+ observationConcepts: /* @__PURE__ */ new Set(),
1482
+ // Empty = all concepts
1483
+ maxTokens: 6e3,
1484
+ showLastSummary: true
1485
+ };
1486
+ }
1487
+ });
1488
+
1489
+ // src/core/memory/context-generator.ts
1490
+ var ContextGenerator;
1491
+ var init_context_generator = __esm({
1492
+ "src/core/memory/context-generator.ts"() {
1493
+ "use strict";
1494
+ init_memory_store();
1495
+ init_memory_search();
1496
+ init_context_config();
1497
+ init_memory_types();
1498
+ ContextGenerator = class {
1499
+ store;
1500
+ search;
1501
+ constructor(projectPath = ".") {
1502
+ this.store = new MemoryStore(projectPath);
1503
+ this.search = new MemorySearch(this.store);
1504
+ }
1505
+ /**
1506
+ * Generate context markdown for AI agent consumption
1507
+ * Writes to heraspec/memory/context.md for on-demand reading
1508
+ */
1509
+ generateContext(projectPath = ".") {
1510
+ const config = loadContextConfig(projectPath);
1511
+ this.store.open();
1512
+ try {
1513
+ const observations = this.store.getRecentObservations(void 0, config.totalObservationCount);
1514
+ const summaries = this.store.getRecentSummaries(void 0, config.sessionCount);
1515
+ if (observations.length === 0 && summaries.length === 0) {
1516
+ return this.renderEmptyState();
1517
+ }
1518
+ let architectureObs = null;
1519
+ try {
1520
+ const db = this.store.getDb();
1521
+ const row = db.prepare(`SELECT id FROM observations WHERE type = 'discovery' AND concepts LIKE '%"architecture"%' ORDER BY created_at_epoch DESC LIMIT 1`).get();
1522
+ if (row) {
1523
+ architectureObs = this.store.getObservationById(row.id);
1524
+ }
1525
+ } catch (e) {
1526
+ }
1527
+ const filteredObservations = observations.filter(
1528
+ (o) => !(o.type === "discovery" && o.concepts.includes("architecture"))
1529
+ );
1530
+ return this.buildContextOutput(filteredObservations, summaries, config, architectureObs);
1531
+ } finally {
1532
+ this.store.close();
1533
+ }
1534
+ }
1535
+ /**
1536
+ * Write context to file for agent to read on-demand
1537
+ */
1538
+ writeContextFile(projectPath = ".") {
1539
+ const context = this.generateContext(projectPath);
1540
+ const contextPath = __require("path").join(projectPath, "heraspec", "memory", "context.md");
1541
+ const fs2 = __require("fs");
1542
+ const dir = __require("path").dirname(contextPath);
1543
+ if (!fs2.existsSync(dir)) {
1544
+ fs2.mkdirSync(dir, { recursive: true });
1545
+ }
1546
+ fs2.writeFileSync(contextPath, context, "utf-8");
1547
+ return contextPath;
1548
+ }
1549
+ // ============ Private ============
1550
+ renderEmptyState() {
1551
+ return `# HeraSpec Memory Context
1552
+
1553
+ > No observations or session summaries recorded yet.
1554
+ > Memory will build up as you work on this project.
1555
+ >
1556
+ > **How it works (complementary approach):**
1557
+ > - Use \`heraspec memory log\` to record important observations
1558
+ > - Use \`heraspec memory summarize\` at end of sessions
1559
+ > - Use \`heraspec memory search\` to check history before implementing features
1560
+ > - This file updates when you run \`heraspec memory context\`
1561
+ `;
1562
+ }
1563
+ buildContextOutput(observations, summaries, config, architectureObs = null) {
1564
+ const lines = [];
1565
+ let tokenBudget = config.maxTokens;
1566
+ lines.push("# HeraSpec Memory Context");
1567
+ lines.push("");
1568
+ lines.push(`> ${observations.length} observations, ${summaries.length} session summaries`);
1569
+ lines.push(`> Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
1570
+ lines.push("");
1571
+ tokenBudget -= 30;
1572
+ if (architectureObs) {
1573
+ lines.push("## Project Architecture");
1574
+ lines.push("");
1575
+ const archBlock = this.renderFullObservation(architectureObs);
1576
+ const archTokens = estimateTokens(archBlock);
1577
+ if (tokenBudget >= archTokens) {
1578
+ lines.push(archBlock);
1579
+ tokenBudget -= archTokens;
1580
+ } else {
1581
+ lines.push(`> Architecture map is too large to include. Use \`heraspec memory search --id ${architectureObs.id}\` to view it.
1582
+ `);
1583
+ }
1584
+ }
1585
+ if (config.showLastSummary && summaries.length > 0) {
1586
+ const latest = summaries[0];
1587
+ const summaryBlock = this.renderSummary(latest);
1588
+ const summaryTokens = estimateTokens(summaryBlock);
1589
+ if (tokenBudget >= summaryTokens) {
1590
+ lines.push(summaryBlock);
1591
+ lines.push("");
1592
+ tokenBudget -= summaryTokens;
1593
+ }
1594
+ }
1595
+ const fullObs = observations.slice(0, config.fullObservationCount);
1596
+ if (fullObs.length > 0) {
1597
+ lines.push("## Recent Activity (Full Details)");
1598
+ lines.push("");
1599
+ for (const obs of fullObs) {
1600
+ const obsBlock = this.renderFullObservation(obs);
1601
+ const obsTokens = estimateTokens(obsBlock);
1602
+ if (tokenBudget >= obsTokens) {
1603
+ lines.push(obsBlock);
1604
+ tokenBudget -= obsTokens;
1605
+ } else {
1606
+ break;
1607
+ }
1608
+ }
1609
+ }
1610
+ const indexObs = observations.slice(config.fullObservationCount);
1611
+ if (indexObs.length > 0) {
1612
+ lines.push("## Earlier Activity (Index)");
1613
+ lines.push("");
1614
+ lines.push("| ID | Date | Type | Title | Tokens |");
1615
+ lines.push("|----|------|------|-------|--------|");
1616
+ for (const obs of indexObs) {
1617
+ const date = new Date(obs.createdAtEpoch).toLocaleDateString("en-US", {
1618
+ month: "short",
1619
+ day: "numeric"
1620
+ });
1621
+ const icon = OBSERVATION_TYPE_ICONS[obs.type] || "\u{1F4CC}";
1622
+ const tokens = estimateTokens(obs.narrative);
1623
+ const row = `| #${obs.id} | ${date} | ${icon} | ${obs.title} | ~${tokens} |`;
1624
+ const rowTokens = estimateTokens(row);
1625
+ if (tokenBudget >= rowTokens) {
1626
+ lines.push(row);
1627
+ tokenBudget -= rowTokens;
1628
+ } else {
1629
+ lines.push(`| ... | | | ${indexObs.length - indexObs.indexOf(obs)} more observations | |`);
1630
+ break;
1631
+ }
1632
+ }
1633
+ lines.push("");
1634
+ lines.push("> Use `heraspec memory search --id <ID>` to see full details of any observation.");
1635
+ }
1636
+ if (summaries.length > 1) {
1637
+ const olderSummaries = summaries.slice(1);
1638
+ lines.push("");
1639
+ lines.push("## Previous Sessions");
1640
+ lines.push("");
1641
+ for (const sum of olderSummaries) {
1642
+ const date = new Date(sum.createdAtEpoch).toLocaleDateString("en-US", {
1643
+ month: "short",
1644
+ day: "numeric"
1645
+ });
1646
+ const compact = `- **${date}**: ${sum.request || "Untitled"} \u2192 ${sum.completed || "No summary"}`;
1647
+ const compactTokens = estimateTokens(compact);
1648
+ if (tokenBudget >= compactTokens) {
1649
+ lines.push(compact);
1650
+ tokenBudget -= compactTokens;
1651
+ }
1652
+ }
1653
+ }
1654
+ return lines.join("\n");
1655
+ }
1656
+ renderSummary(summary) {
1657
+ const lines = [];
1658
+ const date = new Date(summary.createdAtEpoch).toLocaleDateString("en-US", {
1659
+ year: "numeric",
1660
+ month: "short",
1661
+ day: "numeric"
1662
+ });
1663
+ lines.push(`## Last Session (${date})`);
1664
+ lines.push("");
1665
+ if (summary.request) lines.push(`**Request:** ${summary.request}`);
1666
+ if (summary.investigated) lines.push(`**Investigated:** ${summary.investigated}`);
1667
+ if (summary.learned) lines.push(`**Learned:** ${summary.learned}`);
1668
+ if (summary.completed) lines.push(`**Completed:** ${summary.completed}`);
1669
+ if (summary.nextSteps) lines.push(`**Next Steps:** ${summary.nextSteps}`);
1670
+ if (summary.filesEdited.length > 0) {
1671
+ lines.push(`**Files edited:** ${summary.filesEdited.join(", ")}`);
1672
+ }
1673
+ return lines.join("\n");
1674
+ }
1675
+ renderFullObservation(obs) {
1676
+ const lines = [];
1677
+ const icon = OBSERVATION_TYPE_ICONS[obs.type] || "\u{1F4CC}";
1678
+ const time = new Date(obs.createdAtEpoch).toLocaleString("en-US", {
1679
+ month: "short",
1680
+ day: "numeric",
1681
+ hour: "2-digit",
1682
+ minute: "2-digit"
1683
+ });
1684
+ lines.push(`### ${icon} #${obs.id} \u2014 ${obs.title}`);
1685
+ lines.push(`*${time} | ${obs.type} | Concepts: ${obs.concepts.join(", ") || "none"}*`);
1686
+ lines.push("");
1687
+ if (obs.narrative) {
1688
+ lines.push(obs.narrative);
1689
+ lines.push("");
1690
+ }
1691
+ if (obs.filesModified.length > 0) {
1692
+ lines.push(`**Files modified:** ${obs.filesModified.join(", ")}`);
1693
+ }
1694
+ lines.push("---");
1695
+ lines.push("");
1696
+ return lines.join("\n");
1697
+ }
1698
+ };
1699
+ }
1700
+ });
1701
+
1702
+ // src/core/memory/config-advisor.ts
1703
+ function analyzeAndRecommend(status, projectPath = ".") {
1704
+ const currentConfig = loadContextConfig(projectPath);
1705
+ const scale = detectScale(status);
1706
+ const optimal = SCALE_CONFIGS[scale];
1707
+ const recommendations = [];
1708
+ if (currentConfig.totalObservationCount !== optimal.totalObservationCount) {
1709
+ const current = currentConfig.totalObservationCount;
1710
+ const recommended = optimal.totalObservationCount;
1711
+ if (status.observationCount > current * 0.8) {
1712
+ recommendations.push({
1713
+ setting: "totalObservationCount",
1714
+ currentValue: current,
1715
+ recommendedValue: recommended,
1716
+ reason: `D\u1EF1 \xE1n c\xF3 ${status.observationCount} observations, context \u0111ang hi\u1EC3n th\u1ECB ${current} \u2014 ${current < recommended ? "c\xF3 th\u1EC3 b\u1ECF l\u1EE1 context quan tr\u1ECDng" : "\u0111ang hi\u1EC3n th\u1ECB qu\xE1 nhi\u1EC1u"}`,
1717
+ impact: current < recommended ? `T\u0103ng t\u1EA7m nh\xECn t\u1EEB ${current} \u2192 ${recommended} observations (+${(recommended - current) * 15} tokens index)` : `Gi\u1EA3m t\u1EEB ${current} \u2192 ${recommended} observations (ti\u1EBFt ki\u1EC7m ~${(current - recommended) * 15} tokens)`
1718
+ });
1719
+ }
1720
+ }
1721
+ if (currentConfig.fullObservationCount !== optimal.fullObservationCount) {
1722
+ recommendations.push({
1723
+ setting: "fullObservationCount",
1724
+ currentValue: currentConfig.fullObservationCount,
1725
+ recommendedValue: optimal.fullObservationCount,
1726
+ reason: `Scale "${scale}" t\u1ED1i \u01B0u v\u1EDBi ${optimal.fullObservationCount} full observations`,
1727
+ impact: `${currentConfig.fullObservationCount < optimal.fullObservationCount ? "Th\xEAm" : "Gi\u1EA3m"} narrative \u0111\u1EA7y \u0111\u1EE7 \u2192 ${currentConfig.fullObservationCount < optimal.fullObservationCount ? "context phong ph\xFA h\u01A1n" : "ti\u1EBFt ki\u1EC7m tokens"}`
1728
+ });
1729
+ }
1730
+ if (currentConfig.sessionCount !== optimal.sessionCount) {
1731
+ if (status.summaryCount > currentConfig.sessionCount * 2) {
1732
+ recommendations.push({
1733
+ setting: "sessionCount",
1734
+ currentValue: currentConfig.sessionCount,
1735
+ recommendedValue: optimal.sessionCount,
1736
+ reason: `C\xF3 ${status.summaryCount} summaries nh\u01B0ng ch\u1EC9 hi\u1EC7n ${currentConfig.sessionCount}`,
1737
+ impact: `T\u0103ng session history \u2192 agent hi\u1EC3u r\xF5 h\u01A1n d\xF2ng th\u1EDDi gian d\u1EF1 \xE1n`
1738
+ });
1739
+ }
1740
+ }
1741
+ if (currentConfig.maxTokens !== optimal.maxTokens) {
1742
+ const estimatedContextTokens = estimateContextCost(optimal, status);
1743
+ if (estimatedContextTokens > currentConfig.maxTokens * 0.85) {
1744
+ recommendations.push({
1745
+ setting: "maxTokens",
1746
+ currentValue: currentConfig.maxTokens,
1747
+ recommendedValue: optimal.maxTokens,
1748
+ reason: `Context \u01B0\u1EDBc t\xEDnh ~${estimatedContextTokens} tokens, \u0111ang c\u1EAFt b\u1EDBt v\xEC limit ${currentConfig.maxTokens}`,
1749
+ impact: `T\u0103ng budget \u2192 context \u0111\u1EA7y \u0111\u1EE7 h\u01A1n, ch\u1EC9 chi\u1EBFm ${(optimal.maxTokens / 128e3 * 100).toFixed(1)}% context window nh\u1ECF nh\u1EA5t (128K)`
1750
+ });
1751
+ }
1752
+ }
1753
+ if (status.observationCount > 1e3) {
1754
+ const daysSinceOldest = status.oldestObservation ? Math.floor((Date.now() - new Date(status.oldestObservation).getTime()) / (1e3 * 60 * 60 * 24)) : 0;
1755
+ if (daysSinceOldest > 180) {
1756
+ recommendations.push({
1757
+ setting: "prune",
1758
+ currentValue: daysSinceOldest,
1759
+ recommendedValue: 180,
1760
+ reason: `C\xF3 observations c\u0169 ${daysSinceOldest} ng\xE0y \u2014 \xEDt gi\xE1 tr\u1ECB cho context hi\u1EC7n t\u1EA1i`,
1761
+ impact: `Ch\u1EA1y \`heraspec memory prune 180\` \u0111\u1EC3 x\xF3a observations c\u0169 h\u01A1n 6 th\xE1ng`
1762
+ });
1763
+ }
1764
+ }
1765
+ const hasChanges = recommendations.filter((r) => r.setting !== "prune").length > 0;
1766
+ return {
1767
+ projectScale: scale,
1768
+ recommendations,
1769
+ hasChanges,
1770
+ summary: buildSummary(scale, status, recommendations)
1771
+ };
1772
+ }
1773
+ function detectScale(status) {
1774
+ const obsCount = status.observationCount;
1775
+ const sumCount = status.summaryCount;
1776
+ if (obsCount <= SCALE_THRESHOLDS.small.maxObs && sumCount <= SCALE_THRESHOLDS.small.maxSum) {
1777
+ return "small";
1778
+ }
1779
+ if (obsCount <= SCALE_THRESHOLDS.medium.maxObs && sumCount <= SCALE_THRESHOLDS.medium.maxSum) {
1780
+ return "medium";
1781
+ }
1782
+ if (obsCount <= SCALE_THRESHOLDS.large.maxObs && sumCount <= SCALE_THRESHOLDS.large.maxSum) {
1783
+ return "large";
1784
+ }
1785
+ return "enterprise";
1786
+ }
1787
+ function estimateContextCost(config, status) {
1788
+ const fullObs = Math.min(config.fullObservationCount || 5, status.observationCount);
1789
+ const indexObs = Math.min(
1790
+ (config.totalObservationCount || 50) - fullObs,
1791
+ Math.max(0, status.observationCount - fullObs)
1792
+ );
1793
+ const sessions = Math.min(config.sessionCount || 5, status.summaryCount);
1794
+ const headerTokens = 30;
1795
+ const fullObsTokens = fullObs * 350;
1796
+ const indexTokens = indexObs * 15;
1797
+ const summaryTokens = sessions > 0 ? 300 + (sessions - 1) * 30 : 0;
1798
+ return headerTokens + fullObsTokens + indexTokens + summaryTokens;
1799
+ }
1800
+ function buildSummary(scale, status, recommendations) {
1801
+ const scaleLabels = {
1802
+ small: "\u{1F4E6} Nh\u1ECF (< 50 observations)",
1803
+ medium: "\u{1F4CA} Trung b\xECnh (50-500 observations)",
1804
+ large: "\u{1F3E2} L\u1EDBn (500-2000 observations)",
1805
+ enterprise: "\u{1F3D7}\uFE0F Enterprise (2000+ observations)"
1806
+ };
1807
+ const lines = [];
1808
+ lines.push(`Quy m\xF4 d\u1EF1 \xE1n: ${scaleLabels[scale]}`);
1809
+ lines.push(`Observations: ${status.observationCount} | Summaries: ${status.summaryCount}`);
1810
+ if (recommendations.length === 0) {
1811
+ lines.push(`
1812
+ \u2705 C\u1EA5u h\xECnh hi\u1EC7n t\u1EA1i \u0111\xE3 t\u1ED1i \u01B0u cho quy m\xF4 d\u1EF1 \xE1n.`);
1813
+ } else {
1814
+ const configChanges = recommendations.filter((r) => r.setting !== "prune");
1815
+ if (configChanges.length > 0) {
1816
+ lines.push(`
1817
+ \u26A0\uFE0F C\xF3 ${configChanges.length} \u0111\u1EC1 xu\u1EA5t \u0111i\u1EC1u ch\u1EC9nh config.`);
1818
+ }
1819
+ const pruneAdvice = recommendations.find((r) => r.setting === "prune");
1820
+ if (pruneAdvice) {
1821
+ lines.push(`\u{1F5D1}\uFE0F N\xEAn d\u1ECDn d\u1EB9p observations c\u0169.`);
1822
+ }
1823
+ }
1824
+ return lines.join("\n");
1825
+ }
1826
+ function buildOptimizedConfig(status, projectPath = ".") {
1827
+ const advice = analyzeAndRecommend(status, projectPath);
1828
+ const currentConfig = loadContextConfig(projectPath);
1829
+ const newConfig = { ...currentConfig };
1830
+ for (const rec of advice.recommendations) {
1831
+ if (rec.setting === "prune") continue;
1832
+ switch (rec.setting) {
1833
+ case "totalObservationCount":
1834
+ newConfig.totalObservationCount = rec.recommendedValue;
1835
+ break;
1836
+ case "fullObservationCount":
1837
+ newConfig.fullObservationCount = rec.recommendedValue;
1838
+ break;
1839
+ case "sessionCount":
1840
+ newConfig.sessionCount = rec.recommendedValue;
1841
+ break;
1842
+ case "maxTokens":
1843
+ newConfig.maxTokens = rec.recommendedValue;
1844
+ break;
1845
+ }
1846
+ }
1847
+ return { config: newConfig, advice };
1848
+ }
1849
+ var SCALE_THRESHOLDS, SCALE_CONFIGS;
1850
+ var init_config_advisor = __esm({
1851
+ "src/core/memory/config-advisor.ts"() {
1852
+ "use strict";
1853
+ init_context_config();
1854
+ SCALE_THRESHOLDS = {
1855
+ small: { maxObs: 50, maxSum: 10 },
1856
+ medium: { maxObs: 500, maxSum: 50 },
1857
+ large: { maxObs: 2e3, maxSum: 200 }
1858
+ // enterprise: above large
1859
+ };
1860
+ SCALE_CONFIGS = {
1861
+ small: {
1862
+ totalObservationCount: 30,
1863
+ fullObservationCount: 3,
1864
+ sessionCount: 3,
1865
+ maxTokens: 4e3
1866
+ },
1867
+ medium: {
1868
+ totalObservationCount: 50,
1869
+ fullObservationCount: 5,
1870
+ sessionCount: 5,
1871
+ maxTokens: 6e3
1872
+ },
1873
+ large: {
1874
+ totalObservationCount: 80,
1875
+ fullObservationCount: 5,
1876
+ sessionCount: 8,
1877
+ maxTokens: 8e3
1878
+ },
1879
+ enterprise: {
1880
+ totalObservationCount: 100,
1881
+ fullObservationCount: 7,
1882
+ sessionCount: 10,
1883
+ maxTokens: 1e4
1884
+ }
1885
+ };
1886
+ }
1887
+ });
1888
+
1889
+ // src/commands/memory.ts
1890
+ var memory_exports = {};
1891
+ __export(memory_exports, {
1892
+ MemoryCommand: () => MemoryCommand
1893
+ });
1894
+ import chalk from "chalk";
1895
+ import ora from "ora";
1896
+ import path4 from "path";
1897
+ var MemoryCommand;
1898
+ var init_memory = __esm({
1899
+ "src/commands/memory.ts"() {
1900
+ "use strict";
1901
+ init_memory_store();
1902
+ init_memory_search();
1903
+ init_memory_vector();
1904
+ init_context_generator();
1905
+ init_memory_types();
1906
+ init_config_advisor();
1907
+ init_context_config();
1908
+ init_file_system();
1909
+ init_config();
1910
+ MemoryCommand = class {
1911
+ /**
1912
+ * heraspec memory log - Record an observation
1913
+ */
1914
+ async log(options, projectPath = ".") {
1915
+ const spinner = ora("Recording observation...").start();
1916
+ try {
1917
+ if (!OBSERVATION_TYPES.includes(options.type)) {
1918
+ spinner.fail(`Invalid type "${options.type}". Valid types: ${OBSERVATION_TYPES.join(", ")}`);
1919
+ process.exitCode = 1;
1920
+ return;
1921
+ }
1922
+ const store = new MemoryStore(projectPath);
1923
+ store.open();
1924
+ try {
1925
+ const textToEmbed = `${options.title} ${options.narrative || ""} ${options.concepts || ""}`.trim();
1926
+ let embedding;
1927
+ try {
1928
+ embedding = await MemoryVector.generateEmbedding(textToEmbed);
1929
+ } catch (e) {
1930
+ }
1931
+ const obs = store.addObservation({
1932
+ type: options.type,
1933
+ title: options.title,
1934
+ narrative: options.narrative || "",
1935
+ concepts: options.concepts ? options.concepts.split(",").map((c) => c.trim()) : [],
1936
+ filesModified: options.filesModified ? options.filesModified.split(",").map((f) => f.trim()) : [],
1937
+ filesRead: options.filesRead ? options.filesRead.split(",").map((f) => f.trim()) : [],
1938
+ discoveryTokens: options.discoveryTokens ? parseInt(options.discoveryTokens, 10) : 0,
1939
+ sessionId: options.sessionId,
1940
+ project: options.project,
1941
+ embedding
1942
+ });
1943
+ const icon = OBSERVATION_TYPE_ICONS[obs.type] || "\u{1F4CC}";
1944
+ spinner.succeed(`${icon} Observation #${obs.id} recorded: ${obs.title}`);
1945
+ if (obs.concepts.length > 0) {
1946
+ console.log(` Concepts: ${chalk.cyan(obs.concepts.join(", "))}`);
1947
+ }
1948
+ if (obs.filesModified.length > 0) {
1949
+ console.log(` Files: ${chalk.yellow(obs.filesModified.join(", "))}`);
1950
+ }
1951
+ } finally {
1952
+ store.close();
1953
+ }
1954
+ } catch (error) {
1955
+ spinner.fail(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1956
+ process.exitCode = 1;
1957
+ }
1958
+ try {
1959
+ await this.optimize({ yes: true, silent: true }, projectPath);
1960
+ } catch (e) {
1961
+ }
1962
+ }
1963
+ /**
1964
+ * heraspec memory query - Semantic search using Vector embeddings
1965
+ */
1966
+ async query(question, options = {}, projectPath = ".") {
1967
+ const spinner = ora("Generating embedding for query...").start();
1968
+ try {
1969
+ const store = new MemoryStore(projectPath);
1970
+ store.open();
1971
+ try {
1972
+ const observations = store.getRecentObservations(options.project, 5e3);
1973
+ spinner.text = "Calculating semantic similarity...";
1974
+ const results = await MemoryVector.search(question, observations, options.limit || 10);
1975
+ spinner.stop();
1976
+ if (results.length === 0) {
1977
+ console.log(chalk.yellow("\nNo relevant observations found using vector search.\n"));
1978
+ return;
1979
+ }
1980
+ console.log(chalk.cyan("\n\u{1F50D} Semantic Search Results:\n"));
1981
+ const search = new MemorySearch(store);
1982
+ console.log(search.formatResultsAsIndex(results, question));
1983
+ console.log(chalk.gray("\n\u{1F4A1} Note: Results are ranked by semantic similarity, not chronologically."));
1984
+ } finally {
1985
+ store.close();
1986
+ }
1987
+ } catch (error) {
1988
+ spinner.fail(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1989
+ process.exitCode = 1;
1990
+ }
1991
+ }
1992
+ /**
1993
+ * heraspec memory search - Search observations
1994
+ */
1995
+ async search(query, options = {}, projectPath = ".") {
1996
+ try {
1997
+ const store = new MemoryStore(projectPath);
1998
+ store.open();
1999
+ try {
2000
+ if (options.id) {
2001
+ const obs = store.getObservationById(options.id);
2002
+ if (!obs) {
2003
+ console.log(chalk.red(`Observation #${options.id} not found.`));
2004
+ process.exitCode = 1;
2005
+ return;
2006
+ }
2007
+ const icon = OBSERVATION_TYPE_ICONS[obs.type] || "\u{1F4CC}";
2008
+ console.log(`
2009
+ ${icon} Observation #${obs.id} \u2014 ${obs.title}`);
2010
+ console.log(`${chalk.gray("Type:")} ${obs.type}`);
2011
+ console.log(`${chalk.gray("Date:")} ${obs.createdAt}`);
2012
+ console.log(`${chalk.gray("Concepts:")} ${obs.concepts.join(", ") || "none"}`);
2013
+ console.log(`${chalk.gray("Session:")} ${obs.sessionId}`);
2014
+ if (obs.filesRead.length > 0) {
2015
+ console.log(`${chalk.gray("Files read:")} ${obs.filesRead.join(", ")}`);
2016
+ }
2017
+ if (obs.filesModified.length > 0) {
2018
+ console.log(`${chalk.gray("Files modified:")} ${obs.filesModified.join(", ")}`);
2019
+ }
2020
+ console.log(`${chalk.gray("Tokens:")} ~${estimateTokens(obs.narrative)}`);
2021
+ console.log(`
2022
+ ${obs.narrative || "(no narrative)"}
2023
+ `);
2024
+ return;
2025
+ }
2026
+ const search = new MemorySearch(store);
2027
+ const results = search.searchObservations({
2028
+ query: query || void 0,
2029
+ type: options.type,
2030
+ concepts: options.concepts ? options.concepts.split(",").map((c) => c.trim()) : void 0,
2031
+ files: options.files ? options.files.split(",").map((f) => f.trim()) : void 0,
2032
+ limit: options.limit || 20,
2033
+ project: options.project
2034
+ });
2035
+ console.log("\n" + search.formatResultsAsIndex(results, query || void 0));
2036
+ } finally {
2037
+ store.close();
2038
+ }
2039
+ } catch (error) {
2040
+ console.error(chalk.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
2041
+ process.exitCode = 1;
2042
+ }
2043
+ }
2044
+ /**
2045
+ * heraspec memory context - Generate context for AI agent
2046
+ */
2047
+ async context(options = {}, projectPath = ".") {
2048
+ const spinner = ora("Generating context...").start();
2049
+ try {
2050
+ const generator = new ContextGenerator(projectPath);
2051
+ if (options.output === "file") {
2052
+ const contextPath = generator.writeContextFile(projectPath);
2053
+ spinner.succeed(`Context written to: ${contextPath}`);
2054
+ } else {
2055
+ spinner.stop();
2056
+ const context = generator.generateContext(projectPath);
2057
+ console.log(context);
2058
+ }
2059
+ } catch (error) {
2060
+ spinner.fail(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
2061
+ process.exitCode = 1;
2062
+ }
2063
+ }
2064
+ /**
2065
+ * heraspec memory summarize - Create a session summary
2066
+ */
2067
+ async summarize(options, projectPath = ".") {
2068
+ const spinner = ora("Creating session summary...").start();
2069
+ try {
2070
+ const store = new MemoryStore(projectPath);
2071
+ store.open();
2072
+ try {
2073
+ const summary = store.addSummary({
2074
+ request: options.request,
2075
+ investigated: options.investigated || "",
2076
+ learned: options.learned || "",
2077
+ completed: options.completed || "",
2078
+ nextSteps: options.nextSteps || "",
2079
+ filesRead: options.filesRead ? options.filesRead.split(",").map((f) => f.trim()) : [],
2080
+ filesEdited: options.filesEdited ? options.filesEdited.split(",").map((f) => f.trim()) : [],
2081
+ sessionId: options.sessionId,
2082
+ project: options.project
2083
+ });
2084
+ spinner.succeed(`\u{1F3AF} Session summary #${summary.id} created`);
2085
+ console.log(` Request: ${chalk.white(summary.request)}`);
2086
+ if (summary.completed) console.log(` Completed: ${chalk.green(summary.completed)}`);
2087
+ if (summary.nextSteps) console.log(` Next steps: ${chalk.yellow(summary.nextSteps)}`);
2088
+ } finally {
2089
+ store.close();
2090
+ }
2091
+ } catch (error) {
2092
+ spinner.fail(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
2093
+ process.exitCode = 1;
2094
+ }
2095
+ try {
2096
+ await this.optimize({ yes: true, silent: true }, projectPath);
2097
+ } catch (e) {
2098
+ }
2099
+ }
2100
+ /**
2101
+ * heraspec memory status - Show memory statistics
2102
+ */
2103
+ async status(projectPath = ".") {
2104
+ try {
2105
+ const store = new MemoryStore(projectPath);
2106
+ store.open();
2107
+ try {
2108
+ const status = store.getStatus();
2109
+ console.log("\n\u{1F4CA} HeraSpec Memory Status\n");
2110
+ console.log("\u2550".repeat(50));
2111
+ console.log(` Observations: ${chalk.cyan(String(status.observationCount))}`);
2112
+ console.log(` Summaries: ${chalk.cyan(String(status.summaryCount))}`);
2113
+ console.log(` Sessions: ${chalk.cyan(String(status.sessionCount))}`);
2114
+ console.log(` DB Size: ${chalk.cyan(this.formatBytes(status.dbSizeBytes))}`);
2115
+ console.log(` Total Tokens: ${chalk.cyan("~" + status.estimatedTotalTokens)}`);
2116
+ if (status.oldestObservation) {
2117
+ console.log(` Oldest: ${chalk.gray(status.oldestObservation)}`);
2118
+ console.log(` Newest: ${chalk.gray(status.newestObservation || "")}`);
2119
+ }
2120
+ if (status.topConcepts.length > 0) {
2121
+ console.log("\n Top Concepts:");
2122
+ for (const { concept, count } of status.topConcepts.slice(0, 5)) {
2123
+ console.log(` ${chalk.cyan(concept)} (${count})`);
2124
+ }
2125
+ }
2126
+ if (status.topFiles.length > 0) {
2127
+ console.log("\n Top Files:");
2128
+ for (const { file, count } of status.topFiles.slice(0, 5)) {
2129
+ console.log(` ${chalk.yellow(file)} (${count})`);
2130
+ }
2131
+ }
2132
+ console.log("\n" + "\u2550".repeat(50));
2133
+ const advice = analyzeAndRecommend(status, projectPath);
2134
+ const scaleIcons = {
2135
+ small: "\u{1F4E6}",
2136
+ medium: "\u{1F4CA}",
2137
+ large: "\u{1F3E2}",
2138
+ enterprise: "\u{1F3D7}\uFE0F"
2139
+ };
2140
+ console.log(`
2141
+ Scale: ${scaleIcons[advice.projectScale] || "\u{1F4CA}"} ${advice.projectScale}`);
2142
+ if (advice.recommendations.length > 0) {
2143
+ console.log(`
2144
+ ${chalk.yellow("\u26A0\uFE0F Config recommendations detected:")}`);
2145
+ for (const rec of advice.recommendations) {
2146
+ if (rec.setting === "prune") {
2147
+ console.log(` \u{1F5D1}\uFE0F ${chalk.gray(rec.reason)}`);
2148
+ console.log(` \u2192 ${chalk.cyan(rec.impact)}`);
2149
+ } else {
2150
+ console.log(` ${chalk.white(rec.setting)}: ${chalk.red(String(rec.currentValue))} \u2192 ${chalk.green(String(rec.recommendedValue))}`);
2151
+ console.log(` ${chalk.gray(rec.reason)}`);
2152
+ }
2153
+ }
2154
+ if (advice.hasChanges) {
2155
+ console.log(`
2156
+ \u{1F4A1} Run ${chalk.cyan("heraspec memory optimize")} to apply recommendations.`);
2157
+ }
2158
+ } else {
2159
+ console.log(`
2160
+ ${chalk.green("\u2705 Config is optimal for current project scale.")}`);
2161
+ }
2162
+ console.log("\n");
2163
+ } finally {
2164
+ store.close();
2165
+ }
2166
+ } catch (error) {
2167
+ console.error(chalk.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
2168
+ process.exitCode = 1;
2169
+ }
2170
+ }
2171
+ /**
2172
+ * heraspec memory timeline - Show observation timeline
2173
+ */
2174
+ async timeline(options = {}, projectPath = ".") {
2175
+ try {
2176
+ const store = new MemoryStore(projectPath);
2177
+ store.open();
2178
+ try {
2179
+ const search = new MemorySearch(store);
2180
+ const items = search.getTimeline({
2181
+ depthBefore: options.limit || 20,
2182
+ depthAfter: 0,
2183
+ project: options.project
2184
+ });
2185
+ console.log("\n" + search.formatTimeline(items));
2186
+ } finally {
2187
+ store.close();
2188
+ }
2189
+ } catch (error) {
2190
+ console.error(chalk.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
2191
+ process.exitCode = 1;
2192
+ }
2193
+ }
2194
+ /**
2195
+ * heraspec memory prune - Delete old observations
2196
+ */
2197
+ async prune(days, options = {}, projectPath = ".") {
2198
+ const spinner = ora(`Pruning observations older than ${days} days...`).start();
2199
+ try {
2200
+ const store = new MemoryStore(projectPath);
2201
+ store.open();
2202
+ try {
2203
+ const deleted = store.pruneObservations(days, options.project);
2204
+ spinner.succeed(`Pruned ${deleted} observation(s) older than ${days} days.`);
2205
+ } finally {
2206
+ store.close();
2207
+ }
2208
+ } catch (error) {
2209
+ spinner.fail(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
2210
+ process.exitCode = 1;
2211
+ }
2212
+ }
2213
+ formatBytes(bytes) {
2214
+ if (bytes === 0) return "0 B";
2215
+ const units = ["B", "KB", "MB", "GB"];
2216
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
2217
+ return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
2218
+ }
2219
+ /**
2220
+ * heraspec memory optimize - Auto-detect and apply optimal config
2221
+ */
2222
+ async optimize(options = {}, projectPath = ".") {
2223
+ try {
2224
+ const store = new MemoryStore(projectPath);
2225
+ store.open();
2226
+ try {
2227
+ const status = store.getStatus();
2228
+ const { config: newConfig, advice } = buildOptimizedConfig(status, projectPath);
2229
+ const scaleLabels = {
2230
+ small: "\u{1F4E6} Small (< 50 observations)",
2231
+ medium: "\u{1F4CA} Medium (50-500 observations)",
2232
+ large: "\u{1F3E2} Large (500-2000 observations)",
2233
+ enterprise: "\u{1F3D7}\uFE0F Enterprise (2000+ observations)"
2234
+ };
2235
+ if (!options.silent) {
2236
+ console.log(`
2237
+ \u{1F50D} HeraSpec Memory Config Optimizer
2238
+ `);
2239
+ console.log(`Project scale: ${scaleLabels[advice.projectScale]}`);
2240
+ console.log(`Observations: ${status.observationCount} | Summaries: ${status.summaryCount}
2241
+ `);
2242
+ }
2243
+ if (!advice.hasChanges) {
2244
+ if (!options.silent) {
2245
+ console.log(chalk.green("\u2705 Current config is already optimal for your project scale.\n"));
2246
+ }
2247
+ return;
2248
+ }
2249
+ if (options.silent && options.yes) {
2250
+ saveContextConfig(newConfig, projectPath);
2251
+ return;
2252
+ }
2253
+ if (!options.silent) {
2254
+ console.log("Proposed changes:\n");
2255
+ }
2256
+ console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
2257
+ console.log(" \u2502 Setting \u2502 Current \u2502 Recommended \u2502");
2258
+ console.log(" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
2259
+ for (const rec of advice.recommendations) {
2260
+ if (rec.setting === "prune") continue;
2261
+ const name = rec.setting.padEnd(23);
2262
+ const current = String(rec.currentValue).padEnd(8);
2263
+ const recommended = String(rec.recommendedValue).padEnd(11);
2264
+ console.log(` \u2502 ${name} \u2502 ${chalk.red(current)} \u2502 ${chalk.green(recommended)} \u2502`);
2265
+ }
2266
+ console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n");
2267
+ for (const rec of advice.recommendations) {
2268
+ if (rec.setting === "prune") {
2269
+ console.log(` \u{1F5D1}\uFE0F ${rec.reason}`);
2270
+ console.log(` \u2192 ${chalk.cyan(rec.impact)}
2271
+ `);
2272
+ } else {
2273
+ console.log(` ${chalk.white(rec.setting)}: ${rec.reason}`);
2274
+ }
2275
+ }
2276
+ if (options.yes) {
2277
+ saveContextConfig(newConfig, projectPath);
2278
+ console.log(chalk.green("\n\u2705 Config updated successfully!"));
2279
+ console.log(chalk.gray(" Location: heraspec/memory/config.json\n"));
2280
+ return;
2281
+ }
2282
+ const { confirm } = await import("@inquirer/prompts");
2283
+ const answer = await confirm({
2284
+ message: "Apply these config changes?",
2285
+ default: true
2286
+ });
2287
+ if (answer) {
2288
+ saveContextConfig(newConfig, projectPath);
2289
+ console.log(chalk.green("\n\u2705 Config updated successfully!"));
2290
+ console.log(chalk.gray(" Location: heraspec/memory/config.json\n"));
2291
+ } else {
2292
+ console.log(chalk.gray("\nNo changes made.\n"));
2293
+ }
2294
+ } finally {
2295
+ store.close();
2296
+ }
2297
+ } catch (error) {
2298
+ console.error(chalk.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
2299
+ process.exitCode = 1;
2300
+ }
2301
+ }
2302
+ /**
2303
+ * heraspec memory bootstrap - Import historical specs and archives into memory
2304
+ */
2305
+ async bootstrap(options = {}, projectPath = ".") {
2306
+ console.log(chalk.cyan("\n\u{1F680} Bootstrapping Project Memory from Historical Specs...\n"));
2307
+ const heraspecPath = path4.join(projectPath, HERASPEC_DIR_NAME);
2308
+ const specsDir = path4.join(heraspecPath, SPECS_DIR_NAME);
2309
+ const archivesDir = path4.join(heraspecPath, ARCHIVES_DIR_NAME);
2310
+ const changesDir = path4.join(heraspecPath, CHANGES_DIR_NAME);
2311
+ const mdFiles = [];
2312
+ const scanDir = async (dirPath) => {
2313
+ if (!await FileSystemUtils.fileExists(dirPath)) return;
2314
+ const entries = await FileSystemUtils.readDirectory(dirPath);
2315
+ for (const entry of entries) {
2316
+ const fullPath = path4.join(dirPath, entry);
2317
+ const stat = await FileSystemUtils.stat(fullPath);
2318
+ if (stat.isDirectory()) {
2319
+ const subEntries = await FileSystemUtils.readDirectory(fullPath);
2320
+ for (const sub of subEntries) {
2321
+ if (sub.endsWith(".md") && sub !== "tasks.md" && sub !== "project.md") {
2322
+ mdFiles.push(path4.join(fullPath, sub));
2323
+ }
2324
+ }
2325
+ } else if (entry.endsWith(".md")) {
2326
+ mdFiles.push(fullPath);
2327
+ }
2328
+ }
2329
+ };
2330
+ await scanDir(specsDir);
2331
+ await scanDir(archivesDir);
2332
+ await scanDir(changesDir);
2333
+ if (mdFiles.length === 0) {
2334
+ console.log(chalk.yellow("No historical specs found."));
2335
+ return;
2336
+ }
2337
+ if (!options.yes) {
2338
+ const { confirm } = await import("@inquirer/prompts");
2339
+ const answer = await confirm({
2340
+ message: `Found ${mdFiles.length} potential spec files. Proceed to extract and inject into memory?`,
2341
+ default: true
2342
+ });
2343
+ if (!answer) {
2344
+ console.log(chalk.gray("Aborted."));
2345
+ return;
2346
+ }
2347
+ }
2348
+ const spinner = ora("Parsing and migrating specs...").start();
2349
+ const store = new MemoryStore(projectPath);
2350
+ let parsedCount = 0;
2351
+ let skippedCount = 0;
2352
+ try {
2353
+ store.open();
2354
+ const existingTitles = new Set(
2355
+ store.getRecentObservations(void 0, 1e3).map((o) => o.title.toLowerCase())
2356
+ );
2357
+ for (const filePath of mdFiles) {
2358
+ const content = await FileSystemUtils.readFile(filePath);
2359
+ const fileName = path4.basename(filePath, ".md");
2360
+ const titleMatch = content.match(/^#\s+(.+)$/m);
2361
+ const title = titleMatch ? titleMatch[1].trim() : fileName.replace(/-/g, " ");
2362
+ if (existingTitles.has(title.toLowerCase())) {
2363
+ skippedCount++;
2364
+ continue;
2365
+ }
2366
+ let narrative = "";
2367
+ const narrativeMatch = content.match(/##\s+(?:Goal|Context)\s*\n([\s\S]*?)(?=\n##\s|$)/i);
2368
+ if (narrativeMatch) {
2369
+ narrative = narrativeMatch[1].trim();
2370
+ } else {
2371
+ const firstParagraph = content.replace(/^#.*\n/, "").trim().split("\n\n")[0];
2372
+ narrative = firstParagraph || "Bootstrapped historical spec.";
2373
+ }
2374
+ if (narrative.length > 500) narrative = narrative.substring(0, 500) + "...";
2375
+ const filesModified = [];
2376
+ const fileRegex = /####\s+\[(?:MODIFY|NEW|DELETE)\]\s+(?:\[(.*?)\]|\S+)\s*\((.*?)\)/gi;
2377
+ let fmMatch;
2378
+ while ((fmMatch = fileRegex.exec(content)) !== null) {
2379
+ const fsPath = fmMatch[2];
2380
+ const cleanPath = fsPath.replace(/^file:\/\/\/?/, "");
2381
+ filesModified.push(cleanPath);
2382
+ }
2383
+ const concepts = ["legacy", "bootstrapped"];
2384
+ if (filesModified.length > 0) concepts.push("files-modified");
2385
+ store.addObservation({
2386
+ type: "feature",
2387
+ title,
2388
+ narrative,
2389
+ concepts,
2390
+ filesModified: filesModified.slice(0, 5)
2391
+ // Keep top 5 to not bloat limit
2392
+ });
2393
+ existingTitles.add(title.toLowerCase());
2394
+ parsedCount++;
2395
+ }
2396
+ spinner.succeed(`Migration complete: ${parsedCount} specs imported, ${skippedCount} skipped (already exist).`);
2397
+ console.log(chalk.gray(`
2398
+ You can verify by running: ${chalk.cyan("heraspec memory status")}`));
2399
+ } catch (error) {
2400
+ spinner.fail(`Metadata extraction failed: ${error instanceof Error ? error.message : "Unknown error"}`);
2401
+ process.exitCode = 1;
2402
+ } finally {
2403
+ store.close();
2404
+ }
2405
+ }
2406
+ /**
2407
+ * heraspec memory analytics - Show token usage and economic savings metrics
2408
+ */
2409
+ async analytics(options = {}, projectPath = ".") {
2410
+ try {
2411
+ const store = new MemoryStore(projectPath);
2412
+ store.open();
2413
+ try {
2414
+ const stats = store.getAnalytics();
2415
+ if (stats.length === 0) {
2416
+ console.log(chalk.yellow("\n\u{1F4CA} No memory analytics data found yet.\n"));
2417
+ return;
2418
+ }
2419
+ console.log(chalk.cyan("\n\u{1F4CA} HeraSpec Memory Token Economics\n"));
2420
+ console.log(chalk.gray("Comparing estimated token usage: With Memory vs Without Memory.\n"));
2421
+ console.log("\u2550".repeat(105));
2422
+ console.log(
2423
+ chalk.bold("Project".padEnd(25)) + chalk.bold("Ops".padEnd(6)) + chalk.bold("Tokens (With Memory)".padEnd(22)) + chalk.bold("Tokens (Without)".padEnd(20)) + chalk.bold("Savings".padEnd(10)) + chalk.bold("DB Size".padEnd(12))
2424
+ );
2425
+ console.log("\u2500".repeat(105));
2426
+ let overallWith = 0;
2427
+ let overallWithout = 0;
2428
+ for (const s of stats) {
2429
+ overallWith += s.tokensWithMemory;
2430
+ overallWithout += s.tokensWithoutMemory;
2431
+ const pName = s.project.length > 22 ? s.project.substring(0, 20) + ".." : s.project.padEnd(25);
2432
+ const ops = String(s.totalOps).padEnd(6);
2433
+ const withMem = "~" + this.formatNumber(s.tokensWithMemory);
2434
+ const withoutMem = "~" + this.formatNumber(s.tokensWithoutMemory);
2435
+ const savings = chalk.green("+" + s.savingsPercent.toFixed(0) + "%");
2436
+ const dbSize = this.formatBytes(s.dbSizeBytes || 0);
2437
+ console.log(`${pName}${ops}${withMem.padEnd(22)}${withoutMem.padEnd(20)}${savings.padEnd(10)}${dbSize}`);
2438
+ }
2439
+ console.log("\u2500".repeat(105));
2440
+ const totalSavingsPct = overallWithout > 0 ? (overallWithout - overallWith) / overallWithout * 100 : 0;
2441
+ console.log(
2442
+ chalk.bold("TOTAL".padEnd(31)) + chalk.bold(`~${this.formatNumber(overallWith)}`.padEnd(22)) + chalk.bold(`~${this.formatNumber(overallWithout)}`.padEnd(20)) + chalk.bold(chalk.green(`+${totalSavingsPct.toFixed(0)}%`.padEnd(10)))
2443
+ );
2444
+ console.log("\u2550".repeat(105) + "\n");
2445
+ console.log(chalk.cyan("\u{1F4C8} Top Savings Chart (Tokens Avoided)\n"));
2446
+ const MAX_BAR_LEN = 40;
2447
+ const maxSavings = Math.max(...stats.map((s) => s.savingsTokens));
2448
+ for (const s of stats.slice(0, 5)) {
2449
+ if (s.savingsTokens <= 0) continue;
2450
+ const barLen = Math.ceil(s.savingsTokens / maxSavings * MAX_BAR_LEN);
2451
+ const bar = "\u2588".repeat(barLen);
2452
+ console.log(` ${s.project.substring(0, 15).padEnd(16)} | ${chalk.green(bar)} ${chalk.green("+" + this.formatNumber(s.savingsTokens))} tokens`);
2453
+ }
2454
+ console.log("\n" + chalk.gray('\u{1F4A1} Note: "Tokens Without" assume reading 50k tokens (average codebase context) per operation if memory was absent.') + "\n");
2455
+ if (options.history) {
2456
+ for (const s of stats) {
2457
+ const historyRows = store.getDbHistory(s.project, 13);
2458
+ if (historyRows.length === 0) continue;
2459
+ console.log(chalk.cyan(`
2460
+ \u{1F552} Database Size History: [${s.project}] (Latest 13)`));
2461
+ console.log("\u2550".repeat(60));
2462
+ console.log(
2463
+ chalk.bold("Date".padEnd(25)) + chalk.bold("Size".padEnd(15)) + chalk.bold("Delta")
2464
+ );
2465
+ console.log("\u2500".repeat(60));
2466
+ for (let i = 0; i < historyRows.length; i++) {
2467
+ const row = historyRows[i];
2468
+ let deltaStr = "-";
2469
+ if (i < historyRows.length - 1) {
2470
+ const prev = historyRows[i + 1];
2471
+ const diff = row.dbSizeBytes - prev.dbSizeBytes;
2472
+ if (diff > 0) {
2473
+ deltaStr = chalk.green(`+${this.formatBytes(diff)}`);
2474
+ } else if (diff < 0) {
2475
+ deltaStr = chalk.yellow(`-${this.formatBytes(Math.abs(diff))}`);
2476
+ }
2477
+ }
2478
+ const dStr = new Date(row.createdAtEpoch).toLocaleString();
2479
+ console.log(`${dStr.padEnd(25)}${this.formatBytes(row.dbSizeBytes).padEnd(15)}${deltaStr}`);
2480
+ }
2481
+ console.log("\u2550".repeat(60) + "\n");
2482
+ }
2483
+ }
2484
+ } finally {
2485
+ store.close();
2486
+ }
2487
+ } catch (error) {
2488
+ console.error(chalk.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
2489
+ process.exitCode = 1;
2490
+ }
2491
+ }
2492
+ /**
2493
+ * heraspec memory index - Scan project and generate architecture map observation
2494
+ */
2495
+ async index(options = {}, projectPath = ".") {
2496
+ console.log(chalk.cyan("\n\u{1F50D} Generating Project Architecture Index...\n"));
2497
+ if (!options.yes) {
2498
+ const { confirm } = await import("@inquirer/prompts");
2499
+ const answer = await confirm({
2500
+ message: "This will scan the project directory structure and add an Architecture Map to the Memory DB. Proceed?",
2501
+ default: true
2502
+ });
2503
+ if (!answer) {
2504
+ console.log(chalk.gray("Aborted."));
2505
+ return;
2506
+ }
2507
+ }
2508
+ const spinner = ora("Scanning project structure...").start();
2509
+ try {
2510
+ const maxDepth = options.depth ? parseInt(options.depth, 10) : 3;
2511
+ const treeStr = await FileSystemUtils.generateTree(projectPath, maxDepth);
2512
+ if (!treeStr) {
2513
+ spinner.fail("Failed to generate tree or directory is empty.");
2514
+ process.exitCode = 1;
2515
+ return;
2516
+ }
2517
+ const narrative = `### Project Directory Structure (Depth: ${maxDepth})
2518
+
2519
+ \`\`\`text
2520
+ ${treeStr}
2521
+ \`\`\`
2522
+
2523
+ > This observation provides a structural overview of the project. For deeper insights into specific files, use \`heraspec explore outline <file>\`.`;
2524
+ const store = new MemoryStore(projectPath);
2525
+ store.open();
2526
+ try {
2527
+ const obs = store.addObservation({
2528
+ type: "discovery",
2529
+ title: "Project Architecture and Directory Structure",
2530
+ narrative,
2531
+ concepts: ["architecture", "structure", "index"]
2532
+ });
2533
+ spinner.succeed(`Architecture Map saved to Memory DB as Observation #${obs.id}.`);
2534
+ console.log(chalk.gray(`
2535
+ Use ${chalk.cyan("heraspec memory context")} to see the updated context map.`));
2536
+ } finally {
2537
+ store.close();
2538
+ }
2539
+ } catch (error) {
2540
+ spinner.fail(`Indexing failed: ${error instanceof Error ? error.message : "Unknown error"}`);
2541
+ process.exitCode = 1;
2542
+ }
2543
+ }
2544
+ formatNumber(num) {
2545
+ if (num >= 1e6) return (num / 1e6).toFixed(1) + "M";
2546
+ if (num >= 1e3) return (num / 1e3).toFixed(1) + "K";
2547
+ return String(Math.floor(num));
2548
+ }
2549
+ };
2550
+ }
2551
+ });
2552
+
2553
+ // src/core/skills/skill-parser.ts
2554
+ import { readFileSync } from "fs";
2555
+ import path5 from "path";
2556
+ var SkillParser;
2557
+ var init_skill_parser = __esm({
2558
+ "src/core/skills/skill-parser.ts"() {
2559
+ "use strict";
2560
+ SkillParser = class {
2561
+ static parseSkill(skillPath, skillName) {
2562
+ const skillMdPath = path5.join(skillPath, "skill.md");
2563
+ try {
2564
+ const content = readFileSync(skillMdPath, "utf-8");
2565
+ return this.parseSkillContent(content, skillName, skillPath);
2566
+ } catch (error) {
2567
+ throw new Error(`Failed to parse skill "${skillName}": ${error instanceof Error ? error.message : "Unknown error"}`);
2568
+ }
2569
+ }
2570
+ static parseSkillContent(content, skillName, skillPath) {
2571
+ const lines = content.split("\n");
2572
+ const skill = {
2573
+ name: skillName,
2574
+ skillPath,
2575
+ purpose: "",
2576
+ whenToUse: [],
2577
+ steps: [],
2578
+ inputs: [],
2579
+ outputs: [],
2580
+ toneAndRules: {},
2581
+ templates: [],
2582
+ scripts: [],
2583
+ examples: [],
2584
+ relatedSkills: []
2585
+ };
2586
+ let currentSection = "";
2587
+ let currentList = [];
2588
+ for (let i = 0; i < lines.length; i++) {
2589
+ const line = lines[i].trim();
2590
+ if (line.match(/^##+\s+/)) {
2591
+ this.saveList(currentSection, currentList, skill);
2592
+ currentList = [];
2593
+ const sectionName = line.replace(/^##+\s+/, "").toLowerCase();
2594
+ currentSection = sectionName;
2595
+ if (sectionName.includes("m\u1EE5c \u0111\xEDch") || sectionName.includes("purpose")) {
2596
+ if (i + 1 < lines.length && lines[i + 1].trim()) {
2597
+ skill.purpose = lines[i + 1].trim();
2598
+ }
2599
+ }
2600
+ continue;
2601
+ }
2602
+ if (line.startsWith("- ") || line.startsWith("* ")) {
2603
+ const item = line.replace(/^[-*]\s+/, "").trim();
2604
+ if (item) {
2605
+ currentList.push(item);
2606
+ }
2607
+ continue;
2608
+ }
2609
+ if (line.match(/^\d+\.\s+/)) {
2610
+ const step = line.replace(/^\d+\.\s+/, "").trim();
2611
+ if (step) {
2612
+ skill.steps = skill.steps || [];
2613
+ skill.steps.push(step);
2614
+ }
2615
+ continue;
2616
+ }
2617
+ if (currentSection.includes("input")) {
2618
+ if (line.startsWith("- ")) {
2619
+ skill.inputs = skill.inputs || [];
2620
+ skill.inputs.push(line.replace(/^-\s+/, ""));
2621
+ }
2622
+ }
2623
+ if (currentSection.includes("output")) {
2624
+ if (line.startsWith("- ")) {
2625
+ skill.outputs = skill.outputs || [];
2626
+ skill.outputs.push(line.replace(/^-\s+/, ""));
2627
+ }
2628
+ }
2629
+ if (currentSection.includes("template")) {
2630
+ if (line.includes(".php") || line.includes(".md") || line.includes(".scss") || line.includes(".js") || line.includes(".sh")) {
2631
+ skill.templates = skill.templates || [];
2632
+ const templateName = line.match(/`([^`]+)`/) || line.match(/\*\*([^*]+)\*\*/);
2633
+ if (templateName) {
2634
+ skill.templates.push(templateName[1]);
2635
+ }
2636
+ }
2637
+ }
2638
+ if (currentSection.includes("script")) {
2639
+ if (line.includes(".sh") || line.includes(".py") || line.includes(".js")) {
2640
+ skill.scripts = skill.scripts || [];
2641
+ const scriptName = line.match(/`([^`]+)`/) || line.match(/\*\*([^*]+)\*\*/);
2642
+ if (scriptName) {
2643
+ skill.scripts.push(scriptName[1]);
2644
+ }
2645
+ }
2646
+ }
2647
+ }
2648
+ this.saveList(currentSection, currentList, skill);
2649
+ return {
2650
+ name: skill.name || skillName,
2651
+ skillPath: skill.skillPath || skillPath,
2652
+ purpose: skill.purpose || "No description available",
2653
+ whenToUse: skill.whenToUse || [],
2654
+ steps: skill.steps || [],
2655
+ inputs: skill.inputs || [],
2656
+ outputs: skill.outputs || [],
2657
+ toneAndRules: skill.toneAndRules || {},
2658
+ templates: skill.templates || [],
2659
+ scripts: skill.scripts || [],
2660
+ examples: skill.examples || [],
2661
+ relatedSkills: skill.relatedSkills || []
2662
+ };
2663
+ }
2664
+ static saveList(section, list, skill) {
2665
+ if (list.length === 0) return;
2666
+ if (section.includes("khi n\xE0o") || section.includes("when")) {
2667
+ skill.whenToUse = list;
2668
+ } else if (section.includes("input")) {
2669
+ skill.inputs = list;
2670
+ } else if (section.includes("output")) {
2671
+ skill.outputs = list;
2672
+ } else if (section.includes("h\u1EA1n ch\u1EBF") || section.includes("limitation")) {
2673
+ skill.toneAndRules = skill.toneAndRules || {};
2674
+ skill.toneAndRules.limitations = list;
2675
+ } else if (section.includes("li\xEAn k\u1EBFt") || section.includes("related")) {
2676
+ skill.relatedSkills = list;
2677
+ }
2678
+ }
2679
+ };
2680
+ }
2681
+ });
2682
+
2683
+ // src/core/skills/skill-manager.ts
2684
+ import path6 from "path";
2685
+ var SkillManager;
2686
+ var init_skill_manager = __esm({
2687
+ "src/core/skills/skill-manager.ts"() {
2688
+ "use strict";
2689
+ init_file_system();
2690
+ init_skill_parser();
2691
+ init_config();
2692
+ SkillManager = class {
2693
+ /**
2694
+ * Find skill path for a given project type and skill name
2695
+ */
2696
+ static async findSkillPath(projectType, skillName, projectPath = ".") {
2697
+ const projectSkillPath = path6.join(
2698
+ projectPath,
2699
+ HERASPEC_DIR_NAME,
2700
+ SKILLS_DIR_NAME,
2701
+ projectType,
2702
+ skillName
2703
+ );
2704
+ if (await FileSystemUtils.fileExists(path6.join(projectSkillPath, "skill.md"))) {
2705
+ return projectSkillPath;
2706
+ }
2707
+ const crossCuttingPath = path6.join(
2708
+ projectPath,
2709
+ HERASPEC_DIR_NAME,
2710
+ SKILLS_DIR_NAME,
2711
+ skillName
2712
+ );
2713
+ if (await FileSystemUtils.fileExists(path6.join(crossCuttingPath, "skill.md"))) {
2714
+ return crossCuttingPath;
2715
+ }
2716
+ return null;
2717
+ }
2718
+ /**
2719
+ * Load skill information
2720
+ */
2721
+ static async loadSkill(projectType, skillName, projectPath = ".") {
2722
+ const skillPath = await this.findSkillPath(projectType, skillName, projectPath);
2723
+ if (!skillPath) {
2724
+ return null;
2725
+ }
2726
+ try {
2727
+ return SkillParser.parseSkill(skillPath, skillName);
2728
+ } catch (error) {
2729
+ console.error(`Failed to load skill "${skillName}": ${error instanceof Error ? error.message : "Unknown error"}`);
2730
+ return null;
2731
+ }
2732
+ }
2733
+ /**
2734
+ * List all available skills
2735
+ */
2736
+ static async listSkills(projectPath = ".") {
2737
+ const skillsDir = path6.join(projectPath, HERASPEC_DIR_NAME, SKILLS_DIR_NAME);
2738
+ if (!await FileSystemUtils.fileExists(skillsDir)) {
2739
+ return [];
2740
+ }
2741
+ const skills = [];
2742
+ const entries = await FileSystemUtils.readDirectory(skillsDir);
2743
+ for (const entry of entries) {
2744
+ const entryPath = path6.join(skillsDir, entry);
2745
+ const stats = await FileSystemUtils.stat(entryPath);
2746
+ if (stats.isDirectory()) {
2747
+ const isProjectType = PROJECT_TYPES.includes(entry);
2748
+ if (isProjectType) {
2749
+ const projectSkills = await this.listSkillsInDirectory(entryPath, entry);
2750
+ skills.push(...projectSkills);
2751
+ } else {
2752
+ const skillMdPath = path6.join(entryPath, "skill.md");
2753
+ if (await FileSystemUtils.fileExists(skillMdPath)) {
2754
+ skills.push({
2755
+ skillName: entry,
2756
+ path: entryPath
2757
+ });
2758
+ }
2759
+ }
2760
+ }
2761
+ }
2762
+ return skills;
2763
+ }
2764
+ /**
2765
+ * Extract skill from task line
2766
+ */
2767
+ static extractSkillFromTask(taskLine) {
2768
+ const match = taskLine.match(/\(projectType:\s*([^,)]+)(?:,\s*skill:\s*([^)]+))?\)/i);
2769
+ if (match) {
2770
+ return {
2771
+ projectType: match[1]?.trim(),
2772
+ skill: match[2]?.trim()
2773
+ };
2774
+ }
2775
+ const skillOnlyMatch = taskLine.match(/\(skill:\s*([^)]+)\)/i);
2776
+ if (skillOnlyMatch) {
2777
+ return {
2778
+ skill: skillOnlyMatch[1]?.trim()
2779
+ };
2780
+ }
2781
+ return null;
2782
+ }
2783
+ static async listSkillsInDirectory(dir, projectType) {
2784
+ const skills = [];
2785
+ const entries = await FileSystemUtils.readDirectory(dir);
2786
+ for (const entry of entries) {
2787
+ const entryPath = path6.join(dir, entry);
2788
+ const stats = await FileSystemUtils.stat(entryPath);
2789
+ if (stats.isDirectory()) {
2790
+ const skillMdPath = path6.join(entryPath, "skill.md");
2791
+ if (await FileSystemUtils.fileExists(skillMdPath)) {
2792
+ skills.push({
2793
+ projectType,
2794
+ skillName: entry,
2795
+ path: entryPath
2796
+ });
2797
+ }
2798
+ }
2799
+ }
2800
+ return skills;
2801
+ }
2802
+ };
2803
+ }
2804
+ });
2805
+
2806
+ // src/core/skills/index.ts
2807
+ var init_skills = __esm({
2808
+ "src/core/skills/index.ts"() {
2809
+ "use strict";
2810
+ init_skill_parser();
2811
+ init_skill_manager();
2812
+ }
2813
+ });
2814
+
2815
+ // src/commands/skill.ts
2816
+ var skill_exports = {};
2817
+ __export(skill_exports, {
2818
+ SkillCommand: () => SkillCommand
2819
+ });
2820
+ import path7 from "path";
2821
+ import ora2 from "ora";
2822
+ import { fileURLToPath } from "url";
2823
+ import { dirname, join } from "path";
2824
+ import { createRequire } from "module";
2825
+ async function getCoreTemplatesDir() {
2826
+ const possiblePaths = [];
2827
+ try {
2828
+ const packageJsonPath = require2.resolve("../package.json");
2829
+ const packageDir = path7.dirname(packageJsonPath);
2830
+ possiblePaths.push(
2831
+ join(packageDir, "src", "core", "templates", "skills"),
2832
+ // Source (when linked, this is the actual source)
2833
+ join(packageDir, "dist", "core", "templates", "skills")
2834
+ // Built (templates copied during build)
2835
+ );
2836
+ } catch {
2837
+ }
2838
+ try {
2839
+ const packageJsonPath = require2.resolve("heraspec/package.json");
2840
+ const packageDir = path7.dirname(packageJsonPath);
2841
+ possiblePaths.push(
2842
+ join(packageDir, "dist", "core", "templates", "skills"),
2843
+ // Built
2844
+ join(packageDir, "src", "core", "templates", "skills")
2845
+ // Source (if available)
2846
+ );
2847
+ } catch {
2848
+ }
2849
+ possiblePaths.push(
2850
+ // Source version (for development) - when running from source: src/commands/skill.ts
2851
+ join(__dirname, "..", "..", "src", "core", "templates", "skills"),
2852
+ // Built version - when running from built: dist/commands/skill.js
2853
+ join(__dirname, "..", "core", "templates", "skills"),
2854
+ // Alternative: from project root (when running from HeraSpec source)
2855
+ join(process.cwd(), "src", "core", "templates", "skills")
2856
+ );
2857
+ for (const possiblePath of possiblePaths) {
2858
+ if (await FileSystemUtils.fileExists(possiblePath)) {
2859
+ return possiblePath;
2860
+ }
2861
+ }
2862
+ return null;
2863
+ }
2864
+ var require2, __filename, __dirname, SkillCommand;
2865
+ var init_skill = __esm({
2866
+ "src/commands/skill.ts"() {
2867
+ "use strict";
2868
+ init_file_system();
2869
+ init_skills();
2870
+ init_config();
2871
+ init_skills_template_map();
2872
+ require2 = createRequire(import.meta.url);
2873
+ __filename = fileURLToPath(import.meta.url);
2874
+ __dirname = dirname(__filename);
2875
+ SkillCommand = class {
2876
+ async list(projectPath = ".") {
2877
+ const skills = await SkillManager.listSkills(projectPath);
2878
+ if (skills.length === 0) {
2879
+ console.log("No skills found. Skills will be created as needed.");
2880
+ console.log("See docs/SKILLS_STRUCTURE_PROPOSAL.md for skill structure.");
2881
+ return;
2882
+ }
2883
+ console.log("\nAvailable Skills:\n");
2884
+ console.log("\u2550".repeat(60));
2885
+ const byProjectType = {};
2886
+ const crossCutting = [];
2887
+ for (const skill of skills) {
2888
+ if (skill.projectType) {
2889
+ if (!byProjectType[skill.projectType]) {
2890
+ byProjectType[skill.projectType] = [];
2891
+ }
2892
+ byProjectType[skill.projectType].push({
2893
+ skillName: skill.skillName,
2894
+ path: skill.path
2895
+ });
2896
+ } else {
2897
+ crossCutting.push({
2898
+ skillName: skill.skillName,
2899
+ path: skill.path
2900
+ });
2901
+ }
2902
+ }
2903
+ for (const [projectType, projectSkills] of Object.entries(byProjectType)) {
2904
+ console.log(`
2905
+ \u{1F4E6} ${projectType}:`);
2906
+ for (const skill of projectSkills) {
2907
+ console.log(` \u2022 ${skill.skillName}`);
2908
+ }
2909
+ }
2910
+ if (crossCutting.length > 0) {
2911
+ console.log(`
2912
+ \u{1F527} Cross-cutting skills:`);
2913
+ for (const skill of crossCutting) {
2914
+ console.log(` \u2022 ${skill.skillName}`);
2915
+ }
2916
+ }
2917
+ console.log("\n" + "\u2550".repeat(60) + "\n");
2918
+ }
2919
+ async show(skillName, projectType, projectPath = ".") {
2920
+ if (!skillName) {
2921
+ console.error("Error: Please specify a skill name");
2922
+ console.log("Usage: heraspec skill show <skill-name> [--project-type <type>]");
2923
+ process.exitCode = 1;
2924
+ return;
2925
+ }
2926
+ let skillInfo = null;
2927
+ if (projectType) {
2928
+ skillInfo = await SkillManager.loadSkill(projectType, skillName, projectPath);
2929
+ } else {
2930
+ const skills = await SkillManager.listSkills(projectPath);
2931
+ const found = skills.find((s) => s.skillName === skillName);
2932
+ if (found) {
2933
+ try {
2934
+ skillInfo = SkillParser.parseSkill(found.path, skillName);
2935
+ } catch (error) {
2936
+ console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
2937
+ process.exitCode = 1;
2938
+ return;
2939
+ }
2940
+ }
2941
+ }
2942
+ if (!skillInfo) {
2943
+ console.error(`Error: Skill "${skillName}" not found`);
2944
+ if (projectType) {
2945
+ console.log(`Searched in: heraspec/skills/${projectType}/${skillName}/`);
2946
+ } else {
2947
+ console.log("Searched in: heraspec/skills/");
2948
+ }
2949
+ process.exitCode = 1;
2950
+ return;
2951
+ }
2952
+ console.log(`
2953
+ \u{1F4DA} Skill: ${skillInfo.name}
2954
+ `);
2955
+ console.log("\u2550".repeat(60));
2956
+ console.log(`
2957
+ \u{1F4CD} Path: ${skillInfo.skillPath}
2958
+ `);
2959
+ if (skillInfo.purpose) {
2960
+ console.log("## Purpose");
2961
+ console.log(skillInfo.purpose);
2962
+ console.log();
2963
+ }
2964
+ if (skillInfo.whenToUse.length > 0) {
2965
+ console.log("## When to Use");
2966
+ skillInfo.whenToUse.forEach((item) => {
2967
+ console.log(`- ${item}`);
2968
+ });
2969
+ console.log();
2970
+ }
2971
+ if (skillInfo.steps.length > 0) {
2972
+ console.log("## Steps");
2973
+ skillInfo.steps.forEach((step, index) => {
2974
+ console.log(`${index + 1}. ${step}`);
2975
+ });
2976
+ console.log();
2977
+ }
2978
+ if (skillInfo.inputs.length > 0) {
2979
+ console.log("## Inputs");
2980
+ skillInfo.inputs.forEach((input) => {
2981
+ console.log(`- ${input}`);
2982
+ });
2983
+ console.log();
2984
+ }
2985
+ if (skillInfo.outputs.length > 0) {
2986
+ console.log("## Outputs");
2987
+ skillInfo.outputs.forEach((output) => {
2988
+ console.log(`- ${output}`);
2989
+ });
2990
+ console.log();
2991
+ }
2992
+ if (skillInfo.templates.length > 0) {
2993
+ console.log("## Available Templates");
2994
+ skillInfo.templates.forEach((template) => {
2995
+ console.log(`- ${template}`);
2996
+ });
2997
+ console.log();
2998
+ }
2999
+ if (skillInfo.scripts.length > 0) {
3000
+ console.log("## Available Scripts");
3001
+ skillInfo.scripts.forEach((script) => {
3002
+ console.log(`- ${script}`);
3003
+ });
3004
+ console.log();
3005
+ }
3006
+ if (skillInfo.toneAndRules.limitations && skillInfo.toneAndRules.limitations.length > 0) {
3007
+ console.log("## Limitations");
3008
+ skillInfo.toneAndRules.limitations.forEach((limitation) => {
3009
+ console.log(`- ${limitation}`);
3010
+ });
3011
+ console.log();
3012
+ }
3013
+ const skillMdPath = path7.join(skillInfo.skillPath, "skill.md");
3014
+ if (await FileSystemUtils.fileExists(skillMdPath)) {
3015
+ console.log("\u2550".repeat(60));
3016
+ console.log("\n## Full skill.md Content\n");
3017
+ const content = await FileSystemUtils.readFile(skillMdPath);
3018
+ console.log(content);
3019
+ }
3020
+ console.log("\n" + "\u2550".repeat(60) + "\n");
3021
+ }
3022
+ async repair(projectPath = ".") {
3023
+ const spinner = ora2("Repairing skills structure...").start();
3024
+ try {
3025
+ const skillsDir = path7.join(projectPath, HERASPEC_DIR_NAME, SKILLS_DIR_NAME);
3026
+ if (!await FileSystemUtils.fileExists(skillsDir)) {
3027
+ spinner.fail('Skills directory does not exist. Run "heraspec init" first.');
3028
+ process.exitCode = 1;
3029
+ return;
3030
+ }
3031
+ const skills = await SkillManager.listSkills(projectPath);
3032
+ let fixed = 0;
3033
+ let errors = 0;
3034
+ for (const skill of skills) {
3035
+ const skillPath = skill.path;
3036
+ const skillMdPath = path7.join(skillPath, "skill.md");
3037
+ if (!await FileSystemUtils.fileExists(skillMdPath)) {
3038
+ spinner.warn(`Missing skill.md in ${skillPath}`);
3039
+ errors++;
3040
+ continue;
3041
+ }
3042
+ const standardDirs = ["templates", "scripts", "examples"];
3043
+ for (const dir of standardDirs) {
3044
+ const dirPath = path7.join(skillPath, dir);
3045
+ if (!await FileSystemUtils.fileExists(dirPath)) {
3046
+ await FileSystemUtils.createDirectory(dirPath);
3047
+ fixed++;
3048
+ }
3049
+ }
3050
+ try {
3051
+ const content = await FileSystemUtils.readFile(skillMdPath);
3052
+ if (!content.includes("## Purpose") && !content.includes("# Skill:")) {
3053
+ spinner.warn(`Invalid skill.md structure in ${skillPath}`);
3054
+ }
3055
+ } catch (error) {
3056
+ spinner.warn(`Cannot read skill.md in ${skillPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
3057
+ errors++;
3058
+ }
3059
+ }
3060
+ if (errors === 0 && fixed === 0) {
3061
+ spinner.succeed("All skills are properly structured");
3062
+ } else {
3063
+ spinner.succeed(`Repaired ${fixed} issues, found ${errors} errors`);
3064
+ }
3065
+ } catch (error) {
3066
+ spinner.fail(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
3067
+ process.exitCode = 1;
3068
+ }
3069
+ }
3070
+ async add(skillName, projectType, projectPath = ".") {
3071
+ if (!skillName) {
3072
+ console.error("Error: Please specify a skill name");
3073
+ console.log("Usage: heraspec skills add <skill-name> [--project-type <type>]");
3074
+ process.exitCode = 1;
3075
+ return;
3076
+ }
3077
+ const spinner = ora2(`Adding skill "${skillName}"...`).start();
3078
+ try {
3079
+ const templateInfo = getSkillTemplateInfo(skillName, projectType);
3080
+ if (!templateInfo) {
3081
+ spinner.fail(`Skill template "${skillName}" not found`);
3082
+ console.log("\nAvailable skills:");
3083
+ const allTemplates = getAllSkillTemplates();
3084
+ for (const { skillName: name, projectType: pt } of allTemplates) {
3085
+ if (pt) {
3086
+ console.log(` - ${name} (projectType: ${pt})`);
3087
+ } else {
3088
+ console.log(` - ${name} (cross-cutting)`);
3089
+ }
3090
+ }
3091
+ process.exitCode = 1;
3092
+ return;
3093
+ }
3094
+ const skillsDir = path7.join(projectPath, HERASPEC_DIR_NAME, SKILLS_DIR_NAME);
3095
+ let destPath;
3096
+ if (templateInfo.isCrossCutting) {
3097
+ destPath = path7.join(skillsDir, skillName);
3098
+ } else {
3099
+ if (!templateInfo.projectType) {
3100
+ spinner.fail("Project type is required for this skill");
3101
+ process.exitCode = 1;
3102
+ return;
3103
+ }
3104
+ let effectiveSkillName = skillName;
3105
+ if (skillName.includes(":")) {
3106
+ const parts = skillName.split(":");
3107
+ if (parts.length === 2 && parts[0] === templateInfo.projectType) {
3108
+ effectiveSkillName = parts[1];
3109
+ }
3110
+ }
3111
+ destPath = path7.join(skillsDir, templateInfo.projectType, effectiveSkillName);
3112
+ const legacyPath = path7.join(skillsDir, templateInfo.projectType, `${templateInfo.projectType}:${effectiveSkillName}`);
3113
+ if (legacyPath !== destPath && await FileSystemUtils.fileExists(legacyPath)) {
3114
+ spinner.info(`Cleaning up legacy prefixed skill folder: ${legacyPath}`);
3115
+ try {
3116
+ await FileSystemUtils.removeDirectory(legacyPath, true);
3117
+ } catch (cleanupError) {
3118
+ spinner.warn(`Could not remove legacy folder ${legacyPath}: ${cleanupError instanceof Error ? cleanupError.message : "Unknown error"}`);
3119
+ }
3120
+ }
3121
+ }
3122
+ const isUpdate = await FileSystemUtils.fileExists(destPath);
3123
+ if (isUpdate) {
3124
+ spinner.info(`Skill "${skillName}" already exists at ${destPath}`);
3125
+ spinner.start(`Removing old version to update with latest...`);
3126
+ try {
3127
+ await FileSystemUtils.removeDirectory(destPath, true);
3128
+ spinner.succeed(`Removed old skill "${skillName}"`);
3129
+ } catch (error) {
3130
+ spinner.fail(`Failed to remove old skill: ${error instanceof Error ? error.message : "Unknown error"}`);
3131
+ process.exitCode = 1;
3132
+ return;
3133
+ }
3134
+ spinner.start(`Adding updated skill "${skillName}"...`);
3135
+ }
3136
+ await FileSystemUtils.createDirectory(destPath);
3137
+ const coreTemplatesDir = await getCoreTemplatesDir();
3138
+ if (!coreTemplatesDir) {
3139
+ spinner.fail("Cannot find HeraSpec templates directory. Make sure you are running from HeraSpec project or have templates installed.");
3140
+ process.exitCode = 1;
3141
+ return;
3142
+ }
3143
+ const templateFile = path7.join(coreTemplatesDir, templateInfo.templateFileName);
3144
+ if (!await FileSystemUtils.fileExists(templateFile)) {
3145
+ spinner.fail(`Template file not found: ${templateFile}`);
3146
+ process.exitCode = 1;
3147
+ return;
3148
+ }
3149
+ await FileSystemUtils.copyFile(templateFile, path7.join(destPath, "skill.md"));
3150
+ if (templateInfo.viFileName) {
3151
+ const viFile = path7.join(coreTemplatesDir, templateInfo.viFileName);
3152
+ if (await FileSystemUtils.fileExists(viFile)) {
3153
+ await FileSystemUtils.copyFile(viFile, path7.join(destPath, "skill.vi.md"));
3154
+ }
3155
+ }
3156
+ if (templateInfo.resourceDirs) {
3157
+ for (const resourceDir of templateInfo.resourceDirs) {
3158
+ const srcResourceDir = path7.join(coreTemplatesDir, resourceDir);
3159
+ const destResourceDir = path7.join(destPath, resourceDir);
3160
+ if (await FileSystemUtils.fileExists(srcResourceDir)) {
3161
+ await FileSystemUtils.copyDirectory(srcResourceDir, destResourceDir);
3162
+ }
3163
+ }
3164
+ }
3165
+ await FileSystemUtils.createDirectory(path7.join(destPath, "templates"));
3166
+ await FileSystemUtils.createDirectory(path7.join(destPath, "scripts"));
3167
+ await FileSystemUtils.createDirectory(path7.join(destPath, "examples"));
3168
+ const successMessage = isUpdate ? `Skill "${skillName}" updated successfully` : `Skill "${skillName}" added successfully`;
3169
+ spinner.succeed(successMessage);
3170
+ console.log(`
3171
+ \u{1F4CD} Location: ${destPath}`);
3172
+ if (isUpdate) {
3173
+ console.log(`
3174
+ \u2728 Skill has been updated with latest features and improvements.`);
3175
+ }
3176
+ console.log(`
3177
+ \u{1F4A1} Run "heraspec skill show ${skillName}${projectType ? ` --project-type ${projectType}` : ""}" to view details
3178
+ `);
3179
+ } catch (error) {
3180
+ spinner.fail(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
3181
+ process.exitCode = 1;
3182
+ }
3183
+ }
3184
+ async update(skillName, projectType, projectPath = ".") {
3185
+ return this.add(skillName, projectType, projectPath);
3186
+ }
3187
+ };
3188
+ }
3189
+ });
3190
+
3191
+ // src/index.ts
3192
+ init_config();
3193
+
110
3194
  // src/core/schemas/base.schema.ts
111
3195
  import { z } from "zod";
112
3196
  var ProjectTypeSchema = z.enum([
@@ -167,115 +3251,50 @@ var DeltaRequirementSchema = z3.object({
167
3251
  name: z3.string(),
168
3252
  description: z3.string(),
169
3253
  scenarios: z3.array(z3.object({
170
- name: z3.string(),
171
- steps: z3.array(z3.string())
172
- })).optional(),
173
- constraints: z3.array(z3.string()).optional()
174
- })
175
- });
176
- var ChangeSchema = z3.object({
177
- name: z3.string(),
178
- proposal: z3.string().min(1),
179
- tasks: z3.array(z3.string()).optional(),
180
- design: z3.string().optional()
181
- });
182
-
183
- // src/core/init.ts
184
- import ora from "ora";
185
- import chalk from "chalk";
186
- import path2 from "path";
187
-
188
- // src/utils/file-system.ts
189
- import { promises as fs } from "fs";
190
- import path from "path";
191
- var FileSystemUtils = class {
192
- static async createDirectory(dirPath) {
193
- await fs.mkdir(dirPath, { recursive: true });
194
- }
195
- static async fileExists(filePath) {
196
- try {
197
- await fs.access(filePath);
198
- return true;
199
- } catch {
200
- return false;
201
- }
202
- }
203
- static async readFile(filePath) {
204
- return await fs.readFile(filePath, "utf-8");
205
- }
206
- static async writeFile(filePath, content) {
207
- await fs.writeFile(filePath, content, "utf-8");
208
- }
209
- static async readDirectory(dirPath) {
210
- return await fs.readdir(dirPath);
211
- }
212
- static async stat(filePath) {
213
- return await fs.stat(filePath);
214
- }
215
- static async copyFile(src, dest) {
216
- await fs.copyFile(src, dest);
217
- }
218
- static async copyDirectory(src, dest) {
219
- await fs.mkdir(dest, { recursive: true });
220
- const entries = await fs.readdir(src, { withFileTypes: true });
221
- for (const entry of entries) {
222
- const srcPath = path.join(src, entry.name);
223
- const destPath = path.join(dest, entry.name);
224
- if (entry.isDirectory()) {
225
- await this.copyDirectory(srcPath, destPath);
226
- } else {
227
- await fs.copyFile(srcPath, destPath);
228
- }
229
- }
230
- }
231
- static async removeFile(filePath) {
232
- await fs.unlink(filePath);
233
- }
234
- static async removeDirectory(dirPath, recursive = true) {
235
- if (typeof fs.rm === "function") {
236
- await fs.rm(dirPath, { recursive, force: true });
237
- } else {
238
- await fs.rmdir(dirPath, { recursive });
239
- }
240
- }
241
- static async moveFile(src, dest) {
242
- await fs.rename(src, dest);
243
- }
244
- static joinPath(...segments) {
245
- return path.join(...segments);
246
- }
247
- static resolvePath(...segments) {
248
- return path.resolve(...segments);
249
- }
250
- static getDirectoryName(filePath) {
251
- return path.dirname(filePath);
252
- }
253
- static getBaseName(filePath) {
254
- return path.basename(filePath);
255
- }
256
- };
3254
+ name: z3.string(),
3255
+ steps: z3.array(z3.string())
3256
+ })).optional(),
3257
+ constraints: z3.array(z3.string()).optional()
3258
+ })
3259
+ });
3260
+ var ChangeSchema = z3.object({
3261
+ name: z3.string(),
3262
+ proposal: z3.string().min(1),
3263
+ tasks: z3.array(z3.string()).optional(),
3264
+ design: z3.string().optional()
3265
+ });
3266
+
3267
+ // src/core/init.ts
3268
+ init_file_system();
3269
+ import ora3 from "ora";
3270
+ import chalk2 from "chalk";
3271
+ import path8 from "path";
257
3272
 
258
3273
  // src/core/templates/index.ts
259
3274
  var TemplateManager = class {
260
3275
  static getProjectTemplate() {
261
3276
  return `# HeraSpec Project
262
3277
 
3278
+ ## \u26A1 Slash Commands (Agent Triggers)
3279
+
3280
+ When the user types messages starting with a slash (/), you MUST intercept them as action triggers rather than conversational queries. Please execute the corresponding terminal commands immediately:
3281
+
3282
+ - /heraspec [args] : Run the heraspec CLI tool with the provided arguments (e.g. /heraspec validate my-change)
3283
+ - /sync : Run heraspec sync <current-change>
3284
+ - /archive : Run heraspec archive <current-change> --yes
3285
+ - /skill [name] : Run heraspec skill add [name]
3286
+ - /hotfix [title] : Run heraspec hotfix [title]
3287
+
3288
+ *If the user types a slash command, do not explain what you are going to do, just run the terminal command and report the output.*
3289
+
263
3290
  ## Overview
264
3291
  Describe your project here.
265
3292
 
266
- ## Project Types
267
- - wordpress-plugin
268
- - wordpress-theme
269
- - perfex-module
270
- - laravel-package
271
- - node-service
272
- - generic-webapp
273
- - backend-api
274
- - frontend-app
275
- - multi-stack
3293
+ ## Architecture
3294
+ Describe the high-level architecture: entry points, module structure, data flow.
276
3295
 
277
- ## Tech Stack
278
- List your technologies here (e.g., PHP 8.1, WordPress 6.0, Laravel 10, etc.)
3296
+ ## Key Dependencies
3297
+ List critical dependencies and their purposes (e.g., better-sqlite3 for memory, commander for CLI).
279
3298
 
280
3299
  ## Conventions
281
3300
  Define coding standards, architectural patterns, and conventions to follow.
@@ -345,6 +3364,8 @@ The following commands are classified by risk level. You **MUST NOT** execute th
345
3364
  - Existing architecture patterns
346
3365
  - Coding standards
347
3366
 
3367
+ > **IGNORE backup files**: Files like \\\`project.back1.md\\\`, \\\`project.back2.md\\\`, etc. are automatic backups created during \\\`heraspec init\\\` updates. NEVER read or reference them unless explicitly asked by the user. Only \\\`project.md\\\` is the source of truth.
3368
+
348
3369
  **Then scaffold:**
349
3370
  - \`heraspec/changes/<slug>/\` - Create proposal.md, tasks.md, design.md (optional)
350
3371
  - \`heraspec/specs/<slug>/\` - Create delta specs here (NOT inside changes folder)
@@ -466,6 +3487,15 @@ The following commands are classified by risk level. You **MUST NOT** execute th
466
3487
  - This merges delta specs into source specs
467
3488
  - Moves change folder to archives
468
3489
 
3490
+ #### Handling Parallel Merge Conflicts
3491
+ If \`heraspec archive\` throws a **Parallel Merge Conflict** error:
3492
+ 1. It means someone else modified the base requirement in the source spec while you were working.
3493
+ 2. **DO NOT PANIC** and **DO NOT OVERWRITE** manually.
3494
+ 3. Run: \`heraspec sync <slug>\`
3495
+ 4. The system will automatically update your fingerprint to match the new source spec and warn you.
3496
+ 5. Review your delta spec again. Make sure your \`MODIFIED/REMOVED\` requirements still make sense given the new source.
3497
+ 6. Once verified, run \`heraspec archive <slug> --yes\` again.
3498
+
469
3499
  ## Spec Format
470
3500
 
471
3501
  Specs must include:
@@ -543,6 +3573,26 @@ Example with multiple skills in one change:
543
3573
  \`\`\`
544
3574
 
545
3575
  **Key rule**: Switch skill.md when switching task groups!
3576
+
3577
+ ## Proactive Memory-Aware Development
3578
+
3579
+ If \\\`heraspec/skills/project-memory/\\\` exists, the agent MUST proactively use memory to maintain project context. DO NOT wait for the user to prompt you to use memory.
3580
+
3581
+ ### CRITICAL RULES FOR AUTOMATIC MEMORY USAGE
3582
+ 1. **Always Check Context First**: At the beginning of a complex task or when encountering an unfamiliar codebase area, you MUST autonomously run \\\`heraspec memory context\\\` or \\\`heraspec memory search "keywords"\\\` BEFORE writing any code.
3583
+ 2. **Auto-Log via Archive**: You DO NOT need to run \\\`heraspec memory log\\\` manually. Every time you successfully implement a feature and run \\\`heraspec archive <change-name>\\\`, the system will automatically read your proposal and log the memory observation under the hood. Just use \\\`heraspec archive\\\` as normal!
3584
+ 3. **Session Summary**: (Optional) When wrapping up a large session, you can run \\\`heraspec memory summarize --request "..." --completed "..."\\\` to compress knowledge, though \\\`archive\\\` already auto-logs completed features.
3585
+
3586
+ ### When to SKIP Memory
3587
+ - Simple bug fixes, typo corrections, formatting changes
3588
+ - Trivial tasks that take < 5 minutes
3589
+ - When doing what the agent's built-in context already covers
3590
+
3591
+ ### Smart Code Exploration
3592
+ If \\\`heraspec/skills/smart-explore/\\\` exists, prefer token-efficient exploration:
3593
+ - \\\`heraspec explore outline <file>\\\` \u2192 View structure (~1K tokens vs ~12K full file)
3594
+ - \\\`heraspec explore search "<query>" <path>\\\` \u2192 Find symbols across codebase
3595
+ - \\\`heraspec explore unfold <file> <symbol>\\\` \u2192 Read just one function
546
3596
  `;
547
3597
  }
548
3598
  static getSkillsSection() {
@@ -613,77 +3663,239 @@ skills: []
613
3663
  };
614
3664
 
615
3665
  // src/core/init.ts
3666
+ init_config();
3667
+ init_skills_template_map();
3668
+ init_memory();
3669
+ import { fileURLToPath as fileURLToPath2 } from "url";
3670
+ import { dirname as dirname2, join as join2 } from "path";
3671
+ import { createRequire as createRequire2 } from "module";
3672
+ var require3 = createRequire2(import.meta.url);
3673
+ var __filename2 = fileURLToPath2(import.meta.url);
3674
+ var __dirname2 = dirname2(__filename2);
616
3675
  var InitCommand = class {
617
3676
  async execute(targetPath = ".") {
618
- const resolvedPath = path2.resolve(targetPath);
619
- const heraspecPath = path2.join(resolvedPath, HERASPEC_DIR_NAME);
3677
+ const resolvedPath = path8.resolve(targetPath);
3678
+ const heraspecPath = path8.join(resolvedPath, HERASPEC_DIR_NAME);
620
3679
  const alreadyInitialized = await FileSystemUtils.fileExists(
621
- path2.join(heraspecPath, HERASPEC_MARKERS.PROJECT_MD)
3680
+ path8.join(heraspecPath, HERASPEC_MARKERS.PROJECT_MD)
622
3681
  );
623
- const spinner = ora({
3682
+ const spinner = ora3({
624
3683
  text: alreadyInitialized ? "Updating HeraSpec..." : "Initializing HeraSpec...",
625
3684
  color: "blue"
626
3685
  }).start();
627
3686
  try {
628
3687
  await FileSystemUtils.createDirectory(heraspecPath);
629
- await FileSystemUtils.createDirectory(path2.join(heraspecPath, SPECS_DIR_NAME));
630
- await FileSystemUtils.createDirectory(path2.join(heraspecPath, CHANGES_DIR_NAME));
631
- await FileSystemUtils.createDirectory(path2.join(heraspecPath, ARCHIVES_DIR_NAME));
632
- await FileSystemUtils.createDirectory(path2.join(heraspecPath, SKILLS_DIR_NAME));
633
- const skillsReadmePath = path2.join(heraspecPath, SKILLS_DIR_NAME, "README.md");
3688
+ await FileSystemUtils.createDirectory(path8.join(heraspecPath, SPECS_DIR_NAME));
3689
+ await FileSystemUtils.createDirectory(path8.join(heraspecPath, CHANGES_DIR_NAME));
3690
+ await FileSystemUtils.createDirectory(path8.join(heraspecPath, ARCHIVES_DIR_NAME));
3691
+ await FileSystemUtils.createDirectory(path8.join(heraspecPath, SKILLS_DIR_NAME));
3692
+ await FileSystemUtils.createDirectory(path8.join(heraspecPath, KNOWLEDGE_DIR_NAME));
3693
+ const skillsReadmePath = path8.join(heraspecPath, SKILLS_DIR_NAME, "README.md");
634
3694
  if (!await FileSystemUtils.fileExists(skillsReadmePath)) {
635
3695
  const skillsReadme = await this.getSkillsReadmeTemplate();
636
3696
  await FileSystemUtils.writeFile(skillsReadmePath, skillsReadme);
637
3697
  }
638
- const uiuxGuidePath = path2.join(heraspecPath, SKILLS_DIR_NAME, "UI_UX_SKILL_QUICK_REFERENCE.md");
3698
+ const uiuxGuidePath = path8.join(heraspecPath, SKILLS_DIR_NAME, "UI_UX_SKILL_QUICK_REFERENCE.md");
639
3699
  if (!await FileSystemUtils.fileExists(uiuxGuidePath)) {
640
3700
  const uiuxGuide = await this.getUIUXQuickReference();
641
3701
  await FileSystemUtils.writeFile(uiuxGuidePath, uiuxGuide);
642
3702
  }
3703
+ await this.deployKnowledge(heraspecPath);
643
3704
  await this.createTemplateFiles(heraspecPath, alreadyInitialized);
644
- const legacyAgentsPath = path2.join(resolvedPath, "AGENTS.md");
645
- const newAgentsPath = path2.join(resolvedPath, HERASPEC_MARKERS.AGENTS_MD);
3705
+ const legacyAgentsPath = path8.join(resolvedPath, "AGENTS.md");
3706
+ const newAgentsPath = path8.join(resolvedPath, HERASPEC_MARKERS.AGENTS_MD);
646
3707
  if (await FileSystemUtils.fileExists(legacyAgentsPath) && !await FileSystemUtils.fileExists(newAgentsPath)) {
647
3708
  await FileSystemUtils.moveFile(legacyAgentsPath, newAgentsPath);
648
3709
  }
649
3710
  await this.updateAgentsFile(newAgentsPath, alreadyInitialized);
3711
+ if (alreadyInitialized) {
3712
+ await this.updateInstalledSkills(heraspecPath);
3713
+ }
3714
+ await this.checkAndBootstrapMemory(heraspecPath, resolvedPath, spinner);
650
3715
  await this.updateRelatedMarkdownFiles(resolvedPath);
651
- spinner.succeed(
652
- chalk.green(
653
- alreadyInitialized ? "HeraSpec updated successfully" : "HeraSpec initialized successfully"
3716
+ spinner.stop();
3717
+ try {
3718
+ const memoryCommand = new MemoryCommand();
3719
+ await memoryCommand.index({ depth: "3", yes: true }, resolvedPath);
3720
+ await memoryCommand.context({ output: "file" }, resolvedPath);
3721
+ } catch (err) {
3722
+ }
3723
+ console.log();
3724
+ console.log(
3725
+ chalk2.green(
3726
+ alreadyInitialized ? "\u2714 HeraSpec updated successfully" : "\u2714 HeraSpec initialized successfully"
654
3727
  )
655
3728
  );
656
3729
  console.log();
657
- console.log(chalk.cyan("Next steps:"));
3730
+ console.log(chalk2.cyan("Next steps:"));
658
3731
  console.log(
659
- chalk.gray("1. Review and update heraspec/project.md with your project details")
3732
+ chalk2.gray("1. Review and update heraspec/project.md with your project details")
660
3733
  );
661
3734
  console.log(
662
- chalk.gray('2. Create your first change: "Create a HeraSpec change to..."')
3735
+ chalk2.gray('2. Create your first change: "Create a HeraSpec change to..."')
663
3736
  );
664
3737
  console.log(
665
- chalk.gray("3. List changes: heraspec list")
3738
+ chalk2.gray("3. List changes: heraspec list")
666
3739
  );
667
3740
  } catch (error) {
668
- spinner.fail(chalk.red(`Error: ${error.message}`));
3741
+ spinner.fail(chalk2.red(`Error: ${error.message}`));
669
3742
  throw error;
670
3743
  }
671
3744
  }
672
3745
  async createTemplateFiles(heraspecPath, skipExisting) {
673
- const projectMdPath = path2.join(heraspecPath, HERASPEC_MARKERS.PROJECT_MD);
674
- const configYamlPath = path2.join(heraspecPath, HERASPEC_MARKERS.CONFIG_YAML);
675
- if (!await FileSystemUtils.fileExists(projectMdPath) || !skipExisting) {
676
- await FileSystemUtils.writeFile(
677
- projectMdPath,
678
- TemplateManager.getProjectTemplate()
679
- );
3746
+ const projectMdPath = path8.join(heraspecPath, HERASPEC_MARKERS.PROJECT_MD);
3747
+ const configYamlPath = path8.join(heraspecPath, HERASPEC_MARKERS.CONFIG_YAML);
3748
+ const newProjectTemplate = TemplateManager.getProjectTemplate();
3749
+ const projectExists = await FileSystemUtils.fileExists(projectMdPath);
3750
+ if (!projectExists) {
3751
+ await FileSystemUtils.writeFile(projectMdPath, newProjectTemplate);
3752
+ } else if (skipExisting) {
3753
+ const existingContent = await FileSystemUtils.readFile(projectMdPath);
3754
+ const hasChanges = this.detectTemplateChanges(existingContent, newProjectTemplate);
3755
+ if (hasChanges) {
3756
+ const backupPath = await this.createNumberedBackup(projectMdPath, heraspecPath);
3757
+ const backupName = path8.basename(backupPath);
3758
+ const mergedContent = this.buildMergedProjectMd(existingContent, newProjectTemplate, backupName);
3759
+ await FileSystemUtils.writeFile(projectMdPath, mergedContent);
3760
+ }
3761
+ } else {
3762
+ await FileSystemUtils.writeFile(projectMdPath, newProjectTemplate);
680
3763
  }
681
- if (!await FileSystemUtils.fileExists(configYamlPath) || !skipExisting) {
682
- await FileSystemUtils.writeFile(
683
- configYamlPath,
684
- TemplateManager.getConfigTemplate()
685
- );
3764
+ if (!await FileSystemUtils.fileExists(configYamlPath)) {
3765
+ if (projectExists) {
3766
+ await this.migrateLegacyProjectMd(projectMdPath, configYamlPath);
3767
+ } else {
3768
+ await FileSystemUtils.writeFile(
3769
+ configYamlPath,
3770
+ TemplateManager.getConfigTemplate()
3771
+ );
3772
+ }
3773
+ }
3774
+ }
3775
+ /**
3776
+ * Migrate legacy project.md (extract technical configs to config.yaml)
3777
+ */
3778
+ async migrateLegacyProjectMd(projectMdPath, configYamlPath) {
3779
+ const content = await FileSystemUtils.readFile(projectMdPath);
3780
+ let newYaml = `projectType: generic-webapp
3781
+ projectName: "HeraSpec Project"
3782
+ description: "A new project using HeraSpec"
3783
+ skills: []
3784
+ `;
3785
+ const projectTypeMatch = content.match(/## Project Types\\s*\\n\\s*-\\s*([a-zA-Z0-9-]+)/);
3786
+ if (projectTypeMatch && projectTypeMatch[1]) {
3787
+ newYaml = newYaml.replace(/projectType:.*/, `projectType: ${projectTypeMatch[1]}`);
3788
+ }
3789
+ const stackMatch = content.match(/## Tech Stack\\s*\\n([^#]+)/);
3790
+ if (stackMatch && stackMatch[1]) {
3791
+ const stackItems = stackMatch[1].split("\\n").filter((line) => line.trim().startsWith("-")).map((line) => line.replace("-", "").trim());
3792
+ if (stackItems.length > 0) {
3793
+ newYaml += `techStack:
3794
+ ` + stackItems.map((item) => ` - "${item}"`).join("\n") + "\n";
3795
+ }
3796
+ }
3797
+ await FileSystemUtils.writeFile(configYamlPath, newYaml);
3798
+ let updatedMd = content;
3799
+ updatedMd = updatedMd.replace(/## Project Types[\\s\\S]*?(?=##|$)/, "");
3800
+ updatedMd = updatedMd.replace(/## Tech Stack[\\s\\S]*?(?=##|$)/, "");
3801
+ if (!updatedMd.includes("<!-- HeraSpec Update: Migrated config to config.yaml -->")) {
3802
+ updatedMd = `<!-- HeraSpec Update: Migrated config to config.yaml -->
3803
+ ` + updatedMd.trimStart();
3804
+ }
3805
+ await FileSystemUtils.writeFile(projectMdPath, updatedMd);
3806
+ }
3807
+ /**
3808
+ * Detect if the new template has sections that the existing file lacks
3809
+ */
3810
+ detectTemplateChanges(existingContent, newTemplate) {
3811
+ const sectionRegex = /^## .+$/gm;
3812
+ const existingSections = new Set(
3813
+ (existingContent.match(sectionRegex) || []).map((s) => s.trim().toLowerCase())
3814
+ );
3815
+ const newSections = (newTemplate.match(sectionRegex) || []).map((s) => s.trim().toLowerCase());
3816
+ for (const section of newSections) {
3817
+ if (!existingSections.has(section)) {
3818
+ return true;
3819
+ }
3820
+ }
3821
+ return false;
3822
+ }
3823
+ /**
3824
+ * Create numbered backup: project.back1.md, project.back2.md, etc.
3825
+ */
3826
+ async createNumberedBackup(filePath, dirPath) {
3827
+ const ext = path8.extname(filePath);
3828
+ const base = path8.basename(filePath, ext);
3829
+ let backupNumber = 1;
3830
+ while (true) {
3831
+ const backupPath = path8.join(dirPath, `${base}.back${backupNumber}${ext}`);
3832
+ if (!await FileSystemUtils.fileExists(backupPath)) {
3833
+ await FileSystemUtils.copyFile(filePath, backupPath);
3834
+ return backupPath;
3835
+ }
3836
+ backupNumber++;
3837
+ if (backupNumber > 99) break;
3838
+ }
3839
+ const fallbackPath = path8.join(dirPath, `${base}.back99${ext}`);
3840
+ await FileSystemUtils.copyFile(filePath, fallbackPath);
3841
+ return fallbackPath;
3842
+ }
3843
+ /**
3844
+ * Build merged project.md:
3845
+ * - Keeps all existing user content (descriptions, tech stack, conventions)
3846
+ * - Adds any NEW sections from the template that don't exist yet
3847
+ * - Adds a merge note at the top referencing the backup
3848
+ */
3849
+ buildMergedProjectMd(existingContent, newTemplate, backupFileName) {
3850
+ const existingSections = this.parseSections(existingContent);
3851
+ const templateSections = this.parseSections(newTemplate);
3852
+ const mergedParts = [];
3853
+ const existingSectionHeaders = new Set(
3854
+ existingSections.map((s) => s.header.trim().toLowerCase())
3855
+ );
3856
+ const mergeNote = `<!-- HeraSpec Update: Template updated. Previous version backed up to "${backupFileName}". New sections (if any) have been appended below. -->
3857
+ `;
3858
+ mergedParts.push(mergeNote);
3859
+ mergedParts.push(existingContent.trimEnd());
3860
+ const newSections = [];
3861
+ for (const section of templateSections) {
3862
+ if (!existingSectionHeaders.has(section.header.trim().toLowerCase())) {
3863
+ newSections.push(section);
3864
+ }
3865
+ }
3866
+ if (newSections.length > 0) {
3867
+ mergedParts.push("\n\n<!-- New sections added by HeraSpec update -->");
3868
+ for (const section of newSections) {
3869
+ mergedParts.push(`
3870
+ ${section.header}
3871
+ ${section.content.trimEnd()}`);
3872
+ }
3873
+ }
3874
+ return mergedParts.join("\n").trimEnd() + "\n";
3875
+ }
3876
+ /**
3877
+ * Parse markdown sections (## headers) from content
3878
+ */
3879
+ parseSections(content) {
3880
+ const sections = [];
3881
+ const lines = content.split("\n");
3882
+ let currentHeader = "";
3883
+ let currentContent = [];
3884
+ for (const line of lines) {
3885
+ if (line.match(/^## /)) {
3886
+ if (currentHeader) {
3887
+ sections.push({ header: currentHeader, content: currentContent.join("\n") });
3888
+ }
3889
+ currentHeader = line;
3890
+ currentContent = [];
3891
+ } else if (currentHeader) {
3892
+ currentContent.push(line);
3893
+ }
686
3894
  }
3895
+ if (currentHeader) {
3896
+ sections.push({ header: currentHeader, content: currentContent.join("\n") });
3897
+ }
3898
+ return sections;
687
3899
  }
688
3900
  async updateAgentsFile(agentsPath, alreadyInitialized) {
689
3901
  const skillsSectionMarker = "## Skills System";
@@ -719,16 +3931,62 @@ var InitCommand = class {
719
3931
  existingContent = safetySection + "\n\n" + existingContent;
720
3932
  }
721
3933
  }
3934
+ const coreWorkflowMarker = "## Core Workflow";
3935
+ const coreWorkflowEndMarker = "## Skills System";
3936
+ const coreWorkflowEndIndex = fullTemplate.indexOf(coreWorkflowEndMarker);
3937
+ const coreWorkflowStartIndex = fullTemplate.indexOf(coreWorkflowMarker);
3938
+ if (coreWorkflowStartIndex !== -1 && coreWorkflowEndIndex !== -1) {
3939
+ const coreWorkflowSection = fullTemplate.substring(coreWorkflowStartIndex, coreWorkflowEndIndex).trim();
3940
+ if (existingContent.includes(coreWorkflowMarker)) {
3941
+ existingContent = this.replaceSection(existingContent, coreWorkflowMarker, coreWorkflowSection);
3942
+ } else {
3943
+ const safetyEndPos = existingContent.indexOf("\n## ", existingContent.indexOf(safetyMarker) + safetyMarker.length);
3944
+ if (safetyEndPos !== -1) {
3945
+ const before = existingContent.substring(0, safetyEndPos).trimEnd();
3946
+ const after = existingContent.substring(safetyEndPos);
3947
+ existingContent = before + "\n\n" + coreWorkflowSection + "\n\n" + after;
3948
+ }
3949
+ }
3950
+ }
722
3951
  let updatedContent = existingContent;
723
3952
  if (existingContent.includes(skillsSectionMarker) || existingContent.includes("## Skills system")) {
724
3953
  updatedContent = this.replaceSkillsSection(existingContent, latestSkillsSection);
725
3954
  } else {
726
3955
  updatedContent = this.appendSkillsSection(existingContent, latestSkillsSection);
727
3956
  }
3957
+ const oldMemoryMarker = "## Memory-Aware Development";
3958
+ const newMemoryMarker = "## Proactive Memory-Aware Development";
3959
+ const memoryStartIndex = fullTemplate.indexOf(newMemoryMarker);
3960
+ if (memoryStartIndex !== -1) {
3961
+ let memoryEndIndex = fullTemplate.indexOf("\n## ", memoryStartIndex + newMemoryMarker.length);
3962
+ if (memoryEndIndex === -1) memoryEndIndex = fullTemplate.length;
3963
+ const memorySection = fullTemplate.substring(memoryStartIndex, memoryEndIndex).trim();
3964
+ if (updatedContent.includes(oldMemoryMarker)) {
3965
+ updatedContent = this.replaceSection(updatedContent, oldMemoryMarker, memorySection);
3966
+ } else if (updatedContent.includes(newMemoryMarker)) {
3967
+ updatedContent = this.replaceSection(updatedContent, newMemoryMarker, memorySection);
3968
+ } else {
3969
+ updatedContent = updatedContent.trimEnd() + "\n\n" + memorySection;
3970
+ }
3971
+ }
728
3972
  if (updatedContent !== existingContent) {
729
3973
  await FileSystemUtils.writeFile(agentsPath, updatedContent);
730
3974
  }
731
3975
  }
3976
+ /**
3977
+ * Generic section replacer: replace content from marker to next ## header
3978
+ */
3979
+ replaceSection(content, sectionMarker, newSection) {
3980
+ const startIndex = content.indexOf(sectionMarker);
3981
+ if (startIndex === -1) return content;
3982
+ let endIndex = content.indexOf("\n## ", startIndex + sectionMarker.length);
3983
+ if (endIndex === -1) {
3984
+ endIndex = content.length;
3985
+ }
3986
+ const before = content.substring(0, startIndex).trimEnd();
3987
+ const after = content.substring(endIndex);
3988
+ return before + "\n\n" + newSection + (after.trimStart().startsWith("\n") ? "" : "\n\n") + after;
3989
+ }
732
3990
  replaceSkillsSection(existingContent, newSkillsSection) {
733
3991
  const startMarkers = ["## Skills System", "## Skills system", "### Skills System", "### Skills system"];
734
3992
  let startIndex = -1;
@@ -786,6 +4044,276 @@ var InitCommand = class {
786
4044
  async getSkillsSection() {
787
4045
  return TemplateManager.getSkillsSection();
788
4046
  }
4047
+ /**
4048
+ * Resolve core templates directory (same logic as skill.ts)
4049
+ */
4050
+ async getCoreTemplatesDir() {
4051
+ const possiblePaths = [];
4052
+ try {
4053
+ const packageJsonPath = require3.resolve("../package.json");
4054
+ const packageDir = path8.dirname(packageJsonPath);
4055
+ possiblePaths.push(
4056
+ join2(packageDir, "src", "core", "templates", "skills"),
4057
+ join2(packageDir, "dist", "core", "templates", "skills")
4058
+ );
4059
+ } catch {
4060
+ }
4061
+ try {
4062
+ const packageJsonPath = require3.resolve("heraspec/package.json");
4063
+ const packageDir = path8.dirname(packageJsonPath);
4064
+ possiblePaths.push(
4065
+ join2(packageDir, "dist", "core", "templates", "skills"),
4066
+ join2(packageDir, "src", "core", "templates", "skills")
4067
+ );
4068
+ } catch {
4069
+ }
4070
+ possiblePaths.push(
4071
+ join2(__dirname2, "..", "..", "src", "core", "templates", "skills"),
4072
+ join2(__dirname2, "..", "core", "templates", "skills"),
4073
+ join2(process.cwd(), "src", "core", "templates", "skills")
4074
+ );
4075
+ for (const p of possiblePaths) {
4076
+ if (await FileSystemUtils.fileExists(p)) return p;
4077
+ }
4078
+ return null;
4079
+ }
4080
+ /**
4081
+ * Update installed built-in skills on re-init.
4082
+ *
4083
+ * Rules:
4084
+ * - Only update skills that exist in SKILL_TEMPLATE_MAP (built-in / known skills)
4085
+ * - Skip skills NOT in the template map (custom skills added by the project)
4086
+ * - For each matching skill: update skill.md + re-copy resourceDirs from template
4087
+ * but PRESERVE templates/, scripts/, examples/ sub-folders added by user
4088
+ */
4089
+ async updateInstalledSkills(heraspecPath) {
4090
+ const skillsDir = path8.join(heraspecPath, SKILLS_DIR_NAME);
4091
+ if (!await FileSystemUtils.fileExists(skillsDir)) return;
4092
+ const coreTemplatesDir = await this.getCoreTemplatesDir();
4093
+ if (!coreTemplatesDir) return;
4094
+ const allTemplates = getAllSkillTemplates();
4095
+ let updatedCount = 0;
4096
+ let skippedCount = 0;
4097
+ const installedSkillPaths = await this.collectInstalledSkillPaths(skillsDir);
4098
+ for (const { skillPath, skillName, projectType } of installedSkillPaths) {
4099
+ const templateInfo = getSkillTemplateInfo(skillName, projectType);
4100
+ if (!templateInfo) {
4101
+ skippedCount++;
4102
+ continue;
4103
+ }
4104
+ const templateFile = path8.join(coreTemplatesDir, templateInfo.templateFileName);
4105
+ if (!await FileSystemUtils.fileExists(templateFile)) {
4106
+ skippedCount++;
4107
+ continue;
4108
+ }
4109
+ await FileSystemUtils.copyFile(templateFile, path8.join(skillPath, "skill.md"));
4110
+ if (templateInfo.viFileName) {
4111
+ const viFile = path8.join(coreTemplatesDir, templateInfo.viFileName);
4112
+ if (await FileSystemUtils.fileExists(viFile)) {
4113
+ await FileSystemUtils.copyFile(viFile, path8.join(skillPath, "skill.vi.md"));
4114
+ }
4115
+ }
4116
+ if (templateInfo.resourceDirs) {
4117
+ for (const resourceDir of templateInfo.resourceDirs) {
4118
+ const srcResourceDir = path8.join(coreTemplatesDir, resourceDir);
4119
+ const destResourceDir = path8.join(skillPath, resourceDir);
4120
+ if (await FileSystemUtils.fileExists(srcResourceDir)) {
4121
+ if (await FileSystemUtils.fileExists(destResourceDir)) {
4122
+ await FileSystemUtils.removeDirectory(destResourceDir, true);
4123
+ }
4124
+ await FileSystemUtils.copyDirectory(srcResourceDir, destResourceDir);
4125
+ }
4126
+ }
4127
+ }
4128
+ for (const dir of ["templates", "scripts", "examples"]) {
4129
+ await FileSystemUtils.createDirectory(path8.join(skillPath, dir));
4130
+ }
4131
+ updatedCount++;
4132
+ }
4133
+ if (updatedCount > 0) {
4134
+ console.log(chalk2.gray(` \u2713 Updated ${updatedCount} built-in skill(s)${skippedCount > 0 ? `, skipped ${skippedCount} custom skill(s)` : ""}`));
4135
+ }
4136
+ }
4137
+ /**
4138
+ * Walk heraspec/skills/ and return all installed skill paths with metadata
4139
+ */
4140
+ async collectInstalledSkillPaths(skillsDir) {
4141
+ const result = [];
4142
+ const entries = await FileSystemUtils.readDirectory(skillsDir);
4143
+ const knownProjectTypes = [
4144
+ "wordpress",
4145
+ "wordpress-plugin",
4146
+ "wordpress-theme",
4147
+ "perfex-module",
4148
+ "laravel-package",
4149
+ "node-service",
4150
+ "generic-webapp",
4151
+ "backend-api",
4152
+ "frontend-app",
4153
+ "multi-stack"
4154
+ ];
4155
+ for (const entry of entries) {
4156
+ const entryPath = path8.join(skillsDir, entry);
4157
+ const stats = await FileSystemUtils.stat(entryPath);
4158
+ if (!stats.isDirectory()) continue;
4159
+ if (knownProjectTypes.includes(entry)) {
4160
+ const subEntries = await FileSystemUtils.readDirectory(entryPath);
4161
+ for (const sub of subEntries) {
4162
+ const subPath = path8.join(entryPath, sub);
4163
+ const subStats = await FileSystemUtils.stat(subPath);
4164
+ if (subStats.isDirectory() && await FileSystemUtils.fileExists(path8.join(subPath, "skill.md"))) {
4165
+ result.push({ skillPath: subPath, skillName: sub, projectType: entry });
4166
+ }
4167
+ }
4168
+ } else {
4169
+ if (await FileSystemUtils.fileExists(path8.join(entryPath, "skill.md"))) {
4170
+ result.push({ skillPath: entryPath, skillName: entry });
4171
+ }
4172
+ }
4173
+ }
4174
+ return result;
4175
+ }
4176
+ /**
4177
+ * Prompt user to bootstrap memory if historical specs are found
4178
+ */
4179
+ async checkAndBootstrapMemory(heraspecPath, projectPath, spinner) {
4180
+ const memorySkillDir = path8.join(heraspecPath, SKILLS_DIR_NAME, "project-memory");
4181
+ if (!await FileSystemUtils.fileExists(memorySkillDir)) {
4182
+ spinner.stop();
4183
+ try {
4184
+ const { SkillCommand: SkillCommand2 } = await Promise.resolve().then(() => (init_skill(), skill_exports));
4185
+ const skillCommand = new SkillCommand2();
4186
+ console.log(chalk2.cyan('\n\u{1F4E6} Auto-installing recommended skill: "project-memory"'));
4187
+ await skillCommand.add("project-memory", void 0, projectPath);
4188
+ console.log();
4189
+ } catch (err) {
4190
+ spinner.start();
4191
+ return;
4192
+ }
4193
+ spinner.start();
4194
+ }
4195
+ const specsDir = path8.join(heraspecPath, SPECS_DIR_NAME);
4196
+ const archivesDir = path8.join(heraspecPath, ARCHIVES_DIR_NAME);
4197
+ let hasHistoricalData = false;
4198
+ for (const dir of [specsDir, archivesDir]) {
4199
+ if (await FileSystemUtils.fileExists(dir)) {
4200
+ const entries = await FileSystemUtils.readDirectory(dir);
4201
+ if (entries.some((e) => e.endsWith(".md"))) {
4202
+ hasHistoricalData = true;
4203
+ break;
4204
+ }
4205
+ if (!hasHistoricalData) {
4206
+ for (const e of entries) {
4207
+ const fullPath = path8.join(dir, e);
4208
+ const st = await FileSystemUtils.stat(fullPath);
4209
+ if (st.isDirectory()) {
4210
+ const subEntries = await FileSystemUtils.readDirectory(fullPath);
4211
+ if (subEntries.some((sub) => sub.endsWith(".md"))) {
4212
+ hasHistoricalData = true;
4213
+ break;
4214
+ }
4215
+ }
4216
+ }
4217
+ }
4218
+ }
4219
+ }
4220
+ if (!hasHistoricalData) return;
4221
+ const memoryDbPath = path8.join(heraspecPath, "memory", "heraspec-memory.db");
4222
+ if (await FileSystemUtils.fileExists(memoryDbPath)) {
4223
+ return;
4224
+ }
4225
+ spinner.stop();
4226
+ console.log(chalk2.cyan("\n\u{1F4A1} Tip: Your project has historical specs."));
4227
+ const { confirm } = await import("@inquirer/prompts");
4228
+ const answer = await confirm({
4229
+ message: 'Would you like to bootstrap the "project-memory" system from these existing specs?',
4230
+ default: true
4231
+ });
4232
+ if (answer) {
4233
+ try {
4234
+ const memoryCommand = new MemoryCommand();
4235
+ await memoryCommand.bootstrap({ yes: true }, projectPath);
4236
+ } catch (error) {
4237
+ console.log(chalk2.red(`Bootstrap failed: ${error instanceof Error ? error.message : "Unknown"}`));
4238
+ }
4239
+ }
4240
+ spinner.start();
4241
+ }
4242
+ /**
4243
+ * Get the knowledge source directory from CLI package
4244
+ */
4245
+ async getKnowledgeSourceDir() {
4246
+ const possiblePaths = [];
4247
+ try {
4248
+ const packageJsonPath = require3.resolve("../package.json");
4249
+ const packageDir = path8.dirname(packageJsonPath);
4250
+ possiblePaths.push(
4251
+ join2(packageDir, "src", "core", "templates", "skills", "knowledge"),
4252
+ join2(packageDir, "dist", "core", "templates", "skills", "knowledge")
4253
+ );
4254
+ } catch {
4255
+ }
4256
+ try {
4257
+ const packageJsonPath = require3.resolve("heraspec/package.json");
4258
+ const packageDir = path8.dirname(packageJsonPath);
4259
+ possiblePaths.push(
4260
+ join2(packageDir, "dist", "core", "templates", "skills", "knowledge"),
4261
+ join2(packageDir, "src", "core", "templates", "skills", "knowledge")
4262
+ );
4263
+ } catch {
4264
+ }
4265
+ possiblePaths.push(
4266
+ join2(__dirname2, "..", "..", "src", "core", "templates", "skills", "knowledge"),
4267
+ join2(__dirname2, "..", "core", "templates", "skills", "knowledge"),
4268
+ join2(process.cwd(), "src", "core", "templates", "skills", "knowledge")
4269
+ );
4270
+ for (const possiblePath of possiblePaths) {
4271
+ if (await FileSystemUtils.fileExists(possiblePath)) {
4272
+ return possiblePath;
4273
+ }
4274
+ }
4275
+ return null;
4276
+ }
4277
+ /**
4278
+ * Deploy/update built-in knowledge base, preserving custom/ directory
4279
+ */
4280
+ async deployKnowledge(heraspecPath) {
4281
+ const knowledgeDir = path8.join(heraspecPath, KNOWLEDGE_DIR_NAME);
4282
+ const sourceDir = await this.getKnowledgeSourceDir();
4283
+ if (!sourceDir) {
4284
+ return;
4285
+ }
4286
+ const builtinFiles = ["index.json", "README.md"];
4287
+ for (const file of builtinFiles) {
4288
+ const srcFile = path8.join(sourceDir, file);
4289
+ if (await FileSystemUtils.fileExists(srcFile)) {
4290
+ await FileSystemUtils.copyFile(srcFile, path8.join(knowledgeDir, file));
4291
+ }
4292
+ }
4293
+ const builtinCategories = ["frameworks", "apis", "platforms"];
4294
+ for (const category of builtinCategories) {
4295
+ const srcCategory = path8.join(sourceDir, category);
4296
+ if (await FileSystemUtils.fileExists(srcCategory)) {
4297
+ const destCategory = path8.join(knowledgeDir, category);
4298
+ if (await FileSystemUtils.fileExists(destCategory)) {
4299
+ await FileSystemUtils.removeDirectory(destCategory, true);
4300
+ }
4301
+ await FileSystemUtils.copyDirectory(srcCategory, destCategory);
4302
+ }
4303
+ }
4304
+ const customDir = path8.join(knowledgeDir, "custom");
4305
+ if (!await FileSystemUtils.fileExists(customDir)) {
4306
+ await FileSystemUtils.createDirectory(customDir);
4307
+ await FileSystemUtils.writeFile(
4308
+ path8.join(customDir, "index.json"),
4309
+ JSON.stringify({
4310
+ version: "1.0",
4311
+ description: "Custom knowledge entries \u2014 managed by user, never overwritten by CLI",
4312
+ entries: []
4313
+ }, null, 2)
4314
+ );
4315
+ }
4316
+ }
789
4317
  async getSkillsReadmeTemplate() {
790
4318
  return `# Skills Directory
791
4319
 
@@ -1082,7 +4610,7 @@ After copying UI/UX skill to your project, see:
1082
4610
  * Update related markdown files in the project (README.md, etc.)
1083
4611
  */
1084
4612
  async updateRelatedMarkdownFiles(projectPath) {
1085
- const readmePath = path2.join(projectPath, "README.md");
4613
+ const readmePath = path8.join(projectPath, "README.md");
1086
4614
  if (await FileSystemUtils.fileExists(readmePath)) {
1087
4615
  await this.updateReadmeFile(readmePath);
1088
4616
  }
@@ -1214,10 +4742,12 @@ For more information, see the [HeraSpec documentation](https://github.com/your-o
1214
4742
  };
1215
4743
 
1216
4744
  // src/core/list.ts
1217
- import path3 from "path";
4745
+ init_file_system();
4746
+ init_config();
4747
+ import path9 from "path";
1218
4748
  var ListCommand = class {
1219
4749
  async execute(targetPath = ".", mode = "changes") {
1220
- const heraspecPath = path3.join(targetPath, HERASPEC_DIR_NAME);
4750
+ const heraspecPath = path9.join(targetPath, HERASPEC_DIR_NAME);
1221
4751
  if (mode === "changes") {
1222
4752
  await this.listChanges(heraspecPath);
1223
4753
  } else {
@@ -1225,7 +4755,7 @@ var ListCommand = class {
1225
4755
  }
1226
4756
  }
1227
4757
  async listChanges(heraspecPath) {
1228
- const changesDir = path3.join(heraspecPath, CHANGES_DIR_NAME);
4758
+ const changesDir = path9.join(heraspecPath, CHANGES_DIR_NAME);
1229
4759
  try {
1230
4760
  await FileSystemUtils.stat(changesDir);
1231
4761
  } catch {
@@ -1235,7 +4765,7 @@ var ListCommand = class {
1235
4765
  const entries = await FileSystemUtils.readDirectory(changesDir);
1236
4766
  const changeDirs = [];
1237
4767
  for (const entry of entries) {
1238
- const entryPath = path3.join(changesDir, entry);
4768
+ const entryPath = path9.join(changesDir, entry);
1239
4769
  const stats = await FileSystemUtils.stat(entryPath);
1240
4770
  if (stats.isDirectory() && entry !== ARCHIVES_DIR_NAME) {
1241
4771
  changeDirs.push(entry);
@@ -1254,7 +4784,7 @@ var ListCommand = class {
1254
4784
  console.log();
1255
4785
  }
1256
4786
  async listSpecs(heraspecPath) {
1257
- const specsDir = path3.join(heraspecPath, SPECS_DIR_NAME);
4787
+ const specsDir = path9.join(heraspecPath, SPECS_DIR_NAME);
1258
4788
  try {
1259
4789
  await FileSystemUtils.stat(specsDir);
1260
4790
  } catch {
@@ -1278,7 +4808,7 @@ var ListCommand = class {
1278
4808
  const specs = [];
1279
4809
  const entries = await FileSystemUtils.readDirectory(dir);
1280
4810
  for (const entry of entries) {
1281
- const entryPath = path3.join(dir, entry);
4811
+ const entryPath = path9.join(dir, entry);
1282
4812
  const stats = await FileSystemUtils.stat(entryPath);
1283
4813
  if (stats.isDirectory()) {
1284
4814
  const subSpecs = await this.findSpecFiles(
@@ -1295,9 +4825,12 @@ var ListCommand = class {
1295
4825
  };
1296
4826
 
1297
4827
  // src/core/archive.ts
1298
- import path4 from "path";
1299
- import ora2 from "ora";
1300
- import chalk2 from "chalk";
4828
+ init_file_system();
4829
+ init_config();
4830
+ import path10 from "path";
4831
+ import ora4 from "ora";
4832
+ import { createHash } from "crypto";
4833
+ import chalk3 from "chalk";
1301
4834
 
1302
4835
  // src/core/parsers/markdown-parser.ts
1303
4836
  var MarkdownParser = class _MarkdownParser {
@@ -1461,7 +4994,7 @@ var ArchiveCommand = class {
1461
4994
  process.exitCode = 1;
1462
4995
  return;
1463
4996
  }
1464
- const changePath = path4.join(".", HERASPEC_DIR_NAME, CHANGES_DIR_NAME, changeName);
4997
+ const changePath = path10.join(".", HERASPEC_DIR_NAME, CHANGES_DIR_NAME, changeName);
1465
4998
  if (!await FileSystemUtils.fileExists(changePath)) {
1466
4999
  console.error(`Error: Change "${changeName}" not found`);
1467
5000
  process.exitCode = 1;
@@ -1475,13 +5008,13 @@ This will archive "${changeName}" and merge delta specs into source specs.`);
1475
5008
  process.exitCode = 1;
1476
5009
  return;
1477
5010
  }
1478
- const spinner = ora2({
5011
+ const spinner = ora4({
1479
5012
  text: `Archiving change "${changeName}"...`,
1480
5013
  color: "blue"
1481
5014
  }).start();
1482
5015
  try {
1483
5016
  await this.mergeDeltaSpecs(changePath, changeName);
1484
- const specsDir = path4.join(
5017
+ const specsDir = path10.join(
1485
5018
  ".",
1486
5019
  HERASPEC_DIR_NAME,
1487
5020
  SPECS_DIR_NAME,
@@ -1490,7 +5023,7 @@ This will archive "${changeName}" and merge delta specs into source specs.`);
1490
5023
  if (await FileSystemUtils.fileExists(specsDir)) {
1491
5024
  await FileSystemUtils.removeDirectory(specsDir, true);
1492
5025
  }
1493
- const archiveDir = path4.join(
5026
+ const archiveDir = path10.join(
1494
5027
  ".",
1495
5028
  HERASPEC_DIR_NAME,
1496
5029
  CHANGES_DIR_NAME,
@@ -1498,18 +5031,19 @@ This will archive "${changeName}" and merge delta specs into source specs.`);
1498
5031
  );
1499
5032
  await FileSystemUtils.createDirectory(archiveDir);
1500
5033
  const datePrefix = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1501
- const archivePath = path4.join(archiveDir, `${datePrefix}-${changeName}`);
5034
+ const archivePath = path10.join(archiveDir, `${datePrefix}-${changeName}`);
1502
5035
  await FileSystemUtils.createDirectory(archivePath);
1503
5036
  await this.moveChangeToArchive(changePath, archivePath);
1504
5037
  await FileSystemUtils.removeDirectory(changePath, true);
1505
- spinner.succeed(chalk2.green(`Change "${changeName}" archived successfully`));
5038
+ spinner.succeed(chalk3.green(`Change "${changeName}" archived successfully`));
5039
+ await this.autoLogToMemory(changeName, archivePath);
1506
5040
  } catch (error) {
1507
- spinner.fail(chalk2.red(`Error: ${error.message}`));
5041
+ spinner.fail(chalk3.red(`Error: ${error.message}`));
1508
5042
  throw error;
1509
5043
  }
1510
5044
  }
1511
5045
  async mergeDeltaSpecs(changePath, changeName) {
1512
- const deltaSpecsDir = path4.join(
5046
+ const deltaSpecsDir = path10.join(
1513
5047
  ".",
1514
5048
  HERASPEC_DIR_NAME,
1515
5049
  SPECS_DIR_NAME,
@@ -1520,8 +5054,8 @@ This will archive "${changeName}" and merge delta specs into source specs.`);
1520
5054
  }
1521
5055
  const deltaSpecs = await this.findDeltaSpecFiles(deltaSpecsDir);
1522
5056
  for (const deltaSpec of deltaSpecs) {
1523
- const relativePath = path4.relative(deltaSpecsDir, deltaSpec.path);
1524
- const targetSpecPath = path4.join(
5057
+ const relativePath = path10.relative(deltaSpecsDir, deltaSpec.path);
5058
+ const targetSpecPath = path10.join(
1525
5059
  ".",
1526
5060
  HERASPEC_DIR_NAME,
1527
5061
  SPECS_DIR_NAME,
@@ -1534,8 +5068,28 @@ This will archive "${changeName}" and merge delta specs into source specs.`);
1534
5068
  if (await FileSystemUtils.fileExists(targetSpecPath)) {
1535
5069
  targetContent = await FileSystemUtils.readFile(targetSpecPath);
1536
5070
  }
5071
+ const fingerprintsPath = path10.join(changePath, "fingerprints.json");
5072
+ let fingerprints = {};
5073
+ if (await FileSystemUtils.fileExists(fingerprintsPath)) {
5074
+ fingerprints = JSON.parse(await FileSystemUtils.readFile(fingerprintsPath));
5075
+ }
5076
+ const reqs = [...delta.modified, ...delta.removed];
5077
+ for (const req of reqs) {
5078
+ const hashKey = `${relativePath}:${req.name}`;
5079
+ const expectedHash = fingerprints[hashKey];
5080
+ if (expectedHash) {
5081
+ const currentReqBlock = this.extractRequirementBlock(targetContent, req.name);
5082
+ const currentHash = currentReqBlock ? createHash("sha256").update(currentReqBlock).digest("hex") : null;
5083
+ if (currentHash !== expectedHash) {
5084
+ throw new Error(
5085
+ `Parallel Merge Conflict: The requirement "${req.name}" in ${relativePath} has been changed by someone else since you started this change.
5086
+ Please run "heraspec sync ${changeName}" to update your base and resolve the conflict.`
5087
+ );
5088
+ }
5089
+ }
5090
+ }
1537
5091
  const mergedContent = this.mergeDeltaIntoSpec(targetContent, delta, deltaSpec.name);
1538
- await FileSystemUtils.createDirectory(path4.dirname(targetSpecPath));
5092
+ await FileSystemUtils.createDirectory(path10.dirname(targetSpecPath));
1539
5093
  await FileSystemUtils.writeFile(targetSpecPath, mergedContent);
1540
5094
  }
1541
5095
  }
@@ -1547,22 +5101,69 @@ This will archive "${changeName}" and merge delta specs into source specs.`);
1547
5101
  ## Requirements
1548
5102
 
1549
5103
  `;
1550
- if (delta.added.length > 0) {
1551
- merged += "\n## ADDED Requirements\n\n";
5104
+ if (delta.removed && delta.removed.length > 0) {
5105
+ for (const req of delta.removed) {
5106
+ merged = this.modifyRequirementBlock(merged, req.name, null);
5107
+ }
5108
+ }
5109
+ if (delta.modified && delta.modified.length > 0) {
5110
+ for (const req of delta.modified) {
5111
+ const newBlock = this.stringifyRequirement(req);
5112
+ merged = this.modifyRequirementBlock(merged, req.name, newBlock);
5113
+ }
5114
+ }
5115
+ if (delta.added && delta.added.length > 0) {
1552
5116
  for (const req of delta.added) {
1553
- merged += `### Requirement: ${req.name}
5117
+ const newBlock = this.stringifyRequirement(req);
5118
+ merged += `
5119
+ ${newBlock}`;
5120
+ }
5121
+ }
5122
+ return merged;
5123
+ }
5124
+ modifyRequirementBlock(content, reqName, newContent) {
5125
+ const escapedName = reqName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5126
+ const reqRegex = new RegExp(`###\\s+Requirement:\\s*${escapedName}\\s*\\n([\\s\\S]*?)(?=(?:###\\s+Requirement:|$))`, "i");
5127
+ if (newContent === null) {
5128
+ return content.replace(reqRegex, "");
5129
+ } else {
5130
+ if (reqRegex.test(content)) {
5131
+ return content.replace(reqRegex, newContent);
5132
+ } else {
5133
+ return content + "\\n" + newContent;
5134
+ }
5135
+ }
5136
+ }
5137
+ extractRequirementBlock(content, reqName) {
5138
+ const escapedName = reqName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5139
+ const reqRegex = new RegExp(`###\\s+Requirement:\\s*${escapedName}\\s*\\n([\\s\\S]*?)(?=(?:###\\s+Requirement:|$))`, "i");
5140
+ const match = content.match(reqRegex);
5141
+ return match ? match[0].trim() : null;
5142
+ }
5143
+ stringifyRequirement(req) {
5144
+ let str = `### Requirement: ${req.name}
1554
5145
  ${req.description}
1555
5146
 
5147
+ `;
5148
+ if (req.scenarios && req.scenarios.length > 0) {
5149
+ for (const sc of req.scenarios) {
5150
+ str += `#### Scenario: ${sc.name}
5151
+ `;
5152
+ for (const step of sc.steps) {
5153
+ str += `- ${step}
5154
+ `;
5155
+ }
5156
+ str += `
1556
5157
  `;
1557
5158
  }
1558
5159
  }
1559
- return merged;
5160
+ return str;
1560
5161
  }
1561
5162
  async moveChangeToArchive(sourcePath, archivePath) {
1562
5163
  const entries = await FileSystemUtils.readDirectory(sourcePath);
1563
5164
  for (const entry of entries) {
1564
- const sourceEntry = path4.join(sourcePath, entry);
1565
- const archiveEntry = path4.join(archivePath, entry);
5165
+ const sourceEntry = path10.join(sourcePath, entry);
5166
+ const archiveEntry = path10.join(archivePath, entry);
1566
5167
  const stats = await FileSystemUtils.stat(sourceEntry);
1567
5168
  if (stats.isDirectory()) {
1568
5169
  await FileSystemUtils.createDirectory(archiveEntry);
@@ -1576,7 +5177,7 @@ ${req.description}
1576
5177
  const specs = [];
1577
5178
  const entries = await FileSystemUtils.readDirectory(dir);
1578
5179
  for (const entry of entries) {
1579
- const entryPath = path4.join(dir, entry);
5180
+ const entryPath = path10.join(dir, entry);
1580
5181
  const stats = await FileSystemUtils.stat(entryPath);
1581
5182
  if (stats.isDirectory()) {
1582
5183
  const subSpecs = await this.findDeltaSpecFiles(
@@ -1586,18 +5187,164 @@ ${req.description}
1586
5187
  specs.push(...subSpecs);
1587
5188
  } else if (entry.endsWith(".md")) {
1588
5189
  specs.push({
1589
- name: prefix || path4.basename(entry, ".md"),
5190
+ name: prefix || path10.basename(entry, ".md"),
1590
5191
  path: entryPath
1591
5192
  });
1592
5193
  }
1593
5194
  }
1594
5195
  return specs;
1595
5196
  }
5197
+ async autoLogToMemory(changeName, archivePath) {
5198
+ try {
5199
+ const memoryDbPath = path10.join(".", HERASPEC_DIR_NAME, "memory", "heraspec-memory.db");
5200
+ if (!await FileSystemUtils.fileExists(memoryDbPath)) {
5201
+ return;
5202
+ }
5203
+ const { MemoryCommand: MemoryCommand2 } = await Promise.resolve().then(() => (init_memory(), memory_exports));
5204
+ const memoryCmd = new MemoryCommand2();
5205
+ let narrative = "";
5206
+ const proposalPath = path10.join(archivePath, "proposal.md");
5207
+ if (await FileSystemUtils.fileExists(proposalPath)) {
5208
+ narrative = await FileSystemUtils.readFile(proposalPath);
5209
+ }
5210
+ const tasksPath = path10.join(archivePath, "tasks.md");
5211
+ if (await FileSystemUtils.fileExists(tasksPath)) {
5212
+ const tasksContent = await FileSystemUtils.readFile(tasksPath);
5213
+ if (narrative) narrative += "\n\n---\n\n";
5214
+ narrative += tasksContent;
5215
+ }
5216
+ const MAX_LENGTH = 1e4;
5217
+ if (narrative.length > MAX_LENGTH) {
5218
+ narrative = narrative.substring(0, MAX_LENGTH) + "\n...[truncated]";
5219
+ }
5220
+ await memoryCmd.log({
5221
+ type: "feature",
5222
+ title: `Archived change: ${changeName}`,
5223
+ narrative: narrative || `Auto-archived change: ${changeName}`,
5224
+ // We could theoretically set discoveryTokens here, but auto-log uses 0.
5225
+ discoveryTokens: "0"
5226
+ }, ".");
5227
+ } catch (error) {
5228
+ }
5229
+ }
5230
+ };
5231
+
5232
+ // src/core/sync.ts
5233
+ init_file_system();
5234
+ import path11 from "path";
5235
+ import chalk4 from "chalk";
5236
+ import ora5 from "ora";
5237
+ import { createHash as createHash2 } from "crypto";
5238
+ init_config();
5239
+ import { readFileSync as readFileSync2 } from "fs";
5240
+ var SyncCommand = class {
5241
+ async execute(changeName) {
5242
+ if (!changeName) {
5243
+ console.error("Error: Please specify a change name to sync");
5244
+ console.log("Usage: heraspec sync <change-name>");
5245
+ process.exitCode = 1;
5246
+ return;
5247
+ }
5248
+ const changePath = path11.join(".", HERASPEC_DIR_NAME, CHANGES_DIR_NAME, changeName);
5249
+ if (!await FileSystemUtils.fileExists(changePath)) {
5250
+ console.error(`Error: Change "${changeName}" not found at ${changePath}`);
5251
+ process.exitCode = 1;
5252
+ return;
5253
+ }
5254
+ const spinner = ora5(`Syncing fingerprints for "${changeName}"...`).start();
5255
+ try {
5256
+ const fingerprintsPath = path11.join(changePath, "fingerprints.json");
5257
+ let fingerprints = {};
5258
+ if (await FileSystemUtils.fileExists(fingerprintsPath)) {
5259
+ fingerprints = JSON.parse(readFileSync2(fingerprintsPath, "utf-8"));
5260
+ } else {
5261
+ spinner.info(chalk4.blue(`No fingerprints.json found for "${changeName}". Run 'heraspec validate ${changeName}' to capture initial fingerprints.`));
5262
+ return;
5263
+ }
5264
+ const specsDir = path11.join(".", HERASPEC_DIR_NAME, SPECS_DIR_NAME);
5265
+ const deltaSpecsDir = path11.join(specsDir, changeName);
5266
+ if (!await FileSystemUtils.fileExists(deltaSpecsDir)) {
5267
+ spinner.info(chalk4.yellow(`No delta specs found for "${changeName}". Nothing to sync.`));
5268
+ return;
5269
+ }
5270
+ const deltaSpecs = await this.findDeltaSpecs(deltaSpecsDir);
5271
+ let updatedCount = 0;
5272
+ for (const specPath of deltaSpecs) {
5273
+ const relativePath = path11.relative(path11.resolve(deltaSpecsDir), path11.resolve(specPath));
5274
+ const sourceSpecPath = path11.join(path11.resolve(specsDir), relativePath);
5275
+ let sourceContent = "";
5276
+ if (await FileSystemUtils.fileExists(sourceSpecPath)) {
5277
+ sourceContent = readFileSync2(sourceSpecPath, "utf-8");
5278
+ }
5279
+ const deltaContent = readFileSync2(specPath, "utf-8");
5280
+ const parser = new MarkdownParser(deltaContent);
5281
+ const delta = parser.parseDeltaSpec(deltaContent);
5282
+ const reqs = [...delta.modified, ...delta.removed];
5283
+ for (const req of reqs) {
5284
+ const hashKey = `${relativePath}:${req.name}`;
5285
+ const expectedHash = fingerprints[hashKey];
5286
+ if (expectedHash) {
5287
+ const currentReqBlock = this.extractRequirementBlock(sourceContent, req.name);
5288
+ const currentHash = currentReqBlock ? createHash2("sha256").update(currentReqBlock).digest("hex") : null;
5289
+ if (currentHash && currentHash !== expectedHash) {
5290
+ spinner.stop();
5291
+ console.log(chalk4.yellow(`
5292
+ \u26A0\uFE0F Conflict detected for requirement "${req.name}" in ${relativePath}`));
5293
+ console.log(chalk4.gray(`Source spec was updated after your change started.`));
5294
+ fingerprints[hashKey] = currentHash;
5295
+ updatedCount++;
5296
+ console.log(chalk4.green(`\u2713 Fingerprint updated. Please review the delta spec to ensure your modifications still apply correctly to the new source.`));
5297
+ spinner.start();
5298
+ } else if (!currentHash) {
5299
+ spinner.stop();
5300
+ console.log(chalk4.red(`
5301
+ \u274C Requirement "${req.name}" no longer exists in source spec ${relativePath}!`));
5302
+ console.log(chalk4.gray(`You should probably remove this requirement from your delta spec.`));
5303
+ spinner.start();
5304
+ }
5305
+ }
5306
+ }
5307
+ }
5308
+ if (updatedCount > 0) {
5309
+ await FileSystemUtils.writeFile(fingerprintsPath, JSON.stringify(fingerprints, null, 2));
5310
+ spinner.succeed(chalk4.green(`Synced ${updatedCount} fingerprint(s) successfully.`));
5311
+ } else {
5312
+ spinner.succeed(chalk4.green("Already up to date. No conflicts detected."));
5313
+ }
5314
+ } catch (error) {
5315
+ spinner.fail(chalk4.red(`Error: ${error.message}`));
5316
+ process.exitCode = 1;
5317
+ }
5318
+ }
5319
+ async findDeltaSpecs(dir) {
5320
+ const specs = [];
5321
+ const entries = await FileSystemUtils.readDirectory(dir);
5322
+ for (const entry of entries) {
5323
+ const entryPath = path11.join(dir, entry);
5324
+ const stats = await FileSystemUtils.stat(entryPath);
5325
+ if (stats.isDirectory()) {
5326
+ const subSpecs = await this.findDeltaSpecs(entryPath);
5327
+ specs.push(...subSpecs);
5328
+ } else if (entry.endsWith(".md")) {
5329
+ specs.push(entryPath);
5330
+ }
5331
+ }
5332
+ return specs;
5333
+ }
5334
+ extractRequirementBlock(content, reqName) {
5335
+ const escapedName = reqName.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&");
5336
+ const reqRegex = new RegExp(`###\\S+Requirement:\\S*${escapedName}\\S*\\\\n([\\S\\S]*?)(?=(?:###\\S+Requirement:|$))`, "i");
5337
+ const match = content.match(reqRegex);
5338
+ return match ? match[0].trim() : null;
5339
+ }
1596
5340
  };
1597
5341
 
1598
5342
  // src/core/validation/validator.ts
1599
- import { readFileSync } from "fs";
1600
- import path5 from "path";
5343
+ import { readFileSync as readFileSync3 } from "fs";
5344
+ import path12 from "path";
5345
+ import { createHash as createHash3 } from "crypto";
5346
+ init_file_system();
5347
+ init_config();
1601
5348
  var Validator = class {
1602
5349
  strictMode;
1603
5350
  constructor(strictMode = false) {
@@ -1607,7 +5354,7 @@ var Validator = class {
1607
5354
  const errors = [];
1608
5355
  const warnings = [];
1609
5356
  try {
1610
- const content = readFileSync(filePath, "utf-8");
5357
+ const content = readFileSync3(filePath, "utf-8");
1611
5358
  const parser = new MarkdownParser(content);
1612
5359
  const specName = this.extractNameFromPath(filePath);
1613
5360
  const spec = parser.parseSpec(specName);
@@ -1645,7 +5392,7 @@ var Validator = class {
1645
5392
  const errors = [];
1646
5393
  const warnings = [];
1647
5394
  const suggestions = [];
1648
- const proposalPath = path5.join(changePath, "proposal.md");
5395
+ const proposalPath = path12.join(changePath, "proposal.md");
1649
5396
  if (!await FileSystemUtils.fileExists(proposalPath)) {
1650
5397
  errors.push({
1651
5398
  message: "Change must have a proposal.md file",
@@ -1661,7 +5408,7 @@ var Validator = class {
1661
5408
  });
1662
5409
  suggestions.push(`Create proposal.md file at ${proposalPath}`);
1663
5410
  }
1664
- const tasksPath = path5.join(changePath, "tasks.md");
5411
+ const tasksPath = path12.join(changePath, "tasks.md");
1665
5412
  if (!await FileSystemUtils.fileExists(tasksPath)) {
1666
5413
  warnings.push({
1667
5414
  message: "Change has no tasks.md file",
@@ -1670,8 +5417,8 @@ var Validator = class {
1670
5417
  autoFixable: false
1671
5418
  });
1672
5419
  }
1673
- const changeName = path5.basename(changePath);
1674
- const specsDir = path5.join(
5420
+ const changeName = path12.basename(changePath);
5421
+ const specsDir = path12.join(
1675
5422
  ".",
1676
5423
  HERASPEC_DIR_NAME,
1677
5424
  SPECS_DIR_NAME,
@@ -1680,7 +5427,7 @@ var Validator = class {
1680
5427
  if (await FileSystemUtils.fileExists(specsDir)) {
1681
5428
  const deltaSpecs = await this.findDeltaSpecs(specsDir);
1682
5429
  for (const specPath of deltaSpecs) {
1683
- const report = await this.validateDeltaSpec(specPath);
5430
+ const report = await this.validateDeltaSpec(specPath, changePath);
1684
5431
  errors.push(...report.errors);
1685
5432
  warnings.push(...report.warnings);
1686
5433
  if (report.suggestions) {
@@ -1701,14 +5448,17 @@ var Validator = class {
1701
5448
  suggestions: suggestions.length > 0 ? suggestions : void 0
1702
5449
  };
1703
5450
  }
1704
- async validateDeltaSpec(filePath) {
5451
+ async validateDeltaSpec(filePath, changePath) {
1705
5452
  const errors = [];
1706
5453
  const warnings = [];
1707
5454
  const suggestions = [];
1708
5455
  try {
1709
- const content = readFileSync(filePath, "utf-8");
5456
+ const content = readFileSync3(filePath, "utf-8");
1710
5457
  const parser = new MarkdownParser(content);
1711
5458
  const delta = parser.parseDeltaSpec(content);
5459
+ if (changePath && (delta.modified.length > 0 || delta.removed.length > 0)) {
5460
+ await this.captureFingerprints(changePath, filePath, delta);
5461
+ }
1712
5462
  if (delta.added.length === 0 && delta.modified.length === 0 && delta.removed.length === 0) {
1713
5463
  warnings.push({
1714
5464
  message: "Delta spec has no changes",
@@ -1745,14 +5495,14 @@ var Validator = class {
1745
5495
  }
1746
5496
  }
1747
5497
  extractNameFromPath(filePath) {
1748
- const baseName = path5.basename(filePath, ".md");
1749
- return baseName === "spec" ? path5.basename(path5.dirname(filePath)) : baseName;
5498
+ const baseName = path12.basename(filePath, ".md");
5499
+ return baseName === "spec" ? path12.basename(path12.dirname(filePath)) : baseName;
1750
5500
  }
1751
5501
  async findDeltaSpecs(dir) {
1752
5502
  const specs = [];
1753
5503
  const entries = await FileSystemUtils.readDirectory(dir);
1754
5504
  for (const entry of entries) {
1755
- const entryPath = path5.join(dir, entry);
5505
+ const entryPath = path12.join(dir, entry);
1756
5506
  const stats = await FileSystemUtils.stat(entryPath);
1757
5507
  if (stats.isDirectory()) {
1758
5508
  const subSpecs = await this.findDeltaSpecs(entryPath);
@@ -1763,6 +5513,43 @@ var Validator = class {
1763
5513
  }
1764
5514
  return specs;
1765
5515
  }
5516
+ async captureFingerprints(changePath, deltaSpecPath, delta) {
5517
+ const changeName = path12.basename(changePath);
5518
+ const specsDir = path12.join(HERASPEC_DIR_NAME, SPECS_DIR_NAME);
5519
+ const deltaSpecsDir = path12.join(specsDir, changeName);
5520
+ const relativePath = path12.relative(path12.resolve(deltaSpecsDir), path12.resolve(deltaSpecPath));
5521
+ const sourceSpecPath = path12.join(path12.resolve(specsDir), relativePath);
5522
+ let sourceContent = "";
5523
+ if (await FileSystemUtils.fileExists(sourceSpecPath)) {
5524
+ sourceContent = readFileSync3(sourceSpecPath, "utf-8");
5525
+ }
5526
+ const fingerprintsPath = path12.join(changePath, "fingerprints.json");
5527
+ let fingerprints = {};
5528
+ if (await FileSystemUtils.fileExists(fingerprintsPath)) {
5529
+ fingerprints = JSON.parse(readFileSync3(fingerprintsPath, "utf-8"));
5530
+ }
5531
+ let updated = false;
5532
+ const reqs = [...delta.modified, ...delta.removed];
5533
+ for (const req of reqs) {
5534
+ const hashKey = `${relativePath}:${req.name}`;
5535
+ if (!fingerprints[hashKey]) {
5536
+ const reqBlock = this.extractRequirementBlock(sourceContent, req.name);
5537
+ if (reqBlock) {
5538
+ fingerprints[hashKey] = createHash3("sha256").update(reqBlock).digest("hex");
5539
+ updated = true;
5540
+ }
5541
+ }
5542
+ }
5543
+ if (updated) {
5544
+ await FileSystemUtils.writeFile(fingerprintsPath, JSON.stringify(fingerprints, null, 2));
5545
+ }
5546
+ }
5547
+ extractRequirementBlock(content, reqName) {
5548
+ const escapedName = reqName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5549
+ const reqRegex = new RegExp(`###\\s+Requirement:\\s*${escapedName}\\s*\\n([\\s\\S]*?)(?=(?:###\\s+Requirement:|$))`, "i");
5550
+ const match = content.match(reqRegex);
5551
+ return match ? match[0].trim() : null;
5552
+ }
1766
5553
  };
1767
5554
  export {
1768
5555
  ARCHIVES_DIR_NAME,
@@ -1774,7 +5561,9 @@ export {
1774
5561
  HERASPEC_DIR_NAME,
1775
5562
  HERASPEC_MARKERS,
1776
5563
  InitCommand,
5564
+ KNOWLEDGE_DIR_NAME,
1777
5565
  ListCommand,
5566
+ MEMORY_DIR_NAME,
1778
5567
  PROJECT_TYPES,
1779
5568
  ProjectTypeSchema,
1780
5569
  RequirementSchema,
@@ -1784,6 +5573,7 @@ export {
1784
5573
  ScenarioSchema,
1785
5574
  SpecMetaSchema,
1786
5575
  SpecSchema,
5576
+ SyncCommand,
1787
5577
  Validator
1788
5578
  };
1789
5579
  //# sourceMappingURL=index.js.map