@intentius/chant 0.0.22 → 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/package.json +1 -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.ts +12 -774
- package/src/cli/conflict-check.test.ts +43 -0
- package/src/cli/main.ts +1 -1
- package/src/cli/mcp/resource-handlers.ts +227 -0
- package/src/cli/mcp/server.ts +20 -409
- package/src/cli/mcp/state-tools.ts +138 -0
- package/src/cli/mcp/types.ts +45 -0
- 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 +75 -0
- package/src/composite.ts +37 -0
- package/src/discovery/collect.test.ts +34 -0
- package/src/discovery/collect.ts +12 -0
- package/src/lexicon-plugin-helpers.ts +130 -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
|
@@ -252,3 +252,46 @@ describe("checkConflicts", () => {
|
|
|
252
252
|
expect(report.conflicts[0].plugins).toEqual(["aws", "gcp", "azure"]);
|
|
253
253
|
});
|
|
254
254
|
});
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// Cross-lexicon skill naming consistency
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
describe("skill naming consistency", () => {
|
|
261
|
+
test("all skill names match chant-{lexicon}(-{topic})* pattern", async () => {
|
|
262
|
+
const { readdirSync, readFileSync } = await import("fs");
|
|
263
|
+
const { join } = await import("path");
|
|
264
|
+
const lexiconsDir = join(import.meta.dir, "../../../../lexicons");
|
|
265
|
+
const lexiconNames = readdirSync(lexiconsDir, { withFileTypes: true })
|
|
266
|
+
.filter((d) => d.isDirectory())
|
|
267
|
+
.map((d) => d.name);
|
|
268
|
+
|
|
269
|
+
const violations: string[] = [];
|
|
270
|
+
const namePattern = /^chant-[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
271
|
+
|
|
272
|
+
for (const lex of lexiconNames) {
|
|
273
|
+
const skillsDir = join(lexiconsDir, lex, "src", "skills");
|
|
274
|
+
let files: string[];
|
|
275
|
+
try {
|
|
276
|
+
files = readdirSync(skillsDir).filter((f: string) => f.endsWith(".md"));
|
|
277
|
+
} catch {
|
|
278
|
+
continue; // no skills dir
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for (const file of files) {
|
|
282
|
+
const skillName = file.replace(/\.md$/, "");
|
|
283
|
+
if (!namePattern.test(skillName)) {
|
|
284
|
+
violations.push(`${lex}: ${file} → name "${skillName}" does not match pattern`);
|
|
285
|
+
}
|
|
286
|
+
// Verify file name starts with chant-{lexicon}
|
|
287
|
+
if (!skillName.startsWith(`chant-${lex}`)) {
|
|
288
|
+
// Allow k8s cross-provider skills (chant-k8s-eks, chant-k8s-aks, chant-k8s-gke)
|
|
289
|
+
// which are valid as they belong to the k8s lexicon
|
|
290
|
+
violations.push(`${lex}: ${file} → name should start with "chant-${lex}"`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
expect(violations).toEqual([]);
|
|
296
|
+
});
|
|
297
|
+
});
|
package/src/cli/main.ts
CHANGED
|
@@ -252,7 +252,7 @@ async function main(): Promise<void> {
|
|
|
252
252
|
|
|
253
253
|
// For compound commands (e.g. "spell cast"), args.path is the subcommand,
|
|
254
254
|
// so always use "." as the project path. For simple commands, use args.path.
|
|
255
|
-
const projectPath = match.compound ? "." : args.path;
|
|
255
|
+
const projectPath = match.compound ? (args.extraPositional || ".") : args.path;
|
|
256
256
|
const plugins = match.def.requiresPlugins
|
|
257
257
|
? await loadPluginsOrExit(projectPath)
|
|
258
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
|
+
}
|