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.
- package/LICENSE +22 -22
- package/README.md +188 -103
- package/bin/heraspec.js +4805 -1122
- package/bin/heraspec.js.map +4 -4
- package/dist/core/templates/skills/CHANGELOG.md +117 -117
- package/dist/core/templates/skills/README-template.md +58 -58
- package/dist/core/templates/skills/README.md +38 -38
- package/dist/core/templates/skills/content-optimization-skill.md +104 -104
- package/dist/core/templates/skills/data/design-systems.csv +54 -0
- package/dist/core/templates/skills/data/pages-proposed.csv +21 -21
- package/dist/core/templates/skills/data/pages.csv +9 -9
- package/dist/core/templates/skills/data/typography.csv +57 -57
- package/dist/core/templates/skills/deploy-documentation-skill.md +408 -0
- package/dist/core/templates/skills/design-system-skill.md +176 -0
- package/dist/core/templates/skills/documents/templates/documentation-landing-page.html +63 -63
- package/dist/core/templates/skills/documents/templates/documentation.html +49 -49
- package/dist/core/templates/skills/documents/templates/landing-script.js +38 -38
- package/dist/core/templates/skills/documents/templates/landing-style.css +158 -158
- package/dist/core/templates/skills/documents/templates/script.js +56 -56
- package/dist/core/templates/skills/documents/templates/style.css +155 -155
- package/dist/core/templates/skills/documents/templates/technical-doc-template.md +16 -16
- package/dist/core/templates/skills/documents/templates/user-guide-template.md +16 -16
- package/dist/core/templates/skills/documents-skill.md +104 -104
- package/dist/core/templates/skills/e2e-test-skill.md +119 -119
- package/dist/core/templates/skills/git-embed-skill.md +57 -0
- package/dist/core/templates/skills/integration-test-skill.md +118 -118
- package/dist/core/templates/skills/knowledge/README.md +63 -0
- package/dist/core/templates/skills/knowledge/design-systems/airbnb/DESIGN.md +246 -0
- package/dist/core/templates/skills/knowledge/design-systems/airtable/DESIGN.md +89 -0
- package/dist/core/templates/skills/knowledge/design-systems/apple/DESIGN.md +313 -0
- package/dist/core/templates/skills/knowledge/design-systems/bmw/DESIGN.md +180 -0
- package/dist/core/templates/skills/knowledge/design-systems/cal/DESIGN.md +259 -0
- package/dist/core/templates/skills/knowledge/design-systems/claude/DESIGN.md +312 -0
- package/dist/core/templates/skills/knowledge/design-systems/clay/DESIGN.md +304 -0
- package/dist/core/templates/skills/knowledge/design-systems/clickhouse/DESIGN.md +281 -0
- package/dist/core/templates/skills/knowledge/design-systems/cohere/DESIGN.md +266 -0
- package/dist/core/templates/skills/knowledge/design-systems/coinbase/DESIGN.md +129 -0
- package/dist/core/templates/skills/knowledge/design-systems/composio/DESIGN.md +307 -0
- package/dist/core/templates/skills/knowledge/design-systems/cursor/DESIGN.md +309 -0
- package/dist/core/templates/skills/knowledge/design-systems/elevenlabs/DESIGN.md +265 -0
- package/dist/core/templates/skills/knowledge/design-systems/expo/DESIGN.md +281 -0
- package/dist/core/templates/skills/knowledge/design-systems/figma/DESIGN.md +220 -0
- package/dist/core/templates/skills/knowledge/design-systems/framer/DESIGN.md +246 -0
- package/dist/core/templates/skills/knowledge/design-systems/hashicorp/DESIGN.md +278 -0
- package/dist/core/templates/skills/knowledge/design-systems/ibm/DESIGN.md +332 -0
- package/dist/core/templates/skills/knowledge/design-systems/index.json +72 -0
- package/dist/core/templates/skills/knowledge/design-systems/intercom/DESIGN.md +146 -0
- package/dist/core/templates/skills/knowledge/design-systems/kraken/DESIGN.md +125 -0
- package/dist/core/templates/skills/knowledge/design-systems/linear.app/DESIGN.md +367 -0
- package/dist/core/templates/skills/knowledge/design-systems/lovable/DESIGN.md +298 -0
- package/dist/core/templates/skills/knowledge/design-systems/minimax/DESIGN.md +257 -0
- package/dist/core/templates/skills/knowledge/design-systems/mintlify/DESIGN.md +326 -0
- package/dist/core/templates/skills/knowledge/design-systems/miro/DESIGN.md +108 -0
- package/dist/core/templates/skills/knowledge/design-systems/mistral.ai/DESIGN.md +261 -0
- package/dist/core/templates/skills/knowledge/design-systems/mongodb/DESIGN.md +266 -0
- package/dist/core/templates/skills/knowledge/design-systems/notion/DESIGN.md +309 -0
- package/dist/core/templates/skills/knowledge/design-systems/nvidia/DESIGN.md +293 -0
- package/dist/core/templates/skills/knowledge/design-systems/ollama/DESIGN.md +267 -0
- package/dist/core/templates/skills/knowledge/design-systems/opencode.ai/DESIGN.md +281 -0
- package/dist/core/templates/skills/knowledge/design-systems/pinterest/DESIGN.md +230 -0
- package/dist/core/templates/skills/knowledge/design-systems/posthog/DESIGN.md +256 -0
- package/dist/core/templates/skills/knowledge/design-systems/raycast/DESIGN.md +268 -0
- package/dist/core/templates/skills/knowledge/design-systems/replicate/DESIGN.md +261 -0
- package/dist/core/templates/skills/knowledge/design-systems/resend/DESIGN.md +303 -0
- package/dist/core/templates/skills/knowledge/design-systems/revolut/DESIGN.md +185 -0
- package/dist/core/templates/skills/knowledge/design-systems/runwayml/DESIGN.md +244 -0
- package/dist/core/templates/skills/knowledge/design-systems/sanity/DESIGN.md +357 -0
- package/dist/core/templates/skills/knowledge/design-systems/sentry/DESIGN.md +262 -0
- package/dist/core/templates/skills/knowledge/design-systems/spacex/DESIGN.md +194 -0
- package/dist/core/templates/skills/knowledge/design-systems/spotify/DESIGN.md +246 -0
- package/dist/core/templates/skills/knowledge/design-systems/stripe/DESIGN.md +322 -0
- package/dist/core/templates/skills/knowledge/design-systems/supabase/DESIGN.md +255 -0
- package/dist/core/templates/skills/knowledge/design-systems/superhuman/DESIGN.md +252 -0
- package/dist/core/templates/skills/knowledge/design-systems/together.ai/DESIGN.md +263 -0
- package/dist/core/templates/skills/knowledge/design-systems/uber/DESIGN.md +295 -0
- package/dist/core/templates/skills/knowledge/design-systems/vercel/DESIGN.md +310 -0
- package/dist/core/templates/skills/knowledge/design-systems/voltagent/DESIGN.md +323 -0
- package/dist/core/templates/skills/knowledge/design-systems/warp/DESIGN.md +253 -0
- package/dist/core/templates/skills/knowledge/design-systems/webflow/DESIGN.md +92 -0
- package/dist/core/templates/skills/knowledge/design-systems/wise/DESIGN.md +173 -0
- package/dist/core/templates/skills/knowledge/design-systems/x.ai/DESIGN.md +257 -0
- package/dist/core/templates/skills/knowledge/design-systems/zapier/DESIGN.md +328 -0
- package/dist/core/templates/skills/knowledge/frameworks/php/codeigniter/rise-cms/profile.json +27 -0
- package/dist/core/templates/skills/knowledge/frameworks/php/codeigniter/rise-cms/structure.md +137 -0
- package/dist/core/templates/skills/knowledge/frameworks/php/laravel/botble/profile.json +39 -0
- package/dist/core/templates/skills/knowledge/frameworks/php/laravel/botble/structure.md +208 -0
- package/dist/core/templates/skills/knowledge/frameworks/php/wordpress/core/profile.json +51 -0
- package/dist/core/templates/skills/knowledge/frameworks/php/wordpress/core/structure.md +369 -0
- package/dist/core/templates/skills/knowledge/index.json +65 -0
- package/dist/core/templates/skills/module-codebase-skill.md +110 -110
- package/dist/core/templates/skills/plugin-directory-skill.md +396 -396
- package/dist/core/templates/skills/project-memory-skill.md +222 -0
- package/dist/core/templates/skills/project-memory-skill.vi.md +223 -0
- package/dist/core/templates/skills/scripts/CODE_EXPLANATION.md +394 -394
- package/dist/core/templates/skills/scripts/SEARCH_ALGORITHMS_COMPARISON.md +421 -421
- package/dist/core/templates/skills/scripts/SEARCH_MODES_GUIDE.md +238 -238
- package/dist/core/templates/skills/scripts/__pycache__/core.cpython-311.pyc +0 -0
- package/dist/core/templates/skills/scripts/core.py +391 -385
- package/dist/core/templates/skills/scripts/search.py +1 -1
- package/dist/core/templates/skills/smart-explore-skill.md +141 -0
- package/dist/core/templates/skills/sourcecode-analyzer-skill.md +210 -0
- package/dist/core/templates/skills/sourcecode-analyzer-skill.vi.md +210 -0
- package/dist/core/templates/skills/suggestion-skill.md +118 -118
- package/dist/core/templates/skills/templates/accessibility-checklist.md +40 -40
- package/dist/core/templates/skills/templates/example-prompt-full-theme.md +333 -333
- package/dist/core/templates/skills/templates/page-types-guide.md +338 -338
- package/dist/core/templates/skills/templates/pages-proposed-summary.md +273 -273
- package/dist/core/templates/skills/templates/pre-delivery-checklist.md +42 -42
- package/dist/core/templates/skills/templates/prompt-template-full-theme.md +313 -313
- package/dist/core/templates/skills/templates/responsive-design.md +40 -40
- package/dist/core/templates/skills/ui-ux-skill.md +595 -584
- package/dist/core/templates/skills/unit-test-skill.md +111 -111
- package/dist/core/templates/skills/ux-element/templates/Controller.php +50 -50
- package/dist/core/templates/skills/ux-element/templates/Shortcode.php +23 -23
- package/dist/core/templates/skills/ux-element/templates/Template.html +20 -20
- package/dist/core/templates/skills/ux-element/templates/Thumbnail.svg +8 -8
- package/dist/core/templates/skills/ux-element/templates/View.php +21 -21
- package/dist/core/templates/skills/ux-element-skill.md +83 -83
- package/dist/core/templates/skills/wordpress-plugin-check-skill.md +151 -76
- package/dist/core/templates/skills/wordpress-plugin-standard/templates/admin-dashboard.php +47 -47
- package/dist/core/templates/skills/wordpress-plugin-standard/templates/admin-settings.php +60 -60
- package/dist/core/templates/skills/wordpress-plugin-standard/templates/assets/admin-css.css +22 -22
- package/dist/core/templates/skills/wordpress-plugin-standard/templates/assets/admin-js.js +15 -15
- package/dist/core/templates/skills/wordpress-plugin-standard/templates/plugin-main.php +169 -169
- package/dist/core/templates/skills/wordpress-plugin-standard/templates/readme.txt +41 -41
- package/dist/core/templates/skills/wordpress-plugin-standard/templates/uninstall.php +21 -21
- package/dist/core/templates/skills/wordpress-plugin-standard-skill.md +100 -100
- package/dist/index.js +4068 -278
- 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
|
-
|
|
4
|
-
var
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
185
|
-
import
|
|
186
|
-
import
|
|
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
|
-
##
|
|
267
|
-
-
|
|
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
|
-
##
|
|
278
|
-
List
|
|
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 =
|
|
619
|
-
const heraspecPath =
|
|
3677
|
+
const resolvedPath = path8.resolve(targetPath);
|
|
3678
|
+
const heraspecPath = path8.join(resolvedPath, HERASPEC_DIR_NAME);
|
|
620
3679
|
const alreadyInitialized = await FileSystemUtils.fileExists(
|
|
621
|
-
|
|
3680
|
+
path8.join(heraspecPath, HERASPEC_MARKERS.PROJECT_MD)
|
|
622
3681
|
);
|
|
623
|
-
const spinner =
|
|
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(
|
|
630
|
-
await FileSystemUtils.createDirectory(
|
|
631
|
-
await FileSystemUtils.createDirectory(
|
|
632
|
-
await FileSystemUtils.createDirectory(
|
|
633
|
-
|
|
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 =
|
|
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 =
|
|
645
|
-
const newAgentsPath =
|
|
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.
|
|
652
|
-
|
|
653
|
-
|
|
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(
|
|
3730
|
+
console.log(chalk2.cyan("Next steps:"));
|
|
658
3731
|
console.log(
|
|
659
|
-
|
|
3732
|
+
chalk2.gray("1. Review and update heraspec/project.md with your project details")
|
|
660
3733
|
);
|
|
661
3734
|
console.log(
|
|
662
|
-
|
|
3735
|
+
chalk2.gray('2. Create your first change: "Create a HeraSpec change to..."')
|
|
663
3736
|
);
|
|
664
3737
|
console.log(
|
|
665
|
-
|
|
3738
|
+
chalk2.gray("3. List changes: heraspec list")
|
|
666
3739
|
);
|
|
667
3740
|
} catch (error) {
|
|
668
|
-
spinner.fail(
|
|
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 =
|
|
674
|
-
const configYamlPath =
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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)
|
|
682
|
-
|
|
683
|
-
configYamlPath
|
|
684
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1299
|
-
|
|
1300
|
-
import
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
5038
|
+
spinner.succeed(chalk3.green(`Change "${changeName}" archived successfully`));
|
|
5039
|
+
await this.autoLogToMemory(changeName, archivePath);
|
|
1506
5040
|
} catch (error) {
|
|
1507
|
-
spinner.fail(
|
|
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 =
|
|
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 =
|
|
1524
|
-
const targetSpecPath =
|
|
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(
|
|
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.
|
|
1551
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
1565
|
-
const archiveEntry =
|
|
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 =
|
|
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 ||
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1674
|
-
const specsDir =
|
|
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 =
|
|
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 =
|
|
1749
|
-
return baseName === "spec" ?
|
|
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 =
|
|
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
|