@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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +135 -0
  3. package/bin/gbank.js +3 -0
  4. package/dist/cli/commands/init.js +27 -0
  5. package/dist/cli/commands/mcpServe.js +8 -0
  6. package/dist/cli/commands/stats.js +92 -0
  7. package/dist/cli/index.js +67 -0
  8. package/dist/cli/postinstall.js +24 -0
  9. package/dist/cli/prompts/initPrompts.js +89 -0
  10. package/dist/cli/prompts/providerAvailability.js +31 -0
  11. package/dist/core/audit/summarizeEntryContent.js +43 -0
  12. package/dist/core/audit/types.js +1 -0
  13. package/dist/core/bank/canonicalEntry.js +106 -0
  14. package/dist/core/bank/integration.js +16 -0
  15. package/dist/core/bank/layout.js +159 -0
  16. package/dist/core/bank/lifecycle.js +24 -0
  17. package/dist/core/bank/manifest.js +37 -0
  18. package/dist/core/bank/project.js +105 -0
  19. package/dist/core/bank/types.js +6 -0
  20. package/dist/core/context/contextEntryResolver.js +140 -0
  21. package/dist/core/context/contextTextRenderer.js +116 -0
  22. package/dist/core/context/detectProjectContext.js +118 -0
  23. package/dist/core/context/resolveContextService.js +138 -0
  24. package/dist/core/context/types.js +1 -0
  25. package/dist/core/init/initService.js +112 -0
  26. package/dist/core/init/initTypes.js +1 -0
  27. package/dist/core/projects/createBankDeriveGuidance/index.js +47 -0
  28. package/dist/core/projects/createBankDeriveGuidance/shared/general.js +49 -0
  29. package/dist/core/projects/createBankDeriveGuidance/shared/typescript.js +18 -0
  30. package/dist/core/projects/createBankDeriveGuidance/stacks/angular.js +252 -0
  31. package/dist/core/projects/createBankDeriveGuidance/stacks/ios.js +254 -0
  32. package/dist/core/projects/createBankDeriveGuidance/stacks/nextjs.js +220 -0
  33. package/dist/core/projects/createBankDeriveGuidance/stacks/nodejs.js +221 -0
  34. package/dist/core/projects/createBankDeriveGuidance/stacks/other.js +34 -0
  35. package/dist/core/projects/createBankDeriveGuidance/stacks/react.js +214 -0
  36. package/dist/core/projects/createBankFlow.js +252 -0
  37. package/dist/core/projects/createBankIterationPrompt.js +294 -0
  38. package/dist/core/projects/createBankPrompt.js +95 -0
  39. package/dist/core/projects/createFlowPhases.js +28 -0
  40. package/dist/core/projects/discoverCurrentProjectBank.js +43 -0
  41. package/dist/core/projects/discoverExistingGuidance.js +99 -0
  42. package/dist/core/projects/discoverProjectEvidence.js +87 -0
  43. package/dist/core/projects/discoverRecentCommits.js +28 -0
  44. package/dist/core/projects/findReferenceProjects.js +42 -0
  45. package/dist/core/projects/guidanceStrategies.js +29 -0
  46. package/dist/core/projects/identity.js +16 -0
  47. package/dist/core/projects/providerProjectGuidance.js +82 -0
  48. package/dist/core/providers/providerRegistry.js +51 -0
  49. package/dist/core/providers/types.js +1 -0
  50. package/dist/core/stats/statsService.js +117 -0
  51. package/dist/core/sync/syncService.js +145 -0
  52. package/dist/core/sync/syncTypes.js +1 -0
  53. package/dist/core/upgrade/upgradeService.js +134 -0
  54. package/dist/integrations/claudeCode/install.js +78 -0
  55. package/dist/integrations/codex/install.js +80 -0
  56. package/dist/integrations/commandRunner.js +32 -0
  57. package/dist/integrations/cursor/install.js +118 -0
  58. package/dist/integrations/shared.js +20 -0
  59. package/dist/mcp/config.js +19 -0
  60. package/dist/mcp/createMcpServer.js +31 -0
  61. package/dist/mcp/launcher.js +49 -0
  62. package/dist/mcp/registerTools.js +33 -0
  63. package/dist/mcp/serverMetadata.js +7 -0
  64. package/dist/mcp/tools/auditUtils.js +49 -0
  65. package/dist/mcp/tools/createBankApply.js +106 -0
  66. package/dist/mcp/tools/createBankToolRuntime.js +115 -0
  67. package/dist/mcp/tools/createBankToolSchemas.js +234 -0
  68. package/dist/mcp/tools/entryMutationHelpers.js +44 -0
  69. package/dist/mcp/tools/registerBankManifestTool.js +47 -0
  70. package/dist/mcp/tools/registerClearProjectBankTool.js +73 -0
  71. package/dist/mcp/tools/registerCreateBankTool.js +240 -0
  72. package/dist/mcp/tools/registerDeleteEntryTool.js +98 -0
  73. package/dist/mcp/tools/registerDeleteGuidanceSourceTool.js +120 -0
  74. package/dist/mcp/tools/registerListEntriesTool.js +94 -0
  75. package/dist/mcp/tools/registerReadEntryTool.js +99 -0
  76. package/dist/mcp/tools/registerResolveContextTool.js +128 -0
  77. package/dist/mcp/tools/registerSetProjectStateTool.js +121 -0
  78. package/dist/mcp/tools/registerSyncBankTool.js +113 -0
  79. package/dist/mcp/tools/registerUpgradeBankTool.js +89 -0
  80. package/dist/mcp/tools/registerUpsertRuleTool.js +100 -0
  81. package/dist/mcp/tools/registerUpsertSkillTool.js +102 -0
  82. package/dist/mcp/tools/sharedSchemas.js +13 -0
  83. package/dist/shared/errors.js +18 -0
  84. package/dist/shared/paths.js +11 -0
  85. package/dist/storage/atomicWrite.js +15 -0
  86. package/dist/storage/auditLogger.js +20 -0
  87. package/dist/storage/auditStore.js +22 -0
  88. package/dist/storage/bankRepository.js +168 -0
  89. package/dist/storage/entryStore.js +142 -0
  90. package/dist/storage/manifestStore.js +30 -0
  91. package/dist/storage/projectBankStore.js +55 -0
  92. package/dist/storage/providerIntegrationStore.js +22 -0
  93. package/dist/storage/safeFs.js +202 -0
  94. package/package.json +64 -0
  95. 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,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "../dist/cli/index.js";
@@ -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);