@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Morsa contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @morsa/guidance-bank
|
|
2
|
+
|
|
3
|
+
`@morsa/guidance-bank` is a local tool for coding agents that stores persistent rules, skills, and reusable project guidance.
|
|
4
|
+
|
|
5
|
+
It gives agents a stable guidance layer across sessions, projects, and tools.
|
|
6
|
+
|
|
7
|
+
It gives you one durable place for reusable rules and skills across:
|
|
8
|
+
|
|
9
|
+
- different agent providers
|
|
10
|
+
- different projects
|
|
11
|
+
- repeated sessions in the same project
|
|
12
|
+
|
|
13
|
+
The goal is simple:
|
|
14
|
+
|
|
15
|
+
- improve agent quality over time
|
|
16
|
+
- reduce repeated prompting and repeated context reconstruction
|
|
17
|
+
- save tokens by keeping stable guidance in a managed local guidance layer
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
Install globally:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g @morsa/guidance-bank
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Initialize once:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
gbank init
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That is the whole manual setup.
|
|
34
|
+
|
|
35
|
+
After that, your agent can work with the AI Guidance Bank during normal coding sessions. When a project has no bank yet, the agent can detect that and guide creation as part of the workflow.
|
|
36
|
+
|
|
37
|
+
## Why It Exists
|
|
38
|
+
|
|
39
|
+
Agent guidance is usually fragmented.
|
|
40
|
+
|
|
41
|
+
- Some rules live in `AGENTS.md`.
|
|
42
|
+
- Some live in `.cursor`, `.claude`, or `.codex`.
|
|
43
|
+
- Some are project-specific.
|
|
44
|
+
- Some should be shared across many repositories.
|
|
45
|
+
- Most provider-native flows are still weak at generating a good long-lived bank from real project evidence.
|
|
46
|
+
|
|
47
|
+
`@morsa/guidance-bank` solves that by giving the agent one canonical local AI Guidance Bank it can use across providers and across projects.
|
|
48
|
+
|
|
49
|
+
It is designed for two kinds of guidance:
|
|
50
|
+
|
|
51
|
+
- cross-agent reusable guidance shared between projects
|
|
52
|
+
- project-specific guidance derived from the actual codebase and stack
|
|
53
|
+
|
|
54
|
+
## Supported Providers
|
|
55
|
+
|
|
56
|
+
Current provider integrations:
|
|
57
|
+
|
|
58
|
+
- Codex
|
|
59
|
+
- Cursor
|
|
60
|
+
- Claude Code
|
|
61
|
+
|
|
62
|
+
## What Happens Next
|
|
63
|
+
|
|
64
|
+
After `gbank init`, the normal flow is intentionally lightweight:
|
|
65
|
+
|
|
66
|
+
1. You open a project in your agent.
|
|
67
|
+
2. The agent resolves AI Guidance Bank context for that project.
|
|
68
|
+
3. If a project bank does not exist yet, the agent can propose creating it.
|
|
69
|
+
4. The agent can then keep using, improving, syncing, and editing the bank over time.
|
|
70
|
+
|
|
71
|
+
In practice, the agent can:
|
|
72
|
+
|
|
73
|
+
- create a project bank
|
|
74
|
+
- review and improve an existing bank
|
|
75
|
+
- sync an outdated bank layout
|
|
76
|
+
- add or update rules
|
|
77
|
+
- add or update skills
|
|
78
|
+
- delete obsolete entries
|
|
79
|
+
- read and inspect existing bank content
|
|
80
|
+
|
|
81
|
+
The goal is that the agent handles the workflow, instead of you manually managing rule files all the time.
|
|
82
|
+
|
|
83
|
+
## Why This Is Better Than Provider-Native Rules
|
|
84
|
+
|
|
85
|
+
Provider-native repository guidance is useful, but usually limited.
|
|
86
|
+
|
|
87
|
+
Common problems:
|
|
88
|
+
|
|
89
|
+
- guidance is locked to one provider
|
|
90
|
+
- project guidance is hard to reuse across repositories
|
|
91
|
+
- generated rule sets often collapse into folder-structure summaries instead of real operational guidance
|
|
92
|
+
- stack-specific guidance is usually shallow and repetitive
|
|
93
|
+
|
|
94
|
+
`@morsa/guidance-bank` aims to build better project guidance by:
|
|
95
|
+
|
|
96
|
+
- separating shared and project-specific guidance
|
|
97
|
+
- deriving rules from real project evidence
|
|
98
|
+
- carrying reusable rules across repositories
|
|
99
|
+
- keeping one user-managed canonical layer that works with multiple agents
|
|
100
|
+
|
|
101
|
+
## Stats
|
|
102
|
+
|
|
103
|
+
Use `gbank stats` for a local overview of the AI Guidance Bank and recent activity:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
gbank stats
|
|
107
|
+
gbank stats --project /absolute/project/path
|
|
108
|
+
gbank stats --json
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
It currently shows:
|
|
112
|
+
|
|
113
|
+
- shared rule and skill counts
|
|
114
|
+
- project bank counts and creation states
|
|
115
|
+
- recent audit events
|
|
116
|
+
- tool and provider activity breakdowns
|
|
117
|
+
|
|
118
|
+
This is the first visibility layer; it will keep getting richer.
|
|
119
|
+
|
|
120
|
+
## What We Plan To Improve
|
|
121
|
+
|
|
122
|
+
Near-term product direction:
|
|
123
|
+
|
|
124
|
+
- better visualization of rules and skills
|
|
125
|
+
- richer stats, including token-oriented usage and cost insight
|
|
126
|
+
- stronger project-bank management workflows
|
|
127
|
+
- team and workspace-oriented memory sharing
|
|
128
|
+
|
|
129
|
+
The long-term direction is not just “local rule files”, but a real guidance layer for agent work across projects, providers, and eventually teams.
|
|
130
|
+
|
|
131
|
+
## Current Notes
|
|
132
|
+
|
|
133
|
+
- `gbank init` requires an interactive terminal
|
|
134
|
+
- at least one supported provider CLI must already be installed and available on `PATH`
|
|
135
|
+
- the local AI Guidance Bank lives under `~/.guidance-bank`
|
package/bin/gbank.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { InitService } from "../../core/init/initService.js";
|
|
2
|
+
import { promptForProviders } from "../prompts/initPrompts.js";
|
|
3
|
+
export const runInitCommand = async () => {
|
|
4
|
+
const selectedProviders = await promptForProviders();
|
|
5
|
+
const initService = new InitService();
|
|
6
|
+
const result = await initService.run({
|
|
7
|
+
selectedProviders,
|
|
8
|
+
});
|
|
9
|
+
const configuredProviders = result.integrations
|
|
10
|
+
.filter((integration) => integration.action === "installed" || integration.action === "reconfigured")
|
|
11
|
+
.map((integration) => integration.descriptor.displayName);
|
|
12
|
+
const reusedProviders = result.integrations
|
|
13
|
+
.filter((integration) => integration.action === "skipped")
|
|
14
|
+
.map((integration) => integration.descriptor.displayName);
|
|
15
|
+
console.info(result.alreadyExisted
|
|
16
|
+
? `AI Guidance Bank is ready at ${result.bankRoot}.`
|
|
17
|
+
: `AI Guidance Bank initialized successfully at ${result.bankRoot}.`);
|
|
18
|
+
if (configuredProviders.length > 0) {
|
|
19
|
+
console.info(`Connected providers: ${configuredProviders.join(", ")}.`);
|
|
20
|
+
}
|
|
21
|
+
if (reusedProviders.length > 0) {
|
|
22
|
+
console.info(`Existing provider connections kept: ${reusedProviders.join(", ")}.`);
|
|
23
|
+
}
|
|
24
|
+
console.info("");
|
|
25
|
+
console.info("Next step:");
|
|
26
|
+
console.info("Open any project in your agent. The agent can use the AI Guidance Bank MCP to load durable rules and skills, detect when a project bank is missing, and guide you through creating or updating it.");
|
|
27
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createMcpServer } from "../../mcp/createMcpServer.js";
|
|
3
|
+
export const runMcpServeCommand = async () => {
|
|
4
|
+
const bankRoot = process.env.GUIDANCEBANK_ROOT;
|
|
5
|
+
const server = createMcpServer(bankRoot ? { bankRoot } : {});
|
|
6
|
+
const transport = new StdioServerTransport();
|
|
7
|
+
await server.connect(transport);
|
|
8
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { StatsService } from "../../core/stats/statsService.js";
|
|
3
|
+
import { GuidanceBankCliError, UserInputError } from "../../shared/errors.js";
|
|
4
|
+
const printStatsUsage = () => {
|
|
5
|
+
console.info("Usage: gbank stats [--project /absolute/project/path] [--json]");
|
|
6
|
+
};
|
|
7
|
+
const renderCountMap = (label, values) => {
|
|
8
|
+
const entries = Object.entries(values).sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
|
|
9
|
+
if (entries.length === 0) {
|
|
10
|
+
return [`${label}: none`];
|
|
11
|
+
}
|
|
12
|
+
return [
|
|
13
|
+
`${label}:`,
|
|
14
|
+
...entries.map(([key, count]) => ` - ${key}: ${count}`),
|
|
15
|
+
];
|
|
16
|
+
};
|
|
17
|
+
const renderLatestEvents = (label, events) => {
|
|
18
|
+
if (events.length === 0) {
|
|
19
|
+
return [`${label}: none`];
|
|
20
|
+
}
|
|
21
|
+
return [
|
|
22
|
+
`${label}:`,
|
|
23
|
+
...events.map((event) => ` - ${event.timestamp} ${event.tool} provider=${event.provider ?? "unknown"} project=${event.projectId} session=${event.sessionRef ?? "none"}`),
|
|
24
|
+
];
|
|
25
|
+
};
|
|
26
|
+
const renderTextStats = (stats) => {
|
|
27
|
+
const lines = [
|
|
28
|
+
`AI Guidance Bank at ${stats.bankRoot}`,
|
|
29
|
+
"",
|
|
30
|
+
`Bank ID: ${stats.manifest.bankId}`,
|
|
31
|
+
`Storage version: ${stats.manifest.storageVersion}`,
|
|
32
|
+
`Providers: ${stats.manifest.enabledProviders.join(", ") || "none"}`,
|
|
33
|
+
`Transport: ${stats.manifest.defaultMcpTransport}`,
|
|
34
|
+
`Updated: ${stats.manifest.updatedAt}`,
|
|
35
|
+
"",
|
|
36
|
+
`Shared entries: rules=${stats.sharedEntries.rules}, skills=${stats.sharedEntries.skills}`,
|
|
37
|
+
`Project banks: ${stats.projects.total}`,
|
|
38
|
+
...renderCountMap("Project creation states", stats.projects.byCreationState),
|
|
39
|
+
"",
|
|
40
|
+
`Audit events: ${stats.audit.totalEvents}`,
|
|
41
|
+
...renderCountMap("Events by tool", stats.audit.byTool),
|
|
42
|
+
...renderCountMap("Events by provider", stats.audit.byProvider),
|
|
43
|
+
...renderLatestEvents("Latest events", stats.audit.latestEvents),
|
|
44
|
+
];
|
|
45
|
+
if (!stats.project) {
|
|
46
|
+
return lines.join("\n");
|
|
47
|
+
}
|
|
48
|
+
lines.push("", `Project: ${stats.project.projectName}`, `Project ID: ${stats.project.projectId}`, `Project path: ${stats.project.projectPath}`, `Project state: ${stats.project.creationState}`, `Detected stacks: ${stats.project.detectedStacks.join(", ") || "none"}`, `Project entries: rules=${stats.project.entries.rules}, skills=${stats.project.entries.skills}`, `Project updated: ${stats.project.updatedAt}`, `Project audit events: ${stats.project.audit.totalEvents}`, ...renderCountMap("Project events by tool", stats.project.audit.byTool), ...renderCountMap("Project events by provider", stats.project.audit.byProvider), ...renderLatestEvents("Latest project events", stats.project.audit.latestEvents));
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
};
|
|
51
|
+
export const runStatsCommand = async (argv = process.argv.slice(2)) => {
|
|
52
|
+
const parsedArgs = parseArgs({
|
|
53
|
+
args: argv,
|
|
54
|
+
allowPositionals: true,
|
|
55
|
+
options: {
|
|
56
|
+
help: {
|
|
57
|
+
type: "boolean",
|
|
58
|
+
short: "h",
|
|
59
|
+
},
|
|
60
|
+
project: {
|
|
61
|
+
type: "string",
|
|
62
|
+
},
|
|
63
|
+
json: {
|
|
64
|
+
type: "boolean",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
if (parsedArgs.values.help) {
|
|
69
|
+
printStatsUsage();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (parsedArgs.positionals.length > 1 || (parsedArgs.positionals[0] && parsedArgs.positionals[0] !== "stats")) {
|
|
73
|
+
throw new UserInputError("Usage: gbank stats [--project /absolute/project/path] [--json]");
|
|
74
|
+
}
|
|
75
|
+
const statsService = new StatsService();
|
|
76
|
+
try {
|
|
77
|
+
const stats = await statsService.collect(parsedArgs.values.project
|
|
78
|
+
? {
|
|
79
|
+
projectPath: parsedArgs.values.project,
|
|
80
|
+
}
|
|
81
|
+
: undefined);
|
|
82
|
+
if (parsedArgs.values.json) {
|
|
83
|
+
console.info(JSON.stringify(stats, null, 2));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
console.info(renderTextStats(stats));
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
const message = error instanceof Error ? error.message : "Unknown stats error.";
|
|
90
|
+
throw new GuidanceBankCliError(message);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { runInitCommand } from "./commands/init.js";
|
|
5
|
+
import { runMcpServeCommand } from "./commands/mcpServe.js";
|
|
6
|
+
import { runStatsCommand } from "./commands/stats.js";
|
|
7
|
+
import { GuidanceBankCliError } from "../shared/errors.js";
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const packageJson = require("../../package.json");
|
|
10
|
+
const printUsage = () => {
|
|
11
|
+
console.info(`AI Guidance Bank
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
gbank init
|
|
15
|
+
gbank stats [--project /absolute/project/path] [--json]
|
|
16
|
+
gbank mcp serve
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
-h, --help
|
|
20
|
+
-v, --version
|
|
21
|
+
`);
|
|
22
|
+
};
|
|
23
|
+
const main = async () => {
|
|
24
|
+
const rawArgv = process.argv.slice(2);
|
|
25
|
+
const [rawCommand, rawSubcommand] = rawArgv;
|
|
26
|
+
if (rawCommand === "stats" && !["serve"].includes(rawSubcommand ?? "")) {
|
|
27
|
+
await runStatsCommand(rawArgv);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const parsedArgs = parseArgs({
|
|
31
|
+
allowPositionals: true,
|
|
32
|
+
options: {
|
|
33
|
+
help: {
|
|
34
|
+
type: "boolean",
|
|
35
|
+
short: "h",
|
|
36
|
+
},
|
|
37
|
+
version: {
|
|
38
|
+
type: "boolean",
|
|
39
|
+
short: "v",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
if (parsedArgs.values.help) {
|
|
44
|
+
printUsage();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (parsedArgs.values.version) {
|
|
48
|
+
console.info(packageJson.version);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const [command, subcommand] = parsedArgs.positionals;
|
|
52
|
+
if (command === "init" && !subcommand) {
|
|
53
|
+
await runInitCommand();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (command === "mcp" && subcommand === "serve") {
|
|
57
|
+
await runMcpServeCommand();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
printUsage();
|
|
61
|
+
throw new GuidanceBankCliError("Unsupported command.");
|
|
62
|
+
};
|
|
63
|
+
main().catch((error) => {
|
|
64
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
65
|
+
console.error(message);
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ensureMcpLauncher } from "../mcp/launcher.js";
|
|
4
|
+
import { resolveDefaultBankRoot } from "../shared/paths.js";
|
|
5
|
+
const hasInitializedBank = async (bankRoot) => {
|
|
6
|
+
try {
|
|
7
|
+
await access(path.join(bankRoot, "manifest.json"));
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
export const refreshDefaultMcpLauncherIfInitialized = async (options = {}) => {
|
|
15
|
+
const bankRoot = options.bankRoot ?? resolveDefaultBankRoot();
|
|
16
|
+
if (!(await hasInitializedBank(bankRoot))) {
|
|
17
|
+
return "skipped";
|
|
18
|
+
}
|
|
19
|
+
await ensureMcpLauncher(bankRoot);
|
|
20
|
+
return "updated";
|
|
21
|
+
};
|
|
22
|
+
export const runPostinstall = async () => {
|
|
23
|
+
await refreshDefaultMcpLauncherIfInitialized();
|
|
24
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { PROVIDER_DEFINITIONS } from "../../core/providers/providerRegistry.js";
|
|
4
|
+
import { UserInputError } from "../../shared/errors.js";
|
|
5
|
+
import { getProviderAvailability } from "./providerAvailability.js";
|
|
6
|
+
const selectionSeparators = /[\s,]+/;
|
|
7
|
+
const parseSelection = (value, availableProviders) => {
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
const availableProviderIds = new Set(availableProviders.filter((provider) => provider.available).map((provider) => provider.id));
|
|
10
|
+
if (!trimmed) {
|
|
11
|
+
const defaults = PROVIDER_DEFINITIONS.filter((provider) => availableProviderIds.has(provider.id)).map((provider) => provider.id);
|
|
12
|
+
if (defaults.length === 0) {
|
|
13
|
+
throw new UserInputError("No supported provider CLIs were found on PATH. Install at least one provider CLI first.");
|
|
14
|
+
}
|
|
15
|
+
return defaults;
|
|
16
|
+
}
|
|
17
|
+
const loweredValue = trimmed.toLowerCase();
|
|
18
|
+
if (loweredValue === "all" || loweredValue === "available") {
|
|
19
|
+
const defaults = PROVIDER_DEFINITIONS.filter((provider) => availableProviderIds.has(provider.id)).map((provider) => provider.id);
|
|
20
|
+
if (defaults.length === 0) {
|
|
21
|
+
throw new UserInputError("No supported provider CLIs were found on PATH. Install at least one provider CLI first.");
|
|
22
|
+
}
|
|
23
|
+
return defaults;
|
|
24
|
+
}
|
|
25
|
+
const selectedProviders = new Set();
|
|
26
|
+
const tokens = trimmed.split(selectionSeparators).filter(Boolean);
|
|
27
|
+
for (const token of tokens) {
|
|
28
|
+
const lowerToken = token.toLowerCase();
|
|
29
|
+
const numericIndex = Number.parseInt(lowerToken, 10);
|
|
30
|
+
const providerByIndex = Number.isInteger(numericIndex) && numericIndex >= 1 && numericIndex <= PROVIDER_DEFINITIONS.length
|
|
31
|
+
? PROVIDER_DEFINITIONS[numericIndex - 1]
|
|
32
|
+
: undefined;
|
|
33
|
+
if (providerByIndex) {
|
|
34
|
+
if (!availableProviderIds.has(providerByIndex.id)) {
|
|
35
|
+
const unavailableProvider = availableProviders.find((provider) => provider.id === providerByIndex.id);
|
|
36
|
+
throw new UserInputError(unavailableProvider?.unavailableMessage ?? `${providerByIndex.displayName} is not available.`);
|
|
37
|
+
}
|
|
38
|
+
selectedProviders.add(providerByIndex.id);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const providerByName = PROVIDER_DEFINITIONS.find((provider) => {
|
|
42
|
+
const normalizedLabel = provider.displayName.toLowerCase().replaceAll(" ", "-");
|
|
43
|
+
return lowerToken === provider.id || lowerToken === normalizedLabel;
|
|
44
|
+
});
|
|
45
|
+
if (providerByName) {
|
|
46
|
+
if (!availableProviderIds.has(providerByName.id)) {
|
|
47
|
+
const unavailableProvider = availableProviders.find((provider) => provider.id === providerByName.id);
|
|
48
|
+
throw new UserInputError(unavailableProvider?.unavailableMessage ?? `${providerByName.displayName} is not available.`);
|
|
49
|
+
}
|
|
50
|
+
selectedProviders.add(providerByName.id);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
throw new UserInputError(`Unsupported provider selection: ${token}`);
|
|
54
|
+
}
|
|
55
|
+
if (selectedProviders.size === 0) {
|
|
56
|
+
throw new UserInputError("You must select at least one provider.");
|
|
57
|
+
}
|
|
58
|
+
return [...selectedProviders];
|
|
59
|
+
};
|
|
60
|
+
export const promptForProviders = async () => {
|
|
61
|
+
if (!input.isTTY || !output.isTTY) {
|
|
62
|
+
throw new UserInputError("gbank init requires an interactive terminal in the current MVP.");
|
|
63
|
+
}
|
|
64
|
+
const availability = await getProviderAvailability();
|
|
65
|
+
output.write("Select providers to enable for AI Guidance Bank MCP:\n");
|
|
66
|
+
for (const [index, provider] of availability.entries()) {
|
|
67
|
+
output.write(`${index + 1}. ${provider.displayName} [${provider.available ? "available" : "not found"}]\n`);
|
|
68
|
+
}
|
|
69
|
+
output.write('Press Enter to select all available providers. Type numbers, ids, or "all".\n');
|
|
70
|
+
const readline = createInterface({ input, output });
|
|
71
|
+
try {
|
|
72
|
+
while (true) {
|
|
73
|
+
const answer = await readline.question("> ");
|
|
74
|
+
try {
|
|
75
|
+
return parseSelection(answer, availability);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof UserInputError) {
|
|
79
|
+
output.write(`${error.message}\n`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
readline.close();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { delimiter } from "node:path";
|
|
4
|
+
import { PROVIDER_DEFINITIONS } from "../../core/providers/providerRegistry.js";
|
|
5
|
+
const isExecutableAvailable = async (command) => {
|
|
6
|
+
const pathValue = process.env.PATH;
|
|
7
|
+
if (!pathValue) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
for (const directoryPath of pathValue.split(delimiter)) {
|
|
11
|
+
if (!directoryPath) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const executablePath = path.join(directoryPath, command);
|
|
15
|
+
try {
|
|
16
|
+
await access(executablePath);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
};
|
|
25
|
+
export const getProviderAvailability = async () => Promise.all(PROVIDER_DEFINITIONS.map(async (provider) => ({
|
|
26
|
+
id: provider.id,
|
|
27
|
+
displayName: provider.displayName,
|
|
28
|
+
cliCommand: provider.cliCommand,
|
|
29
|
+
available: provider.isAvailable ? await provider.isAvailable() : await isExecutableAvailable(provider.cliCommand),
|
|
30
|
+
unavailableMessage: provider.unavailableMessage,
|
|
31
|
+
})));
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { parseCanonicalRuleDocument, parseCanonicalSkillDocument } from "../bank/canonicalEntry.js";
|
|
3
|
+
const countLines = (content) => (content.length === 0 ? 0 : content.split(/\r?\n/u).length);
|
|
4
|
+
export const summarizeEntryContent = (kind, content) => {
|
|
5
|
+
if (content === null) {
|
|
6
|
+
return {
|
|
7
|
+
exists: false,
|
|
8
|
+
sha256: null,
|
|
9
|
+
charCount: 0,
|
|
10
|
+
lineCount: 0,
|
|
11
|
+
entryId: null,
|
|
12
|
+
title: null,
|
|
13
|
+
entryKind: null,
|
|
14
|
+
stacks: [],
|
|
15
|
+
topics: [],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const baseSummary = {
|
|
19
|
+
exists: true,
|
|
20
|
+
sha256: createHash("sha256").update(content, "utf8").digest("hex"),
|
|
21
|
+
charCount: content.length,
|
|
22
|
+
lineCount: countLines(content),
|
|
23
|
+
entryId: null,
|
|
24
|
+
title: null,
|
|
25
|
+
entryKind: null,
|
|
26
|
+
stacks: [],
|
|
27
|
+
topics: [],
|
|
28
|
+
};
|
|
29
|
+
try {
|
|
30
|
+
const document = kind === "rules" ? parseCanonicalRuleDocument(content) : parseCanonicalSkillDocument(content);
|
|
31
|
+
return {
|
|
32
|
+
...baseSummary,
|
|
33
|
+
entryId: document.frontmatter.id,
|
|
34
|
+
title: document.frontmatter.title,
|
|
35
|
+
entryKind: document.frontmatter.kind,
|
|
36
|
+
stacks: [...document.frontmatter.stacks],
|
|
37
|
+
topics: [...document.frontmatter.topics],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return baseSummary;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { DETECTABLE_STACKS } from "../context/types.js";
|
|
3
|
+
const DetectableStackSchema = z.enum(DETECTABLE_STACKS);
|
|
4
|
+
const RuleFrontmatterSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
id: z.string().trim().min(1),
|
|
7
|
+
kind: z.literal("rule"),
|
|
8
|
+
title: z.string().trim().min(1),
|
|
9
|
+
stacks: z.array(DetectableStackSchema).default([]),
|
|
10
|
+
topics: z.array(z.string().trim().min(1)).default([]),
|
|
11
|
+
})
|
|
12
|
+
.strict();
|
|
13
|
+
const SkillFrontmatterSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
id: z.string().trim().min(1),
|
|
16
|
+
kind: z.literal("skill"),
|
|
17
|
+
title: z.string().trim().min(1),
|
|
18
|
+
name: z.string().trim().min(1).optional(),
|
|
19
|
+
description: z.string().trim().min(1),
|
|
20
|
+
stacks: z.array(DetectableStackSchema).default([]),
|
|
21
|
+
topics: z.array(z.string().trim().min(1)).default([]),
|
|
22
|
+
})
|
|
23
|
+
.strict();
|
|
24
|
+
const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/u;
|
|
25
|
+
const parseScalarValue = (rawValue) => {
|
|
26
|
+
const trimmedValue = rawValue.trim();
|
|
27
|
+
if (trimmedValue.startsWith("[") && trimmedValue.endsWith("]")) {
|
|
28
|
+
const innerValue = trimmedValue.slice(1, -1).trim();
|
|
29
|
+
if (innerValue.length === 0) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
return innerValue.split(",").map((item) => item.trim().replace(/^['"]|['"]$/gu, ""));
|
|
33
|
+
}
|
|
34
|
+
return trimmedValue.replace(/^['"]|['"]$/gu, "");
|
|
35
|
+
};
|
|
36
|
+
const parseFrontmatterBlock = (content) => {
|
|
37
|
+
const match = content.match(FRONTMATTER_PATTERN);
|
|
38
|
+
if (!match) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const rawFrontmatter = match[1] ?? "";
|
|
42
|
+
const body = match[2]?.trim() ?? "";
|
|
43
|
+
const frontmatter = {};
|
|
44
|
+
for (const line of rawFrontmatter.split(/\r?\n/u)) {
|
|
45
|
+
const trimmedLine = line.trim();
|
|
46
|
+
if (trimmedLine.length === 0 || trimmedLine.startsWith("#")) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const separatorIndex = trimmedLine.indexOf(":");
|
|
50
|
+
if (separatorIndex <= 0) {
|
|
51
|
+
throw new Error(`Invalid frontmatter line: ${trimmedLine}`);
|
|
52
|
+
}
|
|
53
|
+
const key = trimmedLine.slice(0, separatorIndex).trim();
|
|
54
|
+
const rawValue = trimmedLine.slice(separatorIndex + 1);
|
|
55
|
+
frontmatter[key] = parseScalarValue(rawValue);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
frontmatter,
|
|
59
|
+
body,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
const assertBody = (body) => {
|
|
63
|
+
const trimmedBody = body.trim();
|
|
64
|
+
if (trimmedBody.length === 0) {
|
|
65
|
+
throw new Error("Canonical entry body must not be empty.");
|
|
66
|
+
}
|
|
67
|
+
return trimmedBody;
|
|
68
|
+
};
|
|
69
|
+
export const parseCanonicalRuleDocument = (content) => {
|
|
70
|
+
const parsedContent = parseFrontmatterBlock(content);
|
|
71
|
+
if (!parsedContent) {
|
|
72
|
+
throw new Error("Canonical rule files must start with a frontmatter block.");
|
|
73
|
+
}
|
|
74
|
+
const frontmatter = RuleFrontmatterSchema.parse(parsedContent.frontmatter);
|
|
75
|
+
return {
|
|
76
|
+
frontmatter,
|
|
77
|
+
body: assertBody(parsedContent.body),
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
export const parseCanonicalSkillDocument = (content) => {
|
|
81
|
+
const parsedContent = parseFrontmatterBlock(content);
|
|
82
|
+
if (!parsedContent) {
|
|
83
|
+
throw new Error("Canonical skill files must start with a frontmatter block.");
|
|
84
|
+
}
|
|
85
|
+
const frontmatter = SkillFrontmatterSchema.parse(parsedContent.frontmatter);
|
|
86
|
+
return {
|
|
87
|
+
frontmatter,
|
|
88
|
+
body: assertBody(parsedContent.body),
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
export const serializeCanonicalRuleFrontmatter = (frontmatter) => `---
|
|
92
|
+
id: ${frontmatter.id}
|
|
93
|
+
kind: rule
|
|
94
|
+
title: ${frontmatter.title}
|
|
95
|
+
stacks: [${frontmatter.stacks.join(", ")}]
|
|
96
|
+
topics: [${frontmatter.topics.join(", ")}]
|
|
97
|
+
---`;
|
|
98
|
+
export const serializeCanonicalSkillFrontmatter = (frontmatter) => `---
|
|
99
|
+
id: ${frontmatter.id}
|
|
100
|
+
kind: skill
|
|
101
|
+
title: ${frontmatter.title}
|
|
102
|
+
${frontmatter.name ? `name: ${frontmatter.name}
|
|
103
|
+
` : ""}description: ${frontmatter.description}
|
|
104
|
+
stacks: [${frontmatter.stacks.join(", ")}]
|
|
105
|
+
topics: [${frontmatter.topics.join(", ")}]
|
|
106
|
+
---`;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { McpServerConfigSchema } from "../../mcp/config.js";
|
|
3
|
+
import { PROVIDER_IDS } from "./types.js";
|
|
4
|
+
export const ProviderIntegrationDescriptorSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
schemaVersion: z.literal(1),
|
|
7
|
+
provider: z.enum(PROVIDER_IDS),
|
|
8
|
+
displayName: z.string().min(1),
|
|
9
|
+
serverName: z.string().min(1),
|
|
10
|
+
installationMethod: z.enum(["provider-cli", "config-file"]),
|
|
11
|
+
scope: z.literal("user"),
|
|
12
|
+
mcpServer: McpServerConfigSchema,
|
|
13
|
+
instructions: z.array(z.string()),
|
|
14
|
+
})
|
|
15
|
+
.strict();
|
|
16
|
+
export const parseProviderIntegrationDescriptor = (value) => ProviderIntegrationDescriptorSchema.parse(value);
|