@morsa/guidance-bank 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/bin/gbank.js +3 -0
- package/dist/cli/commands/init.js +27 -0
- package/dist/cli/commands/mcpServe.js +8 -0
- package/dist/cli/commands/stats.js +92 -0
- package/dist/cli/index.js +67 -0
- package/dist/cli/postinstall.js +24 -0
- package/dist/cli/prompts/initPrompts.js +89 -0
- package/dist/cli/prompts/providerAvailability.js +31 -0
- package/dist/core/audit/summarizeEntryContent.js +43 -0
- package/dist/core/audit/types.js +1 -0
- package/dist/core/bank/canonicalEntry.js +106 -0
- package/dist/core/bank/integration.js +16 -0
- package/dist/core/bank/layout.js +159 -0
- package/dist/core/bank/lifecycle.js +24 -0
- package/dist/core/bank/manifest.js +37 -0
- package/dist/core/bank/project.js +105 -0
- package/dist/core/bank/types.js +6 -0
- package/dist/core/context/contextEntryResolver.js +140 -0
- package/dist/core/context/contextTextRenderer.js +116 -0
- package/dist/core/context/detectProjectContext.js +118 -0
- package/dist/core/context/resolveContextService.js +138 -0
- package/dist/core/context/types.js +1 -0
- package/dist/core/init/initService.js +112 -0
- package/dist/core/init/initTypes.js +1 -0
- package/dist/core/projects/createBankDeriveGuidance/index.js +47 -0
- package/dist/core/projects/createBankDeriveGuidance/shared/general.js +49 -0
- package/dist/core/projects/createBankDeriveGuidance/shared/typescript.js +18 -0
- package/dist/core/projects/createBankDeriveGuidance/stacks/angular.js +252 -0
- package/dist/core/projects/createBankDeriveGuidance/stacks/ios.js +254 -0
- package/dist/core/projects/createBankDeriveGuidance/stacks/nextjs.js +220 -0
- package/dist/core/projects/createBankDeriveGuidance/stacks/nodejs.js +221 -0
- package/dist/core/projects/createBankDeriveGuidance/stacks/other.js +34 -0
- package/dist/core/projects/createBankDeriveGuidance/stacks/react.js +214 -0
- package/dist/core/projects/createBankFlow.js +252 -0
- package/dist/core/projects/createBankIterationPrompt.js +294 -0
- package/dist/core/projects/createBankPrompt.js +95 -0
- package/dist/core/projects/createFlowPhases.js +28 -0
- package/dist/core/projects/discoverCurrentProjectBank.js +43 -0
- package/dist/core/projects/discoverExistingGuidance.js +99 -0
- package/dist/core/projects/discoverProjectEvidence.js +87 -0
- package/dist/core/projects/discoverRecentCommits.js +28 -0
- package/dist/core/projects/findReferenceProjects.js +42 -0
- package/dist/core/projects/guidanceStrategies.js +29 -0
- package/dist/core/projects/identity.js +16 -0
- package/dist/core/projects/providerProjectGuidance.js +82 -0
- package/dist/core/providers/providerRegistry.js +51 -0
- package/dist/core/providers/types.js +1 -0
- package/dist/core/stats/statsService.js +117 -0
- package/dist/core/sync/syncService.js +145 -0
- package/dist/core/sync/syncTypes.js +1 -0
- package/dist/core/upgrade/upgradeService.js +134 -0
- package/dist/integrations/claudeCode/install.js +78 -0
- package/dist/integrations/codex/install.js +80 -0
- package/dist/integrations/commandRunner.js +32 -0
- package/dist/integrations/cursor/install.js +118 -0
- package/dist/integrations/shared.js +20 -0
- package/dist/mcp/config.js +19 -0
- package/dist/mcp/createMcpServer.js +31 -0
- package/dist/mcp/launcher.js +49 -0
- package/dist/mcp/registerTools.js +33 -0
- package/dist/mcp/serverMetadata.js +7 -0
- package/dist/mcp/tools/auditUtils.js +49 -0
- package/dist/mcp/tools/createBankApply.js +106 -0
- package/dist/mcp/tools/createBankToolRuntime.js +115 -0
- package/dist/mcp/tools/createBankToolSchemas.js +234 -0
- package/dist/mcp/tools/entryMutationHelpers.js +44 -0
- package/dist/mcp/tools/registerBankManifestTool.js +47 -0
- package/dist/mcp/tools/registerClearProjectBankTool.js +73 -0
- package/dist/mcp/tools/registerCreateBankTool.js +240 -0
- package/dist/mcp/tools/registerDeleteEntryTool.js +98 -0
- package/dist/mcp/tools/registerDeleteGuidanceSourceTool.js +120 -0
- package/dist/mcp/tools/registerListEntriesTool.js +94 -0
- package/dist/mcp/tools/registerReadEntryTool.js +99 -0
- package/dist/mcp/tools/registerResolveContextTool.js +128 -0
- package/dist/mcp/tools/registerSetProjectStateTool.js +121 -0
- package/dist/mcp/tools/registerSyncBankTool.js +113 -0
- package/dist/mcp/tools/registerUpgradeBankTool.js +89 -0
- package/dist/mcp/tools/registerUpsertRuleTool.js +100 -0
- package/dist/mcp/tools/registerUpsertSkillTool.js +102 -0
- package/dist/mcp/tools/sharedSchemas.js +13 -0
- package/dist/shared/errors.js +18 -0
- package/dist/shared/paths.js +11 -0
- package/dist/storage/atomicWrite.js +15 -0
- package/dist/storage/auditLogger.js +20 -0
- package/dist/storage/auditStore.js +22 -0
- package/dist/storage/bankRepository.js +168 -0
- package/dist/storage/entryStore.js +142 -0
- package/dist/storage/manifestStore.js +30 -0
- package/dist/storage/projectBankStore.js +55 -0
- package/dist/storage/providerIntegrationStore.js +22 -0
- package/dist/storage/safeFs.js +202 -0
- package/package.json +64 -0
- package/scripts/postinstall.js +20 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class GuidanceBankCliError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "GuidanceBankCliError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class ValidationError extends GuidanceBankCliError {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "ValidationError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class UserInputError extends GuidanceBankCliError {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "UserInputError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const BANK_DIRECTORY_NAME = ".guidance-bank";
|
|
4
|
+
export const LEGACY_BANK_DIRECTORY_NAMES = [".guidancebank", ".memory-bank"];
|
|
5
|
+
export const resolveDefaultBankRoot = () => path.join(os.homedir(), BANK_DIRECTORY_NAME);
|
|
6
|
+
export const resolveBankRoot = (overridePath) => {
|
|
7
|
+
if (!overridePath) {
|
|
8
|
+
return resolveDefaultBankRoot();
|
|
9
|
+
}
|
|
10
|
+
return path.resolve(overridePath);
|
|
11
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const atomicWriteFile = async (filePath, content) => {
|
|
4
|
+
const directoryPath = path.dirname(filePath);
|
|
5
|
+
const temporaryFilePath = path.join(directoryPath, `.${path.basename(filePath)}.${process.pid}.${Date.now().toString(36)}.tmp`);
|
|
6
|
+
const handle = await fs.open(temporaryFilePath, "wx", 0o600);
|
|
7
|
+
try {
|
|
8
|
+
await handle.writeFile(content, "utf8");
|
|
9
|
+
await handle.sync();
|
|
10
|
+
}
|
|
11
|
+
finally {
|
|
12
|
+
await handle.close();
|
|
13
|
+
}
|
|
14
|
+
await fs.rename(temporaryFilePath, filePath);
|
|
15
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { resolveBankPaths } from "../core/bank/layout.js";
|
|
3
|
+
import { appendManagedTextFile } from "./safeFs.js";
|
|
4
|
+
export class AuditLogger {
|
|
5
|
+
options;
|
|
6
|
+
paths;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
this.paths = resolveBankPaths(options.bankRoot);
|
|
10
|
+
}
|
|
11
|
+
async writeEvent(event) {
|
|
12
|
+
await appendManagedTextFile(this.options.bankRoot, this.paths.auditEventsFile, `${JSON.stringify({
|
|
13
|
+
schemaVersion: 1,
|
|
14
|
+
eventId: randomUUID(),
|
|
15
|
+
timestamp: new Date().toISOString(),
|
|
16
|
+
provider: this.options.provider,
|
|
17
|
+
...event,
|
|
18
|
+
})}\n`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolveBankPaths } from "../core/bank/layout.js";
|
|
3
|
+
import { managedPathExists } from "./safeFs.js";
|
|
4
|
+
export class AuditStore {
|
|
5
|
+
rootPath;
|
|
6
|
+
paths;
|
|
7
|
+
constructor(rootPath, paths) {
|
|
8
|
+
this.rootPath = rootPath;
|
|
9
|
+
this.paths = paths;
|
|
10
|
+
}
|
|
11
|
+
async readEventsOptional() {
|
|
12
|
+
if (!(await managedPathExists(this.rootPath, this.paths.auditEventsFile))) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const content = await readFile(this.paths.auditEventsFile, "utf8");
|
|
16
|
+
return content
|
|
17
|
+
.split("\n")
|
|
18
|
+
.map((line) => line.trim())
|
|
19
|
+
.filter((line) => line.length > 0)
|
|
20
|
+
.map((line) => JSON.parse(line));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createStarterFiles, resolveBankPaths } from "../core/bank/layout.js";
|
|
3
|
+
import { ensureManagedDirectory, writeManagedTextFileIfMissing, } from "./safeFs.js";
|
|
4
|
+
import { EntryStore } from "./entryStore.js";
|
|
5
|
+
import { ManifestStore } from "./manifestStore.js";
|
|
6
|
+
import { ProjectBankStore } from "./projectBankStore.js";
|
|
7
|
+
import { ProviderIntegrationStore } from "./providerIntegrationStore.js";
|
|
8
|
+
import { AuditStore } from "./auditStore.js";
|
|
9
|
+
export class BankRepository {
|
|
10
|
+
rootPath;
|
|
11
|
+
paths;
|
|
12
|
+
manifestStore;
|
|
13
|
+
projectBanks;
|
|
14
|
+
entries;
|
|
15
|
+
providerIntegrations;
|
|
16
|
+
auditStore;
|
|
17
|
+
constructor(rootPath) {
|
|
18
|
+
this.rootPath = rootPath;
|
|
19
|
+
this.paths = resolveBankPaths(rootPath);
|
|
20
|
+
this.manifestStore = new ManifestStore(rootPath, this.paths);
|
|
21
|
+
this.projectBanks = new ProjectBankStore(rootPath, this.paths);
|
|
22
|
+
this.entries = new EntryStore(rootPath, this.paths);
|
|
23
|
+
this.providerIntegrations = new ProviderIntegrationStore(rootPath, this.paths);
|
|
24
|
+
this.auditStore = new AuditStore(rootPath, this.paths);
|
|
25
|
+
}
|
|
26
|
+
// TODO: Multi-agent concurrency is still last-write-wins at the entry level.
|
|
27
|
+
// Separate `gbank mcp serve` processes can update the same rule or skill concurrently.
|
|
28
|
+
// Atomic writes protect file integrity, but they do not detect semantic conflicts.
|
|
29
|
+
// Add revision stamps or optimistic locking before relying on shared concurrent edits.
|
|
30
|
+
async ensureStructure() {
|
|
31
|
+
await ensureManagedDirectory(this.rootPath, this.paths.root);
|
|
32
|
+
await ensureManagedDirectory(this.rootPath, this.paths.sharedDirectory);
|
|
33
|
+
await ensureManagedDirectory(this.rootPath, this.paths.sharedRulesDirectory);
|
|
34
|
+
await ensureManagedDirectory(this.rootPath, this.paths.sharedSkillsDirectory);
|
|
35
|
+
await ensureManagedDirectory(this.rootPath, this.paths.projectsDirectory);
|
|
36
|
+
await ensureManagedDirectory(this.rootPath, this.paths.mcpDirectory);
|
|
37
|
+
await ensureManagedDirectory(this.rootPath, this.paths.integrationsDirectory);
|
|
38
|
+
await ensureManagedDirectory(this.rootPath, path.join(this.paths.sharedRulesDirectory, "core"));
|
|
39
|
+
await ensureManagedDirectory(this.rootPath, path.join(this.paths.sharedRulesDirectory, "stacks"));
|
|
40
|
+
await ensureManagedDirectory(this.rootPath, path.join(this.paths.sharedRulesDirectory, "providers"));
|
|
41
|
+
await ensureManagedDirectory(this.rootPath, path.join(this.paths.sharedRulesDirectory, "topics"));
|
|
42
|
+
}
|
|
43
|
+
async ensureStarterFiles() {
|
|
44
|
+
for (const starterFile of createStarterFiles(this.paths)) {
|
|
45
|
+
await writeManagedTextFileIfMissing(this.rootPath, starterFile.filePath, starterFile.content);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async ensureProjectStructure(projectId) {
|
|
49
|
+
await this.projectBanks.ensureProjectStructure(projectId);
|
|
50
|
+
}
|
|
51
|
+
async hasManifest() {
|
|
52
|
+
return this.manifestStore.hasManifest();
|
|
53
|
+
}
|
|
54
|
+
async readManifest() {
|
|
55
|
+
return this.manifestStore.readManifest();
|
|
56
|
+
}
|
|
57
|
+
async readManifestOptional() {
|
|
58
|
+
return this.manifestStore.readManifestOptional();
|
|
59
|
+
}
|
|
60
|
+
async writeManifest(manifest) {
|
|
61
|
+
await this.manifestStore.writeManifest(manifest);
|
|
62
|
+
}
|
|
63
|
+
async writeMcpServerConfig(config) {
|
|
64
|
+
await this.manifestStore.writeMcpServerConfig(config);
|
|
65
|
+
}
|
|
66
|
+
async writeProviderIntegration(providerId, descriptor) {
|
|
67
|
+
await this.providerIntegrations.writeProviderIntegration(providerId, descriptor);
|
|
68
|
+
}
|
|
69
|
+
async readProviderIntegrationOptional(providerId) {
|
|
70
|
+
return this.providerIntegrations.readProviderIntegrationOptional(providerId);
|
|
71
|
+
}
|
|
72
|
+
async writeProjectManifest(projectId, manifest) {
|
|
73
|
+
await this.projectBanks.writeProjectManifest(projectId, manifest);
|
|
74
|
+
}
|
|
75
|
+
async readProjectManifestOptional(projectId) {
|
|
76
|
+
return this.projectBanks.readProjectManifestOptional(projectId);
|
|
77
|
+
}
|
|
78
|
+
async listProjectManifests() {
|
|
79
|
+
return this.projectBanks.listProjectManifests();
|
|
80
|
+
}
|
|
81
|
+
async writeProjectState(projectId, state) {
|
|
82
|
+
await this.projectBanks.writeProjectState(projectId, state);
|
|
83
|
+
}
|
|
84
|
+
async readProjectStateOptional(projectId) {
|
|
85
|
+
return this.projectBanks.readProjectStateOptional(projectId);
|
|
86
|
+
}
|
|
87
|
+
async deleteProjectBank(projectId) {
|
|
88
|
+
const deleted = await this.projectBanks.deleteProjectBank(projectId);
|
|
89
|
+
if (deleted) {
|
|
90
|
+
await this.touchManifest();
|
|
91
|
+
}
|
|
92
|
+
return deleted;
|
|
93
|
+
}
|
|
94
|
+
async readAuditEventsOptional() {
|
|
95
|
+
return this.auditStore.readEventsOptional();
|
|
96
|
+
}
|
|
97
|
+
async listEntries(kind, groupPath) {
|
|
98
|
+
return this.listLayerEntries("shared", kind, undefined, groupPath);
|
|
99
|
+
}
|
|
100
|
+
async listLayerEntries(layer, kind, projectId, groupPath) {
|
|
101
|
+
return this.entries.listLayerEntries(layer, kind, projectId, groupPath);
|
|
102
|
+
}
|
|
103
|
+
async readEntry(kind, entryPath) {
|
|
104
|
+
return this.readLayerEntry("shared", kind, entryPath);
|
|
105
|
+
}
|
|
106
|
+
async readLayerEntry(layer, kind, entryPath, projectId) {
|
|
107
|
+
return this.entries.readLayerEntry(layer, kind, entryPath, projectId);
|
|
108
|
+
}
|
|
109
|
+
async readLayerEntryOptional(layer, kind, entryPath, projectId) {
|
|
110
|
+
return this.entries.readLayerEntryOptional(layer, kind, entryPath, projectId);
|
|
111
|
+
}
|
|
112
|
+
async touchManifest() {
|
|
113
|
+
const manifest = await this.readManifestOptional();
|
|
114
|
+
if (manifest === null) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
await this.writeManifest({
|
|
118
|
+
...manifest,
|
|
119
|
+
updatedAt: new Date().toISOString(),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async touchProjectManifest(projectId) {
|
|
123
|
+
const manifest = await this.readProjectManifestOptional(projectId);
|
|
124
|
+
if (manifest === null) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
await this.writeProjectManifest(projectId, {
|
|
128
|
+
...manifest,
|
|
129
|
+
updatedAt: new Date().toISOString(),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async upsertRule(layer, entryPath, content, projectId) {
|
|
133
|
+
const result = await this.entries.upsertRule(layer, entryPath, content, projectId);
|
|
134
|
+
await this.touchManifest();
|
|
135
|
+
if (layer === "project" && projectId) {
|
|
136
|
+
await this.touchProjectManifest(projectId);
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
async upsertSkill(layer, skillPath, content, projectId) {
|
|
141
|
+
const result = await this.entries.upsertSkill(layer, skillPath, content, projectId);
|
|
142
|
+
await this.touchManifest();
|
|
143
|
+
if (layer === "project" && projectId) {
|
|
144
|
+
await this.touchProjectManifest(projectId);
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
async deleteRule(layer, entryPath, projectId) {
|
|
149
|
+
const result = await this.entries.deleteRule(layer, entryPath, projectId);
|
|
150
|
+
if (result.status === "deleted") {
|
|
151
|
+
await this.touchManifest();
|
|
152
|
+
if (layer === "project" && projectId) {
|
|
153
|
+
await this.touchProjectManifest(projectId);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
async deleteSkill(layer, skillPath, projectId) {
|
|
159
|
+
const result = await this.entries.deleteSkill(layer, skillPath, projectId);
|
|
160
|
+
if (result.status === "deleted") {
|
|
161
|
+
await this.touchManifest();
|
|
162
|
+
if (layer === "project" && projectId) {
|
|
163
|
+
await this.touchProjectManifest(projectId);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { parseCanonicalRuleDocument, parseCanonicalSkillDocument } from "../core/bank/canonicalEntry.js";
|
|
3
|
+
import { resolveBankPaths } from "../core/bank/layout.js";
|
|
4
|
+
import { ValidationError } from "../shared/errors.js";
|
|
5
|
+
import { deleteManagedDirectory, deleteManagedFile, listManagedFilesRecursively, managedPathExists, readManagedTextFile, writeManagedTextFile, } from "./safeFs.js";
|
|
6
|
+
export class EntryStore {
|
|
7
|
+
rootPath;
|
|
8
|
+
paths;
|
|
9
|
+
constructor(rootPath, paths) {
|
|
10
|
+
this.rootPath = rootPath;
|
|
11
|
+
this.paths = paths;
|
|
12
|
+
}
|
|
13
|
+
resolveEntryBasePath(kind, layer, projectId) {
|
|
14
|
+
if (layer === "shared") {
|
|
15
|
+
return kind === "rules" ? this.paths.sharedRulesDirectory : this.paths.sharedSkillsDirectory;
|
|
16
|
+
}
|
|
17
|
+
if (!projectId) {
|
|
18
|
+
throw new ValidationError("Project id is required for project-layer entries.");
|
|
19
|
+
}
|
|
20
|
+
return kind === "rules" ? this.paths.projectRulesDirectory(projectId) : this.paths.projectSkillsDirectory(projectId);
|
|
21
|
+
}
|
|
22
|
+
resolvePathWithinEntryBase(basePath, relativePath) {
|
|
23
|
+
const resolvedPath = path.resolve(basePath, relativePath);
|
|
24
|
+
const normalizedRelativePath = path.relative(basePath, resolvedPath);
|
|
25
|
+
if (normalizedRelativePath.startsWith("..") || path.isAbsolute(normalizedRelativePath)) {
|
|
26
|
+
throw new ValidationError(`Entry path escapes ${path.basename(basePath)}: ${relativePath}`);
|
|
27
|
+
}
|
|
28
|
+
return resolvedPath;
|
|
29
|
+
}
|
|
30
|
+
buildReadableEntryCandidates(kind, layer, entryPath) {
|
|
31
|
+
const normalizedPath = entryPath.replaceAll("\\", "/").trim();
|
|
32
|
+
if (kind !== "skills") {
|
|
33
|
+
return [normalizedPath];
|
|
34
|
+
}
|
|
35
|
+
const scopePrefix = `${layer}/`;
|
|
36
|
+
const alternatePath = normalizedPath.startsWith(scopePrefix)
|
|
37
|
+
? normalizedPath.slice(scopePrefix.length)
|
|
38
|
+
: `${scopePrefix}${normalizedPath}`;
|
|
39
|
+
return [...new Set([normalizedPath, alternatePath])];
|
|
40
|
+
}
|
|
41
|
+
validateRuleEntryPath(entryPath) {
|
|
42
|
+
const normalizedPath = entryPath.replaceAll("\\", "/").trim();
|
|
43
|
+
if (!normalizedPath.endsWith(".md")) {
|
|
44
|
+
throw new ValidationError("Rule path must end with .md.");
|
|
45
|
+
}
|
|
46
|
+
if (path.posix.basename(normalizedPath).toLowerCase() === "skill.md") {
|
|
47
|
+
throw new ValidationError("Rule path cannot target SKILL.md.");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
normalizeSkillPath(skillPath) {
|
|
51
|
+
const trimmedPath = skillPath.replaceAll("\\", "/").trim().replace(/\/+$/u, "");
|
|
52
|
+
const lowerCasePath = trimmedPath.toLowerCase();
|
|
53
|
+
if (trimmedPath.length === 0) {
|
|
54
|
+
throw new ValidationError("Skill path must not be empty.");
|
|
55
|
+
}
|
|
56
|
+
if (lowerCasePath === "skill.md") {
|
|
57
|
+
throw new ValidationError("Skill path must reference a skill folder, not SKILL.md directly.");
|
|
58
|
+
}
|
|
59
|
+
if (lowerCasePath.endsWith("/skill.md")) {
|
|
60
|
+
return trimmedPath.slice(0, -"/SKILL.md".length);
|
|
61
|
+
}
|
|
62
|
+
return trimmedPath;
|
|
63
|
+
}
|
|
64
|
+
async listLayerEntries(layer, kind, projectId, groupPath) {
|
|
65
|
+
const basePath = this.resolveEntryBasePath(kind, layer, projectId);
|
|
66
|
+
const resolvedBasePath = groupPath ? this.resolvePathWithinEntryBase(basePath, groupPath) : basePath;
|
|
67
|
+
const filePaths = await listManagedFilesRecursively(this.rootPath, resolvedBasePath);
|
|
68
|
+
return filePaths.map((filePath) => ({
|
|
69
|
+
path: path.relative(basePath, filePath),
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
async readLayerEntry(layer, kind, entryPath, projectId) {
|
|
73
|
+
const basePath = this.resolveEntryBasePath(kind, layer, projectId);
|
|
74
|
+
const candidatePaths = this.buildReadableEntryCandidates(kind, layer, entryPath);
|
|
75
|
+
for (const candidatePath of candidatePaths) {
|
|
76
|
+
const resolvedEntryPath = this.resolvePathWithinEntryBase(basePath, candidatePath);
|
|
77
|
+
if (await managedPathExists(this.rootPath, resolvedEntryPath)) {
|
|
78
|
+
return readManagedTextFile(this.rootPath, resolvedEntryPath);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
throw new ValidationError(`Entry not found: ${kind}/${entryPath}`);
|
|
82
|
+
}
|
|
83
|
+
async readLayerEntryOptional(layer, kind, entryPath, projectId) {
|
|
84
|
+
const basePath = this.resolveEntryBasePath(kind, layer, projectId);
|
|
85
|
+
const candidatePaths = this.buildReadableEntryCandidates(kind, layer, entryPath);
|
|
86
|
+
for (const candidatePath of candidatePaths) {
|
|
87
|
+
const resolvedEntryPath = this.resolvePathWithinEntryBase(basePath, candidatePath);
|
|
88
|
+
if (await managedPathExists(this.rootPath, resolvedEntryPath)) {
|
|
89
|
+
return readManagedTextFile(this.rootPath, resolvedEntryPath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
async upsertRule(layer, entryPath, content, projectId) {
|
|
95
|
+
this.validateRuleEntryPath(entryPath);
|
|
96
|
+
parseCanonicalRuleDocument(content);
|
|
97
|
+
const basePath = this.resolveEntryBasePath("rules", layer, projectId);
|
|
98
|
+
const resolvedEntryPath = this.resolvePathWithinEntryBase(basePath, entryPath);
|
|
99
|
+
const existed = await managedPathExists(this.rootPath, resolvedEntryPath);
|
|
100
|
+
await writeManagedTextFile(this.rootPath, resolvedEntryPath, content);
|
|
101
|
+
return {
|
|
102
|
+
status: existed ? "updated" : "created",
|
|
103
|
+
path: path.relative(basePath, resolvedEntryPath),
|
|
104
|
+
absolutePath: resolvedEntryPath,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async upsertSkill(layer, skillPath, content, projectId) {
|
|
108
|
+
const normalizedSkillPath = this.normalizeSkillPath(skillPath);
|
|
109
|
+
parseCanonicalSkillDocument(content);
|
|
110
|
+
const basePath = this.resolveEntryBasePath("skills", layer, projectId);
|
|
111
|
+
const resolvedSkillDirectory = this.resolvePathWithinEntryBase(basePath, normalizedSkillPath);
|
|
112
|
+
const resolvedEntryPath = path.join(resolvedSkillDirectory, "SKILL.md");
|
|
113
|
+
const existed = await managedPathExists(this.rootPath, resolvedEntryPath);
|
|
114
|
+
await writeManagedTextFile(this.rootPath, resolvedEntryPath, content);
|
|
115
|
+
return {
|
|
116
|
+
status: existed ? "updated" : "created",
|
|
117
|
+
path: path.relative(basePath, resolvedSkillDirectory),
|
|
118
|
+
filePath: path.relative(basePath, resolvedEntryPath),
|
|
119
|
+
absolutePath: resolvedEntryPath,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async deleteRule(layer, entryPath, projectId) {
|
|
123
|
+
this.validateRuleEntryPath(entryPath);
|
|
124
|
+
const basePath = this.resolveEntryBasePath("rules", layer, projectId);
|
|
125
|
+
const resolvedEntryPath = this.resolvePathWithinEntryBase(basePath, entryPath);
|
|
126
|
+
const deleted = await deleteManagedFile(this.rootPath, resolvedEntryPath);
|
|
127
|
+
return {
|
|
128
|
+
status: deleted ? "deleted" : "not_found",
|
|
129
|
+
path: path.relative(basePath, resolvedEntryPath),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
async deleteSkill(layer, skillPath, projectId) {
|
|
133
|
+
const normalizedSkillPath = this.normalizeSkillPath(skillPath);
|
|
134
|
+
const basePath = this.resolveEntryBasePath("skills", layer, projectId);
|
|
135
|
+
const resolvedSkillDirectory = this.resolvePathWithinEntryBase(basePath, normalizedSkillPath);
|
|
136
|
+
const deleted = await deleteManagedDirectory(this.rootPath, resolvedSkillDirectory);
|
|
137
|
+
return {
|
|
138
|
+
status: deleted ? "deleted" : "not_found",
|
|
139
|
+
path: path.relative(basePath, resolvedSkillDirectory),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { parseManifest } from "../core/bank/manifest.js";
|
|
2
|
+
import { resolveBankPaths } from "../core/bank/layout.js";
|
|
3
|
+
import { managedPathExists, readManagedJsonFile, writeManagedJsonFile } from "./safeFs.js";
|
|
4
|
+
export class ManifestStore {
|
|
5
|
+
rootPath;
|
|
6
|
+
paths;
|
|
7
|
+
constructor(rootPath, paths) {
|
|
8
|
+
this.rootPath = rootPath;
|
|
9
|
+
this.paths = paths;
|
|
10
|
+
}
|
|
11
|
+
async hasManifest() {
|
|
12
|
+
return managedPathExists(this.rootPath, this.paths.manifestFile);
|
|
13
|
+
}
|
|
14
|
+
async readManifest() {
|
|
15
|
+
const manifest = await readManagedJsonFile(this.rootPath, this.paths.manifestFile);
|
|
16
|
+
return parseManifest(manifest);
|
|
17
|
+
}
|
|
18
|
+
async readManifestOptional() {
|
|
19
|
+
if (!(await this.hasManifest())) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return this.readManifest();
|
|
23
|
+
}
|
|
24
|
+
async writeManifest(manifest) {
|
|
25
|
+
await writeManagedJsonFile(this.rootPath, this.paths.manifestFile, manifest);
|
|
26
|
+
}
|
|
27
|
+
async writeMcpServerConfig(config) {
|
|
28
|
+
await writeManagedJsonFile(this.rootPath, this.paths.mcpServerConfigFile, config);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { parseProjectBankManifest, parseProjectBankState } from "../core/bank/project.js";
|
|
3
|
+
import { resolveBankPaths } from "../core/bank/layout.js";
|
|
4
|
+
import { deleteManagedDirectory, ensureManagedDirectory, listManagedChildDirectories, managedPathExists, readManagedJsonFile, writeManagedJsonFile, } from "./safeFs.js";
|
|
5
|
+
export class ProjectBankStore {
|
|
6
|
+
rootPath;
|
|
7
|
+
paths;
|
|
8
|
+
constructor(rootPath, paths) {
|
|
9
|
+
this.rootPath = rootPath;
|
|
10
|
+
this.paths = paths;
|
|
11
|
+
}
|
|
12
|
+
async ensureProjectStructure(projectId) {
|
|
13
|
+
await ensureManagedDirectory(this.rootPath, this.paths.projectDirectory(projectId));
|
|
14
|
+
await ensureManagedDirectory(this.rootPath, this.paths.projectRulesDirectory(projectId));
|
|
15
|
+
await ensureManagedDirectory(this.rootPath, this.paths.projectSkillsDirectory(projectId));
|
|
16
|
+
await ensureManagedDirectory(this.rootPath, path.join(this.paths.projectRulesDirectory(projectId), "core"));
|
|
17
|
+
await ensureManagedDirectory(this.rootPath, path.join(this.paths.projectRulesDirectory(projectId), "stacks"));
|
|
18
|
+
await ensureManagedDirectory(this.rootPath, path.join(this.paths.projectRulesDirectory(projectId), "topics"));
|
|
19
|
+
}
|
|
20
|
+
async writeProjectManifest(projectId, manifest) {
|
|
21
|
+
await this.ensureProjectStructure(projectId);
|
|
22
|
+
await writeManagedJsonFile(this.rootPath, this.paths.projectManifestFile(projectId), manifest);
|
|
23
|
+
}
|
|
24
|
+
async readProjectManifestOptional(projectId) {
|
|
25
|
+
const manifestFilePath = this.paths.projectManifestFile(projectId);
|
|
26
|
+
if (!(await managedPathExists(this.rootPath, manifestFilePath))) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const manifest = await readManagedJsonFile(this.rootPath, manifestFilePath);
|
|
30
|
+
return parseProjectBankManifest(manifest);
|
|
31
|
+
}
|
|
32
|
+
async listProjectManifests() {
|
|
33
|
+
const projectDirectoryPaths = await listManagedChildDirectories(this.rootPath, this.paths.projectsDirectory);
|
|
34
|
+
const manifests = await Promise.all(projectDirectoryPaths.map(async (projectDirectoryPath) => {
|
|
35
|
+
const projectId = path.basename(projectDirectoryPath);
|
|
36
|
+
return this.readProjectManifestOptional(projectId);
|
|
37
|
+
}));
|
|
38
|
+
return manifests.filter((manifest) => manifest !== null);
|
|
39
|
+
}
|
|
40
|
+
async writeProjectState(projectId, state) {
|
|
41
|
+
await this.ensureProjectStructure(projectId);
|
|
42
|
+
await writeManagedJsonFile(this.rootPath, this.paths.projectStateFile(projectId), state);
|
|
43
|
+
}
|
|
44
|
+
async readProjectStateOptional(projectId) {
|
|
45
|
+
const stateFilePath = this.paths.projectStateFile(projectId);
|
|
46
|
+
if (!(await managedPathExists(this.rootPath, stateFilePath))) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const state = await readManagedJsonFile(this.rootPath, stateFilePath);
|
|
50
|
+
return parseProjectBankState(state);
|
|
51
|
+
}
|
|
52
|
+
async deleteProjectBank(projectId) {
|
|
53
|
+
return deleteManagedDirectory(this.rootPath, this.paths.projectDirectory(projectId));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { parseProviderIntegrationDescriptor } from "../core/bank/integration.js";
|
|
2
|
+
import { resolveBankPaths } from "../core/bank/layout.js";
|
|
3
|
+
import { managedPathExists, readManagedJsonFile, writeManagedJsonFile } from "./safeFs.js";
|
|
4
|
+
export class ProviderIntegrationStore {
|
|
5
|
+
rootPath;
|
|
6
|
+
paths;
|
|
7
|
+
constructor(rootPath, paths) {
|
|
8
|
+
this.rootPath = rootPath;
|
|
9
|
+
this.paths = paths;
|
|
10
|
+
}
|
|
11
|
+
async writeProviderIntegration(providerId, descriptor) {
|
|
12
|
+
await writeManagedJsonFile(this.rootPath, this.paths.integrationFile(providerId), descriptor);
|
|
13
|
+
}
|
|
14
|
+
async readProviderIntegrationOptional(providerId) {
|
|
15
|
+
const integrationFilePath = this.paths.integrationFile(providerId);
|
|
16
|
+
if (!(await managedPathExists(this.rootPath, integrationFilePath))) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const descriptor = await readManagedJsonFile(this.rootPath, integrationFilePath);
|
|
20
|
+
return parseProviderIntegrationDescriptor(descriptor);
|
|
21
|
+
}
|
|
22
|
+
}
|