@intentius/chant 0.0.18 → 0.0.24
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/bin/chant +4 -1
- package/package.json +20 -1
- package/src/build.test.ts +4 -2
- package/src/build.ts +3 -0
- package/src/builder.test.ts +3 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +0 -3
- package/src/cli/commands/build.ts +5 -12
- package/src/cli/commands/diff.test.ts +2 -1
- package/src/cli/commands/diff.ts +2 -1
- package/src/cli/commands/init-lexicon/templates/codegen.ts +188 -0
- package/src/cli/commands/init-lexicon/templates/docs.ts +81 -0
- package/src/cli/commands/init-lexicon/templates/examples.ts +35 -0
- package/src/cli/commands/init-lexicon/templates/lint.ts +30 -0
- package/src/cli/commands/init-lexicon/templates/lsp.ts +39 -0
- package/src/cli/commands/init-lexicon/templates/plugin.ts +110 -0
- package/src/cli/commands/init-lexicon/templates/project.ts +182 -0
- package/src/cli/commands/init-lexicon/templates/spec.ts +57 -0
- package/src/cli/commands/init-lexicon/templates/tests.ts +70 -0
- package/src/cli/commands/init-lexicon.test.ts +0 -9
- package/src/cli/commands/init-lexicon.ts +12 -868
- package/src/cli/commands/init.ts +2 -20
- package/src/cli/conflict-check.test.ts +43 -0
- package/src/cli/handlers/build.ts +3 -3
- package/src/cli/handlers/lint.ts +2 -2
- package/src/cli/handlers/spell.ts +396 -0
- package/src/cli/handlers/state.ts +230 -0
- package/src/cli/lsp/server.test.ts +4 -0
- package/src/cli/main.ts +37 -3
- package/src/cli/mcp/resource-handlers.ts +227 -0
- package/src/cli/mcp/server.test.ts +13 -9
- package/src/cli/mcp/server.ts +24 -199
- package/src/cli/mcp/state-tools.ts +138 -0
- package/src/cli/mcp/tools/build.ts +2 -1
- package/src/cli/mcp/types.ts +45 -0
- package/src/cli/plugins.ts +1 -1
- package/src/cli/reporters/stylish.test.ts +2 -2
- package/src/cli/reporters/stylish.ts +1 -1
- package/src/codegen/docs-file-markers.ts +69 -0
- package/src/codegen/docs-rule-scanning.ts +159 -0
- package/src/codegen/docs-sections.ts +159 -0
- package/src/codegen/docs-sidebar.ts +56 -0
- package/src/codegen/docs-types.ts +79 -0
- package/src/codegen/docs.ts +9 -495
- package/src/composite.test.ts +76 -1
- package/src/composite.ts +37 -0
- package/src/config.ts +4 -0
- package/src/declarable.test.ts +2 -1
- package/src/declarable.ts +1 -1
- package/src/discovery/collect.test.ts +34 -0
- package/src/discovery/collect.ts +12 -0
- package/src/discovery/graph.test.ts +40 -0
- package/src/discovery/import.test.ts +5 -5
- package/src/discovery/resolve.test.ts +20 -0
- package/src/discovery/resolve.ts +2 -2
- package/src/index.ts +2 -0
- package/src/lexicon-plugin-helpers.ts +130 -0
- package/src/lexicon.ts +24 -0
- package/src/lint/rule-options.test.ts +3 -3
- package/src/lint/rule-registry.test.ts +1 -1
- package/src/lint/rules/composite-scope.ts +1 -1
- package/src/serializer-walker.ts +2 -1
- package/src/spell/discovery.ts +183 -0
- package/src/spell/index.ts +3 -0
- package/src/spell/prompt.ts +133 -0
- package/src/spell/types.ts +89 -0
- package/src/state/digest.ts +88 -0
- package/src/state/git.ts +317 -0
- package/src/state/index.ts +4 -0
- package/src/state/snapshot.ts +179 -0
- package/src/state/types.ts +59 -0
- package/src/toml-emit.ts +182 -0
- package/src/toml-parse.ts +370 -0
- package/src/toml-utils.ts +60 -0
- package/src/toml.ts +5 -602
- package/src/types.ts +2 -1
- package/src/utils.test.ts +16 -3
- package/src/utils.ts +31 -1
- package/src/validation.test.ts +11 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/getting-started.mdx +0 -6
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/lint-rules.mdx +0 -6
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/serialization.mdx +0 -6
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/actions/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/composites/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/coverage.ts +0 -11
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/generator.ts +0 -10
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/parser.ts +0 -10
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/post-synth/.gitkeep +0 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { build } from "../../build";
|
|
3
|
+
import { takeSnapshot } from "../../state/snapshot";
|
|
4
|
+
import { readSnapshot, readEnvironmentSnapshots, listSnapshots, fetchState } from "../../state/git";
|
|
5
|
+
import { computeBuildDigest, diffDigests } from "../../state/digest";
|
|
6
|
+
import { loadChantConfig } from "../../config";
|
|
7
|
+
import { formatError, formatWarning, formatSuccess, formatBold } from "../format";
|
|
8
|
+
import type { CommandContext } from "../registry";
|
|
9
|
+
import type { StateSnapshot } from "../../state/types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* chant state snapshot <environment> [lexicon]
|
|
13
|
+
*/
|
|
14
|
+
export async function runStateSnapshot(ctx: CommandContext): Promise<number> {
|
|
15
|
+
const { args, plugins } = ctx;
|
|
16
|
+
const environment = args.extraPositional;
|
|
17
|
+
const lexiconFilter = args.extraPositional2;
|
|
18
|
+
|
|
19
|
+
if (!environment) {
|
|
20
|
+
console.error(formatError({ message: "Environment is required: chant state snapshot <environment> [lexicon]" }));
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Validate environment against config
|
|
25
|
+
const projectPath = resolve(".");
|
|
26
|
+
const { config } = await loadChantConfig(projectPath);
|
|
27
|
+
if (config.environments && !config.environments.includes(environment)) {
|
|
28
|
+
console.error(formatError({
|
|
29
|
+
message: `Unknown environment "${environment}"`,
|
|
30
|
+
hint: `Defined environments: ${config.environments.join(", ")}`,
|
|
31
|
+
}));
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Filter plugins if lexicon specified
|
|
36
|
+
const targetPlugins = lexiconFilter
|
|
37
|
+
? plugins.filter((p) => p.name === lexiconFilter)
|
|
38
|
+
: plugins;
|
|
39
|
+
const targetSerializers = targetPlugins.map((p) => p.serializer);
|
|
40
|
+
|
|
41
|
+
// Build first to get entity names and build output
|
|
42
|
+
const buildResult = await build(projectPath, targetSerializers);
|
|
43
|
+
if (buildResult.errors.length > 0) {
|
|
44
|
+
console.error(formatError({ message: "Build failed — fix errors before taking a snapshot" }));
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const pluginsWithDescribe = targetPlugins.filter((p) => p.describeResources);
|
|
49
|
+
if (pluginsWithDescribe.length === 0) {
|
|
50
|
+
console.error(formatError({
|
|
51
|
+
message: "No plugins implement describeResources",
|
|
52
|
+
hint: lexiconFilter ? `Lexicon "${lexiconFilter}" does not support state snapshots` : undefined,
|
|
53
|
+
}));
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const result = await takeSnapshot(environment, pluginsWithDescribe, buildResult);
|
|
58
|
+
|
|
59
|
+
for (const w of result.warnings) {
|
|
60
|
+
console.error(formatWarning({ message: w }));
|
|
61
|
+
}
|
|
62
|
+
for (const e of result.errors) {
|
|
63
|
+
console.error(formatError({ message: e }));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (result.snapshots.length > 0) {
|
|
67
|
+
const counts = result.snapshots
|
|
68
|
+
.map((s) => `${s.lexicon}(${Object.keys(s.resources).length})`)
|
|
69
|
+
.join(" ");
|
|
70
|
+
console.error(formatSuccess(`Snapshot saved to chant/state (${counts})`));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return result.errors.length > 0 && result.snapshots.length === 0 ? 1 : 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* chant state show <environment> [lexicon]
|
|
78
|
+
*/
|
|
79
|
+
export async function runStateShow(ctx: CommandContext): Promise<number> {
|
|
80
|
+
const environment = ctx.args.extraPositional;
|
|
81
|
+
const lexiconFilter = ctx.args.extraPositional2;
|
|
82
|
+
|
|
83
|
+
if (!environment) {
|
|
84
|
+
console.error(formatError({ message: "Environment is required: chant state show <environment> [lexicon]" }));
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Fetch from remote first
|
|
89
|
+
await fetchState();
|
|
90
|
+
|
|
91
|
+
if (lexiconFilter) {
|
|
92
|
+
const content = await readSnapshot(environment, lexiconFilter);
|
|
93
|
+
if (!content) {
|
|
94
|
+
console.error(formatError({ message: `No snapshot found for ${environment}/${lexiconFilter}` }));
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const snapshot: StateSnapshot = JSON.parse(content);
|
|
99
|
+
printSnapshotTable(snapshot);
|
|
100
|
+
} else {
|
|
101
|
+
const snapshots = await readEnvironmentSnapshots(environment);
|
|
102
|
+
if (snapshots.size === 0) {
|
|
103
|
+
console.error(formatError({ message: `No snapshots found for environment "${environment}"` }));
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const [lexicon, content] of snapshots) {
|
|
108
|
+
const snapshot: StateSnapshot = JSON.parse(content);
|
|
109
|
+
console.log(`\n${formatBold(`${environment}/${lexicon}`)} — ${Object.keys(snapshot.resources).length} resources — ${snapshot.timestamp}`);
|
|
110
|
+
printSnapshotTable(snapshot);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* chant state diff <environment> [lexicon]
|
|
119
|
+
*/
|
|
120
|
+
export async function runStateDiff(ctx: CommandContext): Promise<number> {
|
|
121
|
+
const { args, plugins, serializers } = ctx;
|
|
122
|
+
const environment = args.extraPositional;
|
|
123
|
+
const lexiconFilter = args.extraPositional2;
|
|
124
|
+
|
|
125
|
+
if (!environment) {
|
|
126
|
+
console.error(formatError({ message: "Environment is required: chant state diff <environment> [lexicon]" }));
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Filter serializers to target lexicon before building
|
|
131
|
+
const targetSerializers = lexiconFilter
|
|
132
|
+
? plugins.filter((p) => p.name === lexiconFilter).map((p) => p.serializer)
|
|
133
|
+
: serializers;
|
|
134
|
+
|
|
135
|
+
// Build to get current digest
|
|
136
|
+
const projectPath = resolve(".");
|
|
137
|
+
const buildResult = await build(projectPath, targetSerializers);
|
|
138
|
+
if (buildResult.errors.length > 0) {
|
|
139
|
+
console.error(formatError({ message: "Build failed — fix errors before diffing" }));
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const currentDigest = computeBuildDigest(buildResult);
|
|
144
|
+
|
|
145
|
+
// Fetch and read previous snapshot
|
|
146
|
+
await fetchState();
|
|
147
|
+
|
|
148
|
+
const lexicons = lexiconFilter
|
|
149
|
+
? [lexiconFilter]
|
|
150
|
+
: Array.from(buildResult.manifest.lexicons);
|
|
151
|
+
|
|
152
|
+
for (const lexicon of lexicons) {
|
|
153
|
+
const content = await readSnapshot(environment, lexicon);
|
|
154
|
+
let previousDigest = undefined;
|
|
155
|
+
if (content) {
|
|
156
|
+
const snapshot: StateSnapshot = JSON.parse(content);
|
|
157
|
+
previousDigest = snapshot.digest;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const diff = diffDigests(currentDigest, previousDigest);
|
|
161
|
+
|
|
162
|
+
console.log(`\n${formatBold(lexicon)}`);
|
|
163
|
+
console.log("RESOURCE".padEnd(20) + "STATUS".padEnd(12) + "TYPE");
|
|
164
|
+
console.log("-".repeat(60));
|
|
165
|
+
|
|
166
|
+
for (const name of diff.added) {
|
|
167
|
+
console.log(name.padEnd(20) + "added".padEnd(12) + (currentDigest.resources[name]?.type ?? ""));
|
|
168
|
+
}
|
|
169
|
+
for (const name of diff.changed) {
|
|
170
|
+
console.log(name.padEnd(20) + "changed".padEnd(12) + (currentDigest.resources[name]?.type ?? ""));
|
|
171
|
+
}
|
|
172
|
+
for (const name of diff.removed) {
|
|
173
|
+
console.log(name.padEnd(20) + "removed".padEnd(12) + (previousDigest?.resources[name]?.type ?? ""));
|
|
174
|
+
}
|
|
175
|
+
for (const name of diff.unchanged) {
|
|
176
|
+
console.log(name.padEnd(20) + "unchanged".padEnd(12) + (currentDigest.resources[name]?.type ?? ""));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* chant state log [environment]
|
|
185
|
+
*/
|
|
186
|
+
export async function runStateLog(ctx: CommandContext): Promise<number> {
|
|
187
|
+
const environment = ctx.args.extraPositional;
|
|
188
|
+
|
|
189
|
+
await fetchState();
|
|
190
|
+
|
|
191
|
+
const entries = await listSnapshots({ environment });
|
|
192
|
+
if (entries.length === 0) {
|
|
193
|
+
console.error(formatError({ message: "No state snapshots found" }));
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const entry of entries) {
|
|
198
|
+
const date = entry.date.split("T")[0];
|
|
199
|
+
console.log(`${entry.commit.slice(0, 7)} ${date} ${entry.message}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Fallback for unknown state subcommands.
|
|
207
|
+
*/
|
|
208
|
+
export async function runStateUnknown(ctx: CommandContext): Promise<number> {
|
|
209
|
+
console.error(formatError({
|
|
210
|
+
message: `Unknown state subcommand: ${ctx.args.extraPositional ?? ctx.args.path}`,
|
|
211
|
+
hint: "Available: chant state snapshot, chant state show, chant state diff, chant state log",
|
|
212
|
+
}));
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function printSnapshotTable(snapshot: StateSnapshot): void {
|
|
217
|
+
console.log("RESOURCE".padEnd(20) + "TYPE".padEnd(28) + "PHYSICAL ID".padEnd(44) + "STATUS");
|
|
218
|
+
console.log("-".repeat(100));
|
|
219
|
+
|
|
220
|
+
for (const [name, meta] of Object.entries(snapshot.resources)) {
|
|
221
|
+
const physicalId = meta.physicalId ?? "";
|
|
222
|
+
const truncId = physicalId.length > 40 ? physicalId.slice(0, 37) + "..." : physicalId;
|
|
223
|
+
console.log(
|
|
224
|
+
name.padEnd(20) +
|
|
225
|
+
meta.type.padEnd(28) +
|
|
226
|
+
truncId.padEnd(44) +
|
|
227
|
+
meta.status
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -10,6 +10,10 @@ function createMockPlugin(overrides?: Partial<LexiconPlugin>): LexiconPlugin {
|
|
|
10
10
|
return {
|
|
11
11
|
name: "mock",
|
|
12
12
|
serializer: { name: "mock", serialize: () => "" } as unknown as Serializer,
|
|
13
|
+
generate: async () => {},
|
|
14
|
+
validate: async () => {},
|
|
15
|
+
coverage: async () => {},
|
|
16
|
+
package: async () => {},
|
|
13
17
|
...overrides,
|
|
14
18
|
};
|
|
15
19
|
}
|
package/src/cli/main.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { runDevGenerate, runDevPublish, runDevOnboard, runDevCheckLexicon, runDe
|
|
|
12
12
|
import { runServeLsp, runServeMcp, runServeUnknown } from "./handlers/serve";
|
|
13
13
|
import { runInit, runInitLexicon } from "./handlers/init";
|
|
14
14
|
import { runList, runImport, runUpdate, runDoctor } from "./handlers/misc";
|
|
15
|
+
import { runStateSnapshot, runStateShow, runStateDiff, runStateLog, runStateUnknown } from "./handlers/state";
|
|
16
|
+
import { runSpellAdd, runSpellRm, runSpellList, runSpellShow, runSpellCast, runSpellDone, runGraph, runSpellUnknown } from "./handlers/spell";
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Parse command line arguments
|
|
@@ -91,6 +93,21 @@ Commands:
|
|
|
91
93
|
list List discovered entities
|
|
92
94
|
import Import external template into TypeScript
|
|
93
95
|
|
|
96
|
+
Spells:
|
|
97
|
+
spell add <name> Create a new spell
|
|
98
|
+
spell rm <name> Remove a spell
|
|
99
|
+
spell list List all spells with status
|
|
100
|
+
spell show <name> Show spell details
|
|
101
|
+
spell cast <name> Generate bootstrap prompt for agent
|
|
102
|
+
spell done <name> <N> Mark task N as done
|
|
103
|
+
graph Show spell dependency graph
|
|
104
|
+
|
|
105
|
+
State:
|
|
106
|
+
state snapshot <env> Query API, save metadata to orphan branch
|
|
107
|
+
state show <env> Show latest state snapshot
|
|
108
|
+
state diff <env> Compare current build against last snapshot
|
|
109
|
+
state log [env] History of state snapshots
|
|
110
|
+
|
|
94
111
|
Lexicon development:
|
|
95
112
|
dev generate Generate lexicon artifacts (+ validate + coverage)
|
|
96
113
|
dev publish Package lexicon for distribution
|
|
@@ -177,11 +194,28 @@ const registry: CommandDef[] = [
|
|
|
177
194
|
{ name: "dev onboard", handler: runDevOnboard },
|
|
178
195
|
{ name: "dev check-lexicon", handler: runDevCheckLexicon },
|
|
179
196
|
|
|
197
|
+
// Spell subcommands
|
|
198
|
+
{ name: "spell add", handler: runSpellAdd },
|
|
199
|
+
{ name: "spell rm", handler: runSpellRm },
|
|
200
|
+
{ name: "spell list", handler: runSpellList },
|
|
201
|
+
{ name: "spell show", handler: runSpellShow },
|
|
202
|
+
{ name: "spell cast", handler: runSpellCast },
|
|
203
|
+
{ name: "spell done", handler: runSpellDone },
|
|
204
|
+
{ name: "graph", handler: runGraph },
|
|
205
|
+
|
|
206
|
+
// State subcommands
|
|
207
|
+
{ name: "state snapshot", requiresPlugins: true, handler: runStateSnapshot },
|
|
208
|
+
{ name: "state show", handler: runStateShow },
|
|
209
|
+
{ name: "state diff", requiresPlugins: true, handler: runStateDiff },
|
|
210
|
+
{ name: "state log", handler: runStateLog },
|
|
211
|
+
|
|
180
212
|
// Serve subcommands
|
|
181
213
|
{ name: "serve lsp", requiresPlugins: true, handler: runServeLsp },
|
|
182
214
|
{ name: "serve mcp", requiresPlugins: true, handler: runServeMcp },
|
|
183
215
|
|
|
184
216
|
// Fallback for unknown subcommands (must come after compound entries)
|
|
217
|
+
{ name: "spell", handler: runSpellUnknown },
|
|
218
|
+
{ name: "state", handler: runStateUnknown },
|
|
185
219
|
{ name: "dev", handler: runDevUnknown },
|
|
186
220
|
{ name: "serve", handler: runServeUnknown },
|
|
187
221
|
];
|
|
@@ -216,9 +250,9 @@ async function main(): Promise<void> {
|
|
|
216
250
|
process.exit(1);
|
|
217
251
|
}
|
|
218
252
|
|
|
219
|
-
// For compound commands (e.g. "
|
|
220
|
-
// so the project path
|
|
221
|
-
const projectPath = match.compound ? (args.extraPositional
|
|
253
|
+
// For compound commands (e.g. "spell cast"), args.path is the subcommand,
|
|
254
|
+
// so always use "." as the project path. For simple commands, use args.path.
|
|
255
|
+
const projectPath = match.compound ? (args.extraPositional || ".") : args.path;
|
|
222
256
|
const plugins = match.def.requiresPlugins
|
|
223
257
|
? await loadPluginsOrExit(projectPath)
|
|
224
258
|
: [];
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type { ResourceDefinition } from "./types";
|
|
2
|
+
import { getContext } from "./resources/context";
|
|
3
|
+
import { readSnapshot, readEnvironmentSnapshots } from "../../state/git";
|
|
4
|
+
import { discoverSpells } from "../../spell/discovery";
|
|
5
|
+
import { generatePrompt } from "../../spell/prompt";
|
|
6
|
+
import { getRuntime } from "../../runtime-adapter";
|
|
7
|
+
|
|
8
|
+
type PluginResourceEntry = { definition: ResourceDefinition; handler: () => Promise<string> };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Core resource definitions (always present regardless of plugins)
|
|
12
|
+
*/
|
|
13
|
+
export const coreResourceDefinitions: ResourceDefinition[] = [
|
|
14
|
+
{
|
|
15
|
+
uri: "chant://context",
|
|
16
|
+
name: "chant Context",
|
|
17
|
+
description: "Lexicon-specific instructions and patterns for chant development",
|
|
18
|
+
mimeType: "text/markdown",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
uri: "chant://examples/list",
|
|
22
|
+
name: "Examples List",
|
|
23
|
+
description: "List of available chant examples",
|
|
24
|
+
mimeType: "application/json",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
uri: "chant://spells",
|
|
28
|
+
name: "Spells",
|
|
29
|
+
description: "List all spells with status, tasks, and lexicon",
|
|
30
|
+
mimeType: "application/json",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
uri: "chant://spell/{name}",
|
|
34
|
+
name: "Spell details",
|
|
35
|
+
description: "Show spell definition and status",
|
|
36
|
+
mimeType: "application/json",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
uri: "chant://spell/{name}/prompt",
|
|
40
|
+
name: "Spell bootstrap prompt",
|
|
41
|
+
description: "Bootstrap prompt for agent consumption",
|
|
42
|
+
mimeType: "text/markdown",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
uri: "chant://state/{environment}",
|
|
46
|
+
name: "State (all lexicons)",
|
|
47
|
+
description: "All lexicon snapshots for an environment",
|
|
48
|
+
mimeType: "application/json",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
uri: "chant://state/{environment}/{lexicon}",
|
|
52
|
+
name: "State (single lexicon)",
|
|
53
|
+
description: "Single lexicon snapshot for an environment",
|
|
54
|
+
mimeType: "application/json",
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build the full resources list merging core + plugin resources
|
|
60
|
+
*/
|
|
61
|
+
export function buildResourcesList(
|
|
62
|
+
pluginResources: Map<string, PluginResourceEntry>,
|
|
63
|
+
): { resources: ResourceDefinition[] } {
|
|
64
|
+
const resources = [...coreResourceDefinitions];
|
|
65
|
+
for (const { definition } of pluginResources.values()) {
|
|
66
|
+
resources.push(definition);
|
|
67
|
+
}
|
|
68
|
+
return { resources };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Collect example resources from plugins whose URI contains "examples/"
|
|
73
|
+
*/
|
|
74
|
+
export function collectExamples(
|
|
75
|
+
pluginResources: Map<string, PluginResourceEntry>,
|
|
76
|
+
): Array<{ name: string; description: string }> {
|
|
77
|
+
const examples: Array<{ name: string; description: string }> = [];
|
|
78
|
+
for (const [uri, { definition }] of pluginResources.entries()) {
|
|
79
|
+
if (uri.includes("/examples/")) {
|
|
80
|
+
const name = uri.replace(/^chant:\/\/[^/]+\/examples\//, "");
|
|
81
|
+
examples.push({ name, description: definition.description });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return examples;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Handle resources/read request — checks plugin resources after core
|
|
89
|
+
*/
|
|
90
|
+
export async function handleResourcesRead(
|
|
91
|
+
params: Record<string, unknown>,
|
|
92
|
+
pluginResources: Map<string, PluginResourceEntry>,
|
|
93
|
+
): Promise<unknown> {
|
|
94
|
+
const uri = params.uri as string;
|
|
95
|
+
|
|
96
|
+
if (uri === "chant://context") {
|
|
97
|
+
return {
|
|
98
|
+
contents: [
|
|
99
|
+
{
|
|
100
|
+
uri,
|
|
101
|
+
mimeType: "text/markdown",
|
|
102
|
+
text: getContext(),
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (uri === "chant://examples/list") {
|
|
109
|
+
return {
|
|
110
|
+
contents: [
|
|
111
|
+
{
|
|
112
|
+
uri,
|
|
113
|
+
mimeType: "application/json",
|
|
114
|
+
text: JSON.stringify(collectExamples(pluginResources)),
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Spell resources
|
|
121
|
+
if (uri === "chant://spells") {
|
|
122
|
+
const { spells } = await discoverSpells();
|
|
123
|
+
const list = Array.from(spells.entries()).map(([name, s]) => ({
|
|
124
|
+
name,
|
|
125
|
+
status: s.status,
|
|
126
|
+
tasks: `${s.definition.tasks.filter((t) => t.done).length}/${s.definition.tasks.length}`,
|
|
127
|
+
lexicon: s.definition.lexicon ?? null,
|
|
128
|
+
overview: s.definition.overview,
|
|
129
|
+
}));
|
|
130
|
+
return {
|
|
131
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(list, null, 2) }],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (uri.startsWith("chant://spell/") && uri.endsWith("/prompt")) {
|
|
136
|
+
const name = uri.replace("chant://spell/", "").replace("/prompt", "");
|
|
137
|
+
const { spells } = await discoverSpells();
|
|
138
|
+
const spell = spells.get(name);
|
|
139
|
+
if (!spell) throw new Error(`Spell "${name}" not found`);
|
|
140
|
+
const rt = getRuntime();
|
|
141
|
+
const gitRootResult = await rt.spawn(["git", "rev-parse", "--show-toplevel"]);
|
|
142
|
+
const gitRoot = gitRootResult.stdout.trim();
|
|
143
|
+
const prompt = await generatePrompt(spell.definition, { gitRoot });
|
|
144
|
+
return {
|
|
145
|
+
contents: [{ uri, mimeType: "text/markdown", text: prompt }],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (uri.startsWith("chant://spell/")) {
|
|
150
|
+
const name = uri.replace("chant://spell/", "");
|
|
151
|
+
const { spells } = await discoverSpells();
|
|
152
|
+
const spell = spells.get(name);
|
|
153
|
+
if (!spell) throw new Error(`Spell "${name}" not found`);
|
|
154
|
+
return {
|
|
155
|
+
contents: [{
|
|
156
|
+
uri,
|
|
157
|
+
mimeType: "application/json",
|
|
158
|
+
text: JSON.stringify({
|
|
159
|
+
...spell.definition,
|
|
160
|
+
status: spell.status,
|
|
161
|
+
filePath: spell.filePath,
|
|
162
|
+
}, null, 2),
|
|
163
|
+
}],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// State resources: chant://state/{environment} and chant://state/{environment}/{lexicon}
|
|
168
|
+
if (uri.startsWith("chant://state/")) {
|
|
169
|
+
const parts = uri.replace("chant://state/", "").split("/");
|
|
170
|
+
const environment = parts[0];
|
|
171
|
+
const lexicon = parts[1];
|
|
172
|
+
|
|
173
|
+
if (lexicon) {
|
|
174
|
+
const content = await readSnapshot(environment, lexicon);
|
|
175
|
+
if (!content) throw new Error(`No snapshot found for ${environment}/${lexicon}`);
|
|
176
|
+
return {
|
|
177
|
+
contents: [{ uri, mimeType: "application/json", text: content }],
|
|
178
|
+
};
|
|
179
|
+
} else {
|
|
180
|
+
const snapshots = await readEnvironmentSnapshots(environment);
|
|
181
|
+
const result: Record<string, unknown> = {};
|
|
182
|
+
for (const [lex, content] of snapshots) {
|
|
183
|
+
result[lex] = JSON.parse(content);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(result, null, 2) }],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (uri.startsWith("chant://examples/")) {
|
|
192
|
+
// Look up example in plugin resources
|
|
193
|
+
const name = uri.replace("chant://examples/", "");
|
|
194
|
+
for (const [pluginUri, pluginResource] of pluginResources.entries()) {
|
|
195
|
+
if (pluginUri.endsWith(`/examples/${name}`)) {
|
|
196
|
+
const text = await pluginResource.handler();
|
|
197
|
+
return {
|
|
198
|
+
contents: [
|
|
199
|
+
{
|
|
200
|
+
uri,
|
|
201
|
+
mimeType: pluginResource.definition.mimeType ?? "text/typescript",
|
|
202
|
+
text,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
throw new Error(`Example not found: ${name}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check plugin resources
|
|
212
|
+
const pluginResource = pluginResources.get(uri);
|
|
213
|
+
if (pluginResource) {
|
|
214
|
+
const text = await pluginResource.handler();
|
|
215
|
+
return {
|
|
216
|
+
contents: [
|
|
217
|
+
{
|
|
218
|
+
uri,
|
|
219
|
+
mimeType: pluginResource.definition.mimeType ?? "text/plain",
|
|
220
|
+
text,
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
227
|
+
}
|
|
@@ -10,6 +10,10 @@ function createMockPlugin(overrides?: Partial<LexiconPlugin>): LexiconPlugin {
|
|
|
10
10
|
return {
|
|
11
11
|
name: "mock",
|
|
12
12
|
serializer: { name: "mock", serialize: () => "" } as unknown as Serializer,
|
|
13
|
+
generate: async () => {},
|
|
14
|
+
validate: async () => {},
|
|
15
|
+
coverage: async () => {},
|
|
16
|
+
package: async () => {},
|
|
13
17
|
...overrides,
|
|
14
18
|
};
|
|
15
19
|
}
|
|
@@ -401,11 +405,11 @@ describe("McpServer", () => {
|
|
|
401
405
|
test("passes template name to initTemplates", async () => {
|
|
402
406
|
const plugin = createMockPlugin({
|
|
403
407
|
name: "test-lex",
|
|
404
|
-
initTemplates: (template?: string) => {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
return { src
|
|
408
|
+
initTemplates: (template?: string | undefined) => {
|
|
409
|
+
const src: Record<string, string> = template === "special"
|
|
410
|
+
? { "special.ts": "export const special = {};" }
|
|
411
|
+
: { "default.ts": "export const def = {};" };
|
|
412
|
+
return { src };
|
|
409
413
|
},
|
|
410
414
|
});
|
|
411
415
|
|
|
@@ -958,19 +962,19 @@ describe("McpServer", () => {
|
|
|
958
962
|
|
|
959
963
|
const toolsRes = await s.handleRequest({ jsonrpc: "2.0", id: 2, method: "tools/list" });
|
|
960
964
|
const tools = (toolsRes.result as { tools: Array<{ name: string }> }).tools;
|
|
961
|
-
expect(tools).toHaveLength(
|
|
962
|
-
expect(tools.map((t) => t.name).sort()).toEqual(["build", "explain", "import", "lint", "scaffold", "search"]);
|
|
965
|
+
expect(tools).toHaveLength(9);
|
|
966
|
+
expect(tools.map((t) => t.name).sort()).toEqual(["build", "explain", "import", "lint", "scaffold", "search", "spell-done", "state-diff", "state-snapshot"]);
|
|
963
967
|
|
|
964
968
|
const resourcesRes = await s.handleRequest({ jsonrpc: "2.0", id: 3, method: "resources/list" });
|
|
965
969
|
const resources = (resourcesRes.result as { resources: Array<{ uri: string }> }).resources;
|
|
966
|
-
expect(resources).toHaveLength(
|
|
970
|
+
expect(resources).toHaveLength(7);
|
|
967
971
|
});
|
|
968
972
|
|
|
969
973
|
test("server with empty plugins array works", async () => {
|
|
970
974
|
const s = new McpServer([]);
|
|
971
975
|
const toolsRes = await s.handleRequest({ jsonrpc: "2.0", id: 1, method: "tools/list" });
|
|
972
976
|
const tools = (toolsRes.result as { tools: Array<{ name: string }> }).tools;
|
|
973
|
-
expect(tools).toHaveLength(
|
|
977
|
+
expect(tools).toHaveLength(9);
|
|
974
978
|
});
|
|
975
979
|
});
|
|
976
980
|
});
|