@thinkingsage/kanon 0.8.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/CHANGELOG.md +410 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/bridge/mcp-server.cjs +14171 -0
- package/package.json +98 -0
- package/src/adapters/capabilities.ts +178 -0
- package/src/adapters/claude-code.ts +110 -0
- package/src/adapters/cline.ts +98 -0
- package/src/adapters/codex.ts +173 -0
- package/src/adapters/copilot.ts +106 -0
- package/src/adapters/cursor.ts +97 -0
- package/src/adapters/degradation.ts +95 -0
- package/src/adapters/index.ts +324 -0
- package/src/adapters/kiro-frontmatter.ts +139 -0
- package/src/adapters/kiro-inclusion.ts +86 -0
- package/src/adapters/kiro.ts +412 -0
- package/src/adapters/qdeveloper.ts +115 -0
- package/src/adapters/types.ts +81 -0
- package/src/adapters/windsurf.ts +96 -0
- package/src/admin.ts +283 -0
- package/src/asset-conventions.ts +118 -0
- package/src/attribution-backfill.ts +319 -0
- package/src/attribution-report.ts +95 -0
- package/src/attribution.ts +239 -0
- package/src/backends/github.ts +194 -0
- package/src/backends/http.ts +122 -0
- package/src/backends/index.ts +39 -0
- package/src/backends/local.ts +47 -0
- package/src/backends/s3.ts +157 -0
- package/src/backends/types.ts +59 -0
- package/src/base-cache.ts +270 -0
- package/src/browse-ui.ts +3754 -0
- package/src/browse.ts +1038 -0
- package/src/build.ts +1108 -0
- package/src/catalog.ts +204 -0
- package/src/cli-deprecated.ts +29 -0
- package/src/cli.ts +773 -0
- package/src/collection-admin.ts +287 -0
- package/src/collection-builder.ts +464 -0
- package/src/collections.ts +116 -0
- package/src/compatibility.ts +105 -0
- package/src/config.ts +743 -0
- package/src/eval/rubrics/kiro-progressive-steering.ts +841 -0
- package/src/eval.ts +1169 -0
- package/src/file-writer.ts +61 -0
- package/src/format-registry.ts +141 -0
- package/src/guild/auto-updater.ts +163 -0
- package/src/guild/backend-resolver.ts +49 -0
- package/src/guild/cli.ts +592 -0
- package/src/guild/collection-expander.ts +47 -0
- package/src/guild/global-cache.ts +247 -0
- package/src/guild/hook-generator.ts +100 -0
- package/src/guild/manifest.ts +154 -0
- package/src/guild/path-utils.ts +12 -0
- package/src/guild/sync.ts +622 -0
- package/src/guild/version-resolver.ts +42 -0
- package/src/help/metadata.ts +445 -0
- package/src/help/renderer.ts +265 -0
- package/src/help/typo-suggester.ts +25 -0
- package/src/hooks/expression.ts +493 -0
- package/src/hooks/pipeline.ts +141 -0
- package/src/import.ts +773 -0
- package/src/importers/claude-code.ts +134 -0
- package/src/importers/cline.ts +103 -0
- package/src/importers/codex.ts +140 -0
- package/src/importers/copilot.ts +103 -0
- package/src/importers/cursor.ts +105 -0
- package/src/importers/index.ts +390 -0
- package/src/importers/kiro.ts +110 -0
- package/src/importers/qdeveloper.ts +103 -0
- package/src/importers/types.ts +54 -0
- package/src/importers/windsurf.ts +104 -0
- package/src/install.ts +1005 -0
- package/src/manifest-admin.ts +306 -0
- package/src/mcp-bridge.ts +240 -0
- package/src/mutation/delta.ts +50 -0
- package/src/mutation/history.ts +66 -0
- package/src/mutation/operators.ts +524 -0
- package/src/mutation/runner.ts +332 -0
- package/src/new.ts +106 -0
- package/src/outcomes/collision.ts +127 -0
- package/src/outcomes/normalize.ts +208 -0
- package/src/outcomes/registry.ts +173 -0
- package/src/parser.ts +446 -0
- package/src/provenance-backfill-cli.ts +319 -0
- package/src/provenance-backfill.ts +520 -0
- package/src/publish.ts +354 -0
- package/src/reconcile-orchestrator.ts +502 -0
- package/src/reconcile-report-renderer.ts +176 -0
- package/src/resolve-body.ts +15 -0
- package/src/rosetta/builtins/compatibility-profiles.ts +297 -0
- package/src/rosetta/builtins/contracts.ts +1033 -0
- package/src/rosetta/builtins/pretty-printers/claude-code-native.ts +122 -0
- package/src/rosetta/builtins/pretty-printers/cline-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/codex-native.ts +127 -0
- package/src/rosetta/builtins/pretty-printers/copilot-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/cursor-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/index.ts +81 -0
- package/src/rosetta/builtins/pretty-printers/kiro-native.ts +166 -0
- package/src/rosetta/builtins/pretty-printers/kiro-power.ts +108 -0
- package/src/rosetta/builtins/pretty-printers/kiro-skill.ts +88 -0
- package/src/rosetta/builtins/pretty-printers/qdeveloper-native.ts +51 -0
- package/src/rosetta/builtins/pretty-printers/superpowers.ts +97 -0
- package/src/rosetta/builtins/pretty-printers/windsurf-native.ts +50 -0
- package/src/rosetta/builtins/sources/claude-code-native.ts +348 -0
- package/src/rosetta/builtins/sources/cline-native.ts +176 -0
- package/src/rosetta/builtins/sources/codex-native.ts +343 -0
- package/src/rosetta/builtins/sources/copilot-native.ts +178 -0
- package/src/rosetta/builtins/sources/cursor-native.ts +176 -0
- package/src/rosetta/builtins/sources/index.ts +95 -0
- package/src/rosetta/builtins/sources/kiro-native.ts +462 -0
- package/src/rosetta/builtins/sources/kiro-power.ts +285 -0
- package/src/rosetta/builtins/sources/kiro-skill.ts +230 -0
- package/src/rosetta/builtins/sources/qdeveloper-native.ts +181 -0
- package/src/rosetta/builtins/sources/superpowers.ts +240 -0
- package/src/rosetta/builtins/sources/windsurf-native.ts +176 -0
- package/src/rosetta/builtins/targets/claude-code.ts +181 -0
- package/src/rosetta/builtins/targets/cline.ts +87 -0
- package/src/rosetta/builtins/targets/codex.ts +226 -0
- package/src/rosetta/builtins/targets/copilot.ts +103 -0
- package/src/rosetta/builtins/targets/cursor.ts +87 -0
- package/src/rosetta/builtins/targets/index.ts +60 -0
- package/src/rosetta/builtins/targets/kiro.ts +278 -0
- package/src/rosetta/builtins/targets/qdeveloper.ts +103 -0
- package/src/rosetta/builtins/targets/windsurf.ts +87 -0
- package/src/rosetta/canonical.ts +729 -0
- package/src/rosetta/compatibility.ts +432 -0
- package/src/rosetta/contracts.ts +329 -0
- package/src/rosetta/detector.ts +724 -0
- package/src/rosetta/diagnostics.ts +630 -0
- package/src/rosetta/engine-bootstrap.ts +103 -0
- package/src/rosetta/engine.ts +744 -0
- package/src/rosetta/index.ts +381 -0
- package/src/rosetta/inspection.ts +530 -0
- package/src/rosetta/plan.ts +448 -0
- package/src/rosetta/provenance-digest.ts +369 -0
- package/src/rosetta/reconcile.ts +812 -0
- package/src/rosetta/redaction.ts +467 -0
- package/src/rosetta/registry.ts +712 -0
- package/src/rosetta/renderers.ts +571 -0
- package/src/rosetta/request-guard.ts +335 -0
- package/src/rosetta/resolution.ts +419 -0
- package/src/rosetta/source-accounting.ts +233 -0
- package/src/rosetta/templates.ts +129 -0
- package/src/rosetta-cli.ts +717 -0
- package/src/rosetta-docs-generator.ts +793 -0
- package/src/rosetta-profiles-cli.ts +367 -0
- package/src/schemas.ts +1712 -0
- package/src/spec-coordination.ts +1141 -0
- package/src/temper.ts +747 -0
- package/src/template-bundle-loader.ts +312 -0
- package/src/template-engine.ts +53 -0
- package/src/translation-application-policy.ts +496 -0
- package/src/translation-orchestrator.ts +1013 -0
- package/src/translation-plan-applier.ts +473 -0
- package/src/tutorial.ts +305 -0
- package/src/validate.ts +1093 -0
- package/src/versioning.ts +553 -0
- package/src/wizard.ts +660 -0
- package/src/workspace.ts +237 -0
- package/templates/eval-contexts/claude-code.md.njk +6 -0
- package/templates/eval-contexts/cline.md.njk +6 -0
- package/templates/eval-contexts/copilot.md.njk +6 -0
- package/templates/eval-contexts/cursor.md.njk +6 -0
- package/templates/eval-contexts/kiro.md.njk +10 -0
- package/templates/eval-contexts/qdeveloper.md.njk +6 -0
- package/templates/eval-contexts/windsurf.md.njk +6 -0
- package/templates/harness-adapters/_base/attribution-footer.md.njk +17 -0
- package/templates/harness-adapters/_base/base.md.njk +16 -0
- package/templates/harness-adapters/claude-code/claude.md.njk +1 -0
- package/templates/harness-adapters/claude-code/mcp.json.njk +1 -0
- package/templates/harness-adapters/claude-code/settings.json.njk +1 -0
- package/templates/harness-adapters/claude-code/skill-library-index.md.njk +13 -0
- package/templates/harness-adapters/claude-code/skill.md.njk +19 -0
- package/templates/harness-adapters/cline/hook.sh.njk +4 -0
- package/templates/harness-adapters/cline/mcp.json.njk +1 -0
- package/templates/harness-adapters/cline/rule.md.njk +1 -0
- package/templates/harness-adapters/codex/agents-md.md.njk +6 -0
- package/templates/harness-adapters/codex/agents-pointer.md.njk +16 -0
- package/templates/harness-adapters/codex/skill.md.njk +27 -0
- package/templates/harness-adapters/copilot/agents.md.njk +1 -0
- package/templates/harness-adapters/copilot/instructions.md.njk +1 -0
- package/templates/harness-adapters/copilot/scoped.md.njk +6 -0
- package/templates/harness-adapters/cursor/mcp.json.njk +1 -0
- package/templates/harness-adapters/cursor/rule.md.njk +6 -0
- package/templates/harness-adapters/kiro/hook.json.njk +1 -0
- package/templates/harness-adapters/kiro/mcp.json.njk +1 -0
- package/templates/harness-adapters/kiro/power-steering.md.njk +3 -0
- package/templates/harness-adapters/kiro/power.md.njk +12 -0
- package/templates/harness-adapters/kiro/steering.md.njk +16 -0
- package/templates/harness-adapters/qdeveloper/agent.md.njk +1 -0
- package/templates/harness-adapters/qdeveloper/mcp.json.njk +1 -0
- package/templates/harness-adapters/qdeveloper/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/mcp.json.njk +1 -0
- package/templates/harness-adapters/windsurf/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/workflow.md.njk +1 -0
- package/templates/knowledge/hooks.yaml.njk +4 -0
- package/templates/knowledge/knowledge.md.njk +53 -0
- package/templates/knowledge/mcp-servers.yaml.njk +2 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rosetta Stone — Claude Code Harness-Native Source Translator
|
|
3
|
+
*
|
|
4
|
+
* Translates Claude Code's native format (CLAUDE.md, .claude/settings.json)
|
|
5
|
+
* into a canonical KnowledgeArtifact candidate.
|
|
6
|
+
*
|
|
7
|
+
* CONSTRAINTS:
|
|
8
|
+
* - NO filesystem, process, clock, random, Git, or network imports
|
|
9
|
+
* - Pure function only
|
|
10
|
+
*
|
|
11
|
+
* Requirements: 2.9, 4.1, 4.2, 4.3, 4.5, 4.6
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import matter from "gray-matter";
|
|
15
|
+
import type {
|
|
16
|
+
CanonicalHook,
|
|
17
|
+
McpServerDefinition,
|
|
18
|
+
SourceDocument,
|
|
19
|
+
TranslationDiagnostic,
|
|
20
|
+
} from "../../../schemas";
|
|
21
|
+
import { createDiagnostic } from "../../diagnostics";
|
|
22
|
+
import type {
|
|
23
|
+
SourceTranslationOutput,
|
|
24
|
+
SourceTranslatorContext,
|
|
25
|
+
} from "../../registry";
|
|
26
|
+
import {
|
|
27
|
+
namespacedExtraField,
|
|
28
|
+
normalizeDocumentOrder,
|
|
29
|
+
SourceAccountant,
|
|
30
|
+
} from "../../source-accounting";
|
|
31
|
+
|
|
32
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
33
|
+
// Internal helpers
|
|
34
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
35
|
+
|
|
36
|
+
function isClaudeMd(path: string): boolean {
|
|
37
|
+
return path === "CLAUDE.md" || path.endsWith("/CLAUDE.md");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isSettingsJson(path: string): boolean {
|
|
41
|
+
return path.endsWith("settings.json");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isMcpJson(path: string): boolean {
|
|
45
|
+
return path.endsWith("mcp.json");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse CLAUDE.md — markdown with optional frontmatter.
|
|
50
|
+
*/
|
|
51
|
+
function parseClaudeMd(
|
|
52
|
+
doc: SourceDocument,
|
|
53
|
+
diagnostics: TranslationDiagnostic[],
|
|
54
|
+
): { body: string; frontmatter: Record<string, unknown> } {
|
|
55
|
+
const content =
|
|
56
|
+
typeof doc.content === "string"
|
|
57
|
+
? doc.content
|
|
58
|
+
: new TextDecoder().decode(doc.content);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const parsed = matter(content);
|
|
62
|
+
return { body: parsed.content.trim(), frontmatter: { ...parsed.data } };
|
|
63
|
+
} catch {
|
|
64
|
+
diagnostics.push(
|
|
65
|
+
createDiagnostic("RS_CANONICAL_INVALID_FRONTMATTER", {
|
|
66
|
+
message: `Failed to parse frontmatter in "${doc.path}".`,
|
|
67
|
+
source: { path: doc.path },
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
return { body: content.trim(), frontmatter: {} };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse .claude/settings.json — extract command entries as agent_stop hooks
|
|
76
|
+
* and preserve unmapped settings as extra fields.
|
|
77
|
+
*/
|
|
78
|
+
function parseSettingsDocument(
|
|
79
|
+
doc: SourceDocument,
|
|
80
|
+
formatId: string,
|
|
81
|
+
diagnostics: TranslationDiagnostic[],
|
|
82
|
+
): { hooks: CanonicalHook[]; extraFields: Record<string, unknown> } {
|
|
83
|
+
const content =
|
|
84
|
+
typeof doc.content === "string"
|
|
85
|
+
? doc.content
|
|
86
|
+
: new TextDecoder().decode(doc.content);
|
|
87
|
+
|
|
88
|
+
let data: Record<string, unknown>;
|
|
89
|
+
try {
|
|
90
|
+
data = JSON.parse(content);
|
|
91
|
+
} catch {
|
|
92
|
+
diagnostics.push(
|
|
93
|
+
createDiagnostic("RS_CANONICAL_INVALID_YAML", {
|
|
94
|
+
message: `Settings file "${doc.path}" contains invalid JSON.`,
|
|
95
|
+
source: { path: doc.path },
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
return { hooks: [], extraFields: {} };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const hooks: CanonicalHook[] = [];
|
|
102
|
+
const extraFields: Record<string, unknown> = {};
|
|
103
|
+
const knownFields = new Set(["commands", "permissions"]);
|
|
104
|
+
|
|
105
|
+
// Extract command entries → CanonicalHook with event: "agent_stop"
|
|
106
|
+
if (data.commands && Array.isArray(data.commands)) {
|
|
107
|
+
for (const cmd of data.commands) {
|
|
108
|
+
if (typeof cmd === "string") {
|
|
109
|
+
hooks.push({
|
|
110
|
+
name: `agent-stop-${cmd.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}`,
|
|
111
|
+
event: "agent_stop",
|
|
112
|
+
action: { type: "run_command", command: cmd },
|
|
113
|
+
});
|
|
114
|
+
} else if (
|
|
115
|
+
cmd &&
|
|
116
|
+
typeof cmd === "object" &&
|
|
117
|
+
(cmd as Record<string, unknown>).command
|
|
118
|
+
) {
|
|
119
|
+
const cmdObj = cmd as Record<string, unknown>;
|
|
120
|
+
hooks.push({
|
|
121
|
+
name:
|
|
122
|
+
(cmdObj.name as string) ??
|
|
123
|
+
`agent-stop-${(cmdObj.command as string).replace(/[^a-z0-9]+/gi, "-").toLowerCase()}`,
|
|
124
|
+
description: cmdObj.description as string | undefined,
|
|
125
|
+
event: "agent_stop",
|
|
126
|
+
action: { type: "run_command", command: cmdObj.command as string },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Preserve unmapped fields as namespaced extra data
|
|
133
|
+
for (const [key, value] of Object.entries(data)) {
|
|
134
|
+
if (!knownFields.has(key)) {
|
|
135
|
+
extraFields[namespacedExtraField(formatId, doc.path, key)] = value;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Preserve permissions as extra data too
|
|
140
|
+
if (data.permissions !== undefined) {
|
|
141
|
+
extraFields[namespacedExtraField(formatId, doc.path, "permissions")] =
|
|
142
|
+
data.permissions;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { hooks, extraFields };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Parse .claude/mcp.json — extract MCP server definitions.
|
|
150
|
+
*/
|
|
151
|
+
function parseMcpJsonDocument(
|
|
152
|
+
doc: SourceDocument,
|
|
153
|
+
diagnostics: TranslationDiagnostic[],
|
|
154
|
+
): McpServerDefinition[] {
|
|
155
|
+
const content =
|
|
156
|
+
typeof doc.content === "string"
|
|
157
|
+
? doc.content
|
|
158
|
+
: new TextDecoder().decode(doc.content);
|
|
159
|
+
|
|
160
|
+
let data: unknown;
|
|
161
|
+
try {
|
|
162
|
+
data = JSON.parse(content);
|
|
163
|
+
} catch {
|
|
164
|
+
diagnostics.push(
|
|
165
|
+
createDiagnostic("RS_CANONICAL_INVALID_YAML", {
|
|
166
|
+
message: `MCP file "${doc.path}" contains invalid JSON.`,
|
|
167
|
+
source: { path: doc.path },
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const servers: McpServerDefinition[] = [];
|
|
174
|
+
|
|
175
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
176
|
+
const obj = data as Record<string, unknown>;
|
|
177
|
+
const mcpServers = (obj.mcpServers ?? obj) as Record<string, unknown>;
|
|
178
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
179
|
+
if (!config || typeof config !== "object") continue;
|
|
180
|
+
const cfg = config as Record<string, unknown>;
|
|
181
|
+
if (cfg.url) {
|
|
182
|
+
const transport =
|
|
183
|
+
cfg.type === "http" ? ("http" as const) : ("sse" as const);
|
|
184
|
+
servers.push({
|
|
185
|
+
name,
|
|
186
|
+
transport,
|
|
187
|
+
url: cfg.url as string,
|
|
188
|
+
env: (cfg.env as Record<string, string>) ?? {},
|
|
189
|
+
...(cfg.timeout ? { timeout: cfg.timeout as number } : {}),
|
|
190
|
+
...(cfg.autoApprove
|
|
191
|
+
? { autoApprove: cfg.autoApprove as string[] }
|
|
192
|
+
: {}),
|
|
193
|
+
});
|
|
194
|
+
} else if (cfg.command) {
|
|
195
|
+
servers.push({
|
|
196
|
+
name,
|
|
197
|
+
transport: "stdio" as const,
|
|
198
|
+
command: cfg.command as string,
|
|
199
|
+
args: (cfg.args as string[]) ?? [],
|
|
200
|
+
env: (cfg.env as Record<string, string>) ?? {},
|
|
201
|
+
...(cfg.timeout ? { timeout: cfg.timeout as number } : {}),
|
|
202
|
+
...(cfg.autoApprove
|
|
203
|
+
? { autoApprove: cfg.autoApprove as string[] }
|
|
204
|
+
: {}),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else if (Array.isArray(data)) {
|
|
209
|
+
for (const entry of data) {
|
|
210
|
+
if (!entry || typeof entry !== "object") continue;
|
|
211
|
+
const cfg = entry as Record<string, unknown>;
|
|
212
|
+
if (!cfg.name) continue;
|
|
213
|
+
if (cfg.url) {
|
|
214
|
+
servers.push({
|
|
215
|
+
name: cfg.name as string,
|
|
216
|
+
transport:
|
|
217
|
+
cfg.transport === "http" ? ("http" as const) : ("sse" as const),
|
|
218
|
+
url: cfg.url as string,
|
|
219
|
+
env: (cfg.env as Record<string, string>) ?? {},
|
|
220
|
+
});
|
|
221
|
+
} else if (cfg.command) {
|
|
222
|
+
servers.push({
|
|
223
|
+
name: cfg.name as string,
|
|
224
|
+
transport: "stdio" as const,
|
|
225
|
+
command: cfg.command as string,
|
|
226
|
+
args: (cfg.args as string[]) ?? [],
|
|
227
|
+
env: (cfg.env as Record<string, string>) ?? {},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return servers;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
237
|
+
// Exported Translator
|
|
238
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Claude Code harness-native source translator.
|
|
242
|
+
*
|
|
243
|
+
* Consumes:
|
|
244
|
+
* - CLAUDE.md → body content becomes the artifact body
|
|
245
|
+
* - .claude/settings.json → extracts hooks and relevant settings as extra fields
|
|
246
|
+
* - .claude/mcp.json → extracts MCP server definitions
|
|
247
|
+
*
|
|
248
|
+
* Sets type: "rule" and harnesses: ["claude-code"]
|
|
249
|
+
*/
|
|
250
|
+
export function translateClaudeCodeNative(
|
|
251
|
+
documents: readonly SourceDocument[],
|
|
252
|
+
context: SourceTranslatorContext,
|
|
253
|
+
): SourceTranslationOutput {
|
|
254
|
+
const accountant = new SourceAccountant();
|
|
255
|
+
const diagnostics: TranslationDiagnostic[] = [];
|
|
256
|
+
const sorted = normalizeDocumentOrder(documents);
|
|
257
|
+
|
|
258
|
+
// Determine artifact name from caller context
|
|
259
|
+
const artifactNameHint = context.callerContext.artifactNameHint as
|
|
260
|
+
| string
|
|
261
|
+
| undefined;
|
|
262
|
+
|
|
263
|
+
// Classify documents
|
|
264
|
+
const claudeMdDocs = sorted.filter((d) => isClaudeMd(d.path));
|
|
265
|
+
const settingsDocs = sorted.filter((d) => isSettingsJson(d.path));
|
|
266
|
+
const mcpDocs = sorted.filter((d) => isMcpJson(d.path));
|
|
267
|
+
|
|
268
|
+
// Handle missing primary file
|
|
269
|
+
if (claudeMdDocs.length === 0) {
|
|
270
|
+
diagnostics.push(
|
|
271
|
+
createDiagnostic("RS_CANONICAL_MISSING_KNOWLEDGE_MD", {
|
|
272
|
+
formatId: context.format.id,
|
|
273
|
+
message: "No CLAUDE.md file found in the document set.",
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
return {
|
|
277
|
+
diagnostics,
|
|
278
|
+
consumedPaths: accountant.getConsumedPaths(),
|
|
279
|
+
preservedPaths: accountant.getPreservedPaths(),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Parse primary CLAUDE.md
|
|
284
|
+
const primaryDoc = claudeMdDocs[0];
|
|
285
|
+
const { body, frontmatter } = parseClaudeMd(primaryDoc, diagnostics);
|
|
286
|
+
|
|
287
|
+
accountant.consume(primaryDoc.path);
|
|
288
|
+
accountant.mapField(primaryDoc.path, "content", "body");
|
|
289
|
+
|
|
290
|
+
// Mark additional CLAUDE.md files as preserved
|
|
291
|
+
for (let i = 1; i < claudeMdDocs.length; i++) {
|
|
292
|
+
accountant.preserve(claudeMdDocs[i].path);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Parse settings files
|
|
296
|
+
const allHooks: CanonicalHook[] = [];
|
|
297
|
+
const allExtraFields: Record<string, unknown> = {};
|
|
298
|
+
|
|
299
|
+
for (const settingsDoc of settingsDocs) {
|
|
300
|
+
const { hooks, extraFields } = parseSettingsDocument(
|
|
301
|
+
settingsDoc,
|
|
302
|
+
context.format.id,
|
|
303
|
+
diagnostics,
|
|
304
|
+
);
|
|
305
|
+
allHooks.push(...hooks);
|
|
306
|
+
Object.assign(allExtraFields, extraFields);
|
|
307
|
+
accountant.consume(settingsDoc.path);
|
|
308
|
+
accountant.mapField(settingsDoc.path, "commands", "hooks");
|
|
309
|
+
accountant.mapField(settingsDoc.path, "settings", "extraFields");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Parse MCP server files
|
|
313
|
+
const allMcpServers: McpServerDefinition[] = [];
|
|
314
|
+
for (const mcpDoc of mcpDocs) {
|
|
315
|
+
const servers = parseMcpJsonDocument(mcpDoc, diagnostics);
|
|
316
|
+
allMcpServers.push(...servers);
|
|
317
|
+
accountant.consume(mcpDoc.path);
|
|
318
|
+
accountant.mapField(mcpDoc.path, "mcpServers", "mcpServers");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Derive artifact name
|
|
322
|
+
const name = artifactNameHint ?? "claude";
|
|
323
|
+
|
|
324
|
+
// Build the canonical candidate
|
|
325
|
+
const candidate: Record<string, unknown> = {
|
|
326
|
+
name,
|
|
327
|
+
frontmatter: {
|
|
328
|
+
name,
|
|
329
|
+
...frontmatter,
|
|
330
|
+
type: "rule",
|
|
331
|
+
harnesses: ["claude-code"],
|
|
332
|
+
},
|
|
333
|
+
body,
|
|
334
|
+
hooks: allHooks,
|
|
335
|
+
mcpServers: allMcpServers,
|
|
336
|
+
workflows: [],
|
|
337
|
+
sourcePath: primaryDoc.path,
|
|
338
|
+
extraFields: allExtraFields,
|
|
339
|
+
bodyOverrides: {},
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
candidate,
|
|
344
|
+
diagnostics,
|
|
345
|
+
consumedPaths: accountant.getConsumedPaths(),
|
|
346
|
+
preservedPaths: accountant.getPreservedPaths(),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rosetta Stone — Cline Harness-Native Source Translator
|
|
3
|
+
*
|
|
4
|
+
* Translates Cline's native format (.clinerules or .cline/rules/*.md)
|
|
5
|
+
* into a canonical KnowledgeArtifact candidate.
|
|
6
|
+
*
|
|
7
|
+
* CONSTRAINTS:
|
|
8
|
+
* - NO filesystem, process, clock, random, Git, or network imports
|
|
9
|
+
* - Pure function only
|
|
10
|
+
*
|
|
11
|
+
* Requirements: 2.9, 4.1, 4.2, 4.3, 4.5, 4.6
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import matter from "gray-matter";
|
|
15
|
+
import type { SourceDocument, TranslationDiagnostic } from "../../../schemas";
|
|
16
|
+
import { createDiagnostic } from "../../diagnostics";
|
|
17
|
+
import type {
|
|
18
|
+
SourceTranslationOutput,
|
|
19
|
+
SourceTranslatorContext,
|
|
20
|
+
} from "../../registry";
|
|
21
|
+
import {
|
|
22
|
+
normalizeDocumentOrder,
|
|
23
|
+
SourceAccountant,
|
|
24
|
+
} from "../../source-accounting";
|
|
25
|
+
|
|
26
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
27
|
+
// Internal helpers
|
|
28
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Checks if a document is the root .clinerules file.
|
|
32
|
+
*/
|
|
33
|
+
function isClineRulesRoot(path: string): boolean {
|
|
34
|
+
return path === ".clinerules" || path.endsWith("/.clinerules");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Checks if a document is a Cline rule file under .cline/rules/.
|
|
39
|
+
*/
|
|
40
|
+
function isClineRuleFile(path: string): boolean {
|
|
41
|
+
return (
|
|
42
|
+
(path.includes(".cline/rules/") || path.includes(".cline/rules\\")) &&
|
|
43
|
+
path.endsWith(".md")
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Checks if a document is any recognized Cline file.
|
|
49
|
+
*/
|
|
50
|
+
function isClineFile(path: string): boolean {
|
|
51
|
+
return isClineRulesRoot(path) || isClineRuleFile(path);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Derives a kebab-case artifact name from a document path.
|
|
56
|
+
*/
|
|
57
|
+
function deriveArtifactName(path: string): string {
|
|
58
|
+
const segments = path.split("/");
|
|
59
|
+
const base = segments[segments.length - 1] ?? "";
|
|
60
|
+
const name = base.replace(/\.[^.]+$/, "");
|
|
61
|
+
return name
|
|
62
|
+
.toLowerCase()
|
|
63
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
64
|
+
.replace(/^-|-$/g, "");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
68
|
+
// Exported Translator
|
|
69
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Cline harness-native source translator.
|
|
73
|
+
*
|
|
74
|
+
* Consumes:
|
|
75
|
+
* - .clinerules → body content becomes the artifact body
|
|
76
|
+
* - .cline/rules/*.md → rule file body content
|
|
77
|
+
*
|
|
78
|
+
* Sets type: "rule" and harnesses: ["cline"]
|
|
79
|
+
*/
|
|
80
|
+
export function translateClineNative(
|
|
81
|
+
documents: readonly SourceDocument[],
|
|
82
|
+
context: SourceTranslatorContext,
|
|
83
|
+
): SourceTranslationOutput {
|
|
84
|
+
const accountant = new SourceAccountant();
|
|
85
|
+
const diagnostics: TranslationDiagnostic[] = [];
|
|
86
|
+
const sorted = normalizeDocumentOrder(documents);
|
|
87
|
+
|
|
88
|
+
// Determine artifact name from caller context
|
|
89
|
+
const artifactNameHint = context.callerContext.artifactNameHint as
|
|
90
|
+
| string
|
|
91
|
+
| undefined;
|
|
92
|
+
|
|
93
|
+
// Classify documents — prefer .clinerules, fall back to rule dir files
|
|
94
|
+
const rootRuleDocs = sorted.filter((d) => isClineRulesRoot(d.path));
|
|
95
|
+
const ruleDirDocs = sorted.filter((d) => isClineRuleFile(d.path));
|
|
96
|
+
|
|
97
|
+
const primaryDoc = rootRuleDocs[0] ?? ruleDirDocs[0];
|
|
98
|
+
|
|
99
|
+
// Handle missing primary file
|
|
100
|
+
if (!primaryDoc) {
|
|
101
|
+
diagnostics.push(
|
|
102
|
+
createDiagnostic("RS_CANONICAL_MISSING_KNOWLEDGE_MD", {
|
|
103
|
+
formatId: context.format.id,
|
|
104
|
+
message:
|
|
105
|
+
"No .clinerules or .cline/rules/ file found in the document set.",
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
return {
|
|
109
|
+
diagnostics,
|
|
110
|
+
consumedPaths: accountant.getConsumedPaths(),
|
|
111
|
+
preservedPaths: accountant.getPreservedPaths(),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Parse primary file
|
|
116
|
+
const content =
|
|
117
|
+
typeof primaryDoc.content === "string"
|
|
118
|
+
? primaryDoc.content
|
|
119
|
+
: new TextDecoder().decode(primaryDoc.content);
|
|
120
|
+
|
|
121
|
+
let frontmatterData: Record<string, unknown> = {};
|
|
122
|
+
let body = "";
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const parsed = matter(content);
|
|
126
|
+
frontmatterData = { ...parsed.data };
|
|
127
|
+
body = parsed.content.trim();
|
|
128
|
+
} catch {
|
|
129
|
+
diagnostics.push(
|
|
130
|
+
createDiagnostic("RS_CANONICAL_INVALID_FRONTMATTER", {
|
|
131
|
+
formatId: context.format.id,
|
|
132
|
+
message: `Failed to parse frontmatter in "${primaryDoc.path}".`,
|
|
133
|
+
source: { path: primaryDoc.path },
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
body = content.trim();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
accountant.consume(primaryDoc.path);
|
|
140
|
+
accountant.mapField(primaryDoc.path, "content", "body");
|
|
141
|
+
|
|
142
|
+
// Mark remaining files as preserved
|
|
143
|
+
for (const doc of sorted) {
|
|
144
|
+
if (doc.path !== primaryDoc.path && isClineFile(doc.path)) {
|
|
145
|
+
accountant.preserve(doc.path);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Derive artifact name
|
|
150
|
+
const name = artifactNameHint ?? deriveArtifactName(primaryDoc.path);
|
|
151
|
+
|
|
152
|
+
// Build the canonical candidate
|
|
153
|
+
const candidate: Record<string, unknown> = {
|
|
154
|
+
name,
|
|
155
|
+
frontmatter: {
|
|
156
|
+
name,
|
|
157
|
+
...frontmatterData,
|
|
158
|
+
type: "rule",
|
|
159
|
+
harnesses: ["cline"],
|
|
160
|
+
},
|
|
161
|
+
body,
|
|
162
|
+
hooks: [],
|
|
163
|
+
mcpServers: [],
|
|
164
|
+
workflows: [],
|
|
165
|
+
sourcePath: primaryDoc.path,
|
|
166
|
+
extraFields: {},
|
|
167
|
+
bodyOverrides: {},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
candidate,
|
|
172
|
+
diagnostics,
|
|
173
|
+
consumedPaths: accountant.getConsumedPaths(),
|
|
174
|
+
preservedPaths: accountant.getPreservedPaths(),
|
|
175
|
+
};
|
|
176
|
+
}
|