mova-claude-import 0.1.1 → 0.1.3
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/README.md +95 -29
- package/control_surface_exclusions_v0.json +8 -0
- package/dist/anthropic_profile_v0.d.ts +1 -1
- package/dist/anthropic_profile_v0.js +1 -0
- package/dist/cli.js +206 -23
- package/dist/control_apply_v0.d.ts +5 -1
- package/dist/control_apply_v0.js +125 -8
- package/dist/control_check_v0.d.ts +1 -0
- package/dist/control_check_v0.js +194 -23
- package/dist/control_prefill_v0.js +128 -9
- package/dist/control_surface_coverage_v0.d.ts +22 -0
- package/dist/control_surface_coverage_v0.js +128 -0
- package/dist/control_v0.d.ts +149 -0
- package/dist/control_v0.js +360 -0
- package/dist/control_v0_schema.d.ts +6 -0
- package/dist/control_v0_schema.js +19 -0
- package/dist/init_v0.d.ts +6 -1
- package/dist/init_v0.js +41 -1
- package/dist/observability_writer_v0.d.ts +1 -0
- package/dist/observability_writer_v0.js +157 -0
- package/dist/observe_v0.d.ts +8 -0
- package/dist/observe_v0.js +57 -0
- package/dist/presets_v0.d.ts +11 -0
- package/dist/presets_v0.js +52 -0
- package/dist/redaction.js +5 -1
- package/dist/run_import.js +111 -26
- package/docs/CLAUDE_CONTROL_SURFACE_MAP_v0.md +78 -0
- package/docs/OPERATOR_GUIDE_v0.md +11 -0
- package/fixtures/pos/basic/mova/control_v0.json +93 -0
- package/fixtures/pos/claude_code_demo_full/.claude/agents/code-reviewer.md +13 -0
- package/fixtures/pos/claude_code_demo_full/.claude/agents/github-workflow.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/code-quality.md +8 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/docs-sync.md +8 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/onboard.md +8 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/pr-review.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/pr-summary.md +8 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/ticket.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-eval.js +13 -0
- package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-eval.sh +15 -0
- package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-rules.json +21 -0
- package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-rules.schema.json +24 -0
- package/fixtures/pos/claude_code_demo_full/.claude/rules/code-style.md +5 -0
- package/fixtures/pos/claude_code_demo_full/.claude/rules/security.md +5 -0
- package/fixtures/pos/claude_code_demo_full/.claude/settings.json +102 -0
- package/fixtures/pos/claude_code_demo_full/.claude/settings.md +6 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/core-components/SKILL.md +9 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/formik-patterns/SKILL.md +9 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/graphql-schema/SKILL.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/react-ui-patterns/SKILL.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/systematic-debugging/SKILL.md +9 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/testing-patterns/SKILL.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.github/workflows/pr-claude-code-review.yml +14 -0
- package/fixtures/pos/claude_code_demo_full/.github/workflows/scheduled-claude-code-dependency-audit.yml +14 -0
- package/fixtures/pos/claude_code_demo_full/.github/workflows/scheduled-claude-code-docs-sync.yml +14 -0
- package/fixtures/pos/claude_code_demo_full/.github/workflows/scheduled-claude-code-quality.yml +14 -0
- package/fixtures/pos/claude_code_demo_full/.mcp.json +44 -0
- package/fixtures/pos/claude_code_demo_full/CLAUDE.md +28 -0
- package/fixtures/pos/control_basic_project/mova/control_v0.json +93 -0
- package/fixtures/pos/observability_basic/CLAUDE.md +3 -0
- package/{.tmp_test_zip/out1 → fixtures/pos/preset_safe_observable_v0}/.mcp.json +3 -3
- package/fixtures/pos/preset_safe_observable_v0/CLAUDE.md +3 -0
- package/package.json +2 -1
- package/presets/safe_observable_v0/assets/.claude/agents/code-reviewer.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/commands/finish.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/commands/start.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/hooks/skill-eval.js +15 -0
- package/presets/safe_observable_v0/assets/.claude/hooks/skill-eval.sh +17 -0
- package/presets/safe_observable_v0/assets/.claude/hooks/skill-rules.json +26 -0
- package/presets/safe_observable_v0/assets/.claude/rules/code-style.md +6 -0
- package/presets/safe_observable_v0/assets/.claude/rules/security.md +6 -0
- package/presets/safe_observable_v0/assets/.claude/skills/git-workflow/SKILL.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/skills/security-basics/SKILL.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/skills/systematic-debugging/SKILL.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/skills/testing-patterns/SKILL.md +9 -0
- package/presets/safe_observable_v0/control_v0.json +214 -0
- package/schemas/mova.control_v0.schema.json +252 -0
- package/src/anthropic_profile_v0.ts +1 -0
- package/src/cli.ts +194 -23
- package/src/control_apply_v0.ts +131 -8
- package/src/control_check_v0.ts +203 -23
- package/src/control_prefill_v0.ts +136 -8
- package/src/control_surface_coverage_v0.ts +164 -0
- package/src/control_v0.ts +808 -0
- package/src/control_v0_schema.ts +26 -0
- package/src/init_v0.ts +48 -1
- package/src/observability_writer_v0.ts +157 -0
- package/src/observe_v0.ts +64 -0
- package/src/presets_v0.ts +58 -0
- package/src/redaction.ts +6 -1
- package/src/run_import.ts +132 -26
- package/test/control_demo_full_roundtrip.test.js +92 -0
- package/test/control_surface_coverage_v0.test.js +36 -0
- package/test/control_v0_schema_validation.test.js +69 -0
- package/test/init_v0.test.js +9 -0
- package/test/observability_writer_v0.test.js +59 -0
- package/test/preset_safe_observable_v0.test.js +55 -0
- package/test/profile_v0_output.test.js +1 -0
- package/test/scaffold_v0_output.test.js +1 -0
- package/tools/control_surface_coverage_v0.mjs +27 -0
- package/tools/smoke_v0.mjs +33 -0
- package/.tmp_test_control_apply/proj/.claude/agents/example_agent.md +0 -3
- package/.tmp_test_control_apply/proj/.claude/commands/example_command.md +0 -3
- package/.tmp_test_control_apply/proj/.claude/hooks/example_hook.sh +0 -2
- package/.tmp_test_control_apply/proj/.claude/output-styles/example_style.md +0 -3
- package/.tmp_test_control_apply/proj/.claude/settings.json +0 -30
- package/.tmp_test_control_apply/proj/.claude/settings.local.example.json +0 -3
- package/.tmp_test_control_apply/proj/.mcp.json +0 -3
- package/.tmp_test_control_apply/proj/CLAUDE.md +0 -13
- package/.tmp_test_control_apply/proj/MOVA.md +0 -3
- package/.tmp_test_control_check/proj/.mcp.json +0 -1
- package/.tmp_test_control_check/proj/CLAUDE.md +0 -1
- package/.tmp_test_control_prefill/out1/claude_control_profile_v0.json +0 -114
- package/.tmp_test_control_prefill/out1/prefill_report_v0.json +0 -13
- package/.tmp_test_control_prefill/out2/claude_control_profile_v0.json +0 -114
- package/.tmp_test_control_prefill/out2/prefill_report_v0.json +0 -13
- package/.tmp_test_overlay/proj/.claude/skills/a.md +0 -1
- package/.tmp_test_overlay/proj/.mcp.json +0 -1
- package/.tmp_test_overlay/proj/CLAUDE.md +0 -1
- package/.tmp_test_profile/proj/.claude/skills/a.md +0 -1
- package/.tmp_test_profile/proj/.mcp.json +0 -1
- package/.tmp_test_profile/proj/CLAUDE.md +0 -1
- package/.tmp_test_scaffold_apply/proj/.claude/agents/example_agent.md +0 -3
- package/.tmp_test_scaffold_apply/proj/.claude/commands/example_command.md +0 -3
- package/.tmp_test_scaffold_apply/proj/.claude/hooks/example_hook.sh +0 -2
- package/.tmp_test_scaffold_apply/proj/.claude/output-styles/example_style.md +0 -3
- package/.tmp_test_scaffold_apply/proj/.claude/settings.json +0 -30
- package/.tmp_test_scaffold_apply/proj/.claude/settings.local.example.json +0 -3
- package/.tmp_test_scaffold_apply/proj/.mcp.json +0 -3
- package/.tmp_test_scaffold_apply/proj/CLAUDE.md +0 -13
- package/.tmp_test_scaffold_apply/proj/MOVA.md +0 -3
- package/.tmp_test_strict/mova/claude_import/v0/VERSION.json +0 -10
- package/.tmp_test_strict/mova/claude_import/v0/episode_import_run.json +0 -20
- package/.tmp_test_strict/mova/claude_import/v0/import_manifest.json +0 -20
- package/.tmp_test_strict/mova/claude_import/v0/input_policy_report_v0.json +0 -32
- package/.tmp_test_zip/out1/.claude/agents/example_agent.md +0 -3
- package/.tmp_test_zip/out1/.claude/commands/example_command.md +0 -3
- package/.tmp_test_zip/out1/.claude/commands/mova_context.md +0 -4
- package/.tmp_test_zip/out1/.claude/commands/mova_lint.md +0 -4
- package/.tmp_test_zip/out1/.claude/commands/mova_proof.md +0 -6
- package/.tmp_test_zip/out1/.claude/hooks/example_hook.sh +0 -2
- package/.tmp_test_zip/out1/.claude/output-styles/example_style.md +0 -3
- package/.tmp_test_zip/out1/.claude/settings.json +0 -30
- package/.tmp_test_zip/out1/.claude/settings.local.example.json +0 -3
- package/.tmp_test_zip/out1/.claude/skills/a/SKILL.md +0 -1
- package/.tmp_test_zip/out1/.claude/skills/mova-control-v0/SKILL.md +0 -11
- package/.tmp_test_zip/out1/.claude/skills/mova-layer-v0/SKILL.md +0 -8
- package/.tmp_test_zip/out1/CLAUDE.md +0 -4
- package/.tmp_test_zip/out1/MOVA.md +0 -10
- package/.tmp_test_zip/out1/export.zip +0 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/VERSION.json +0 -10
- package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/instruction_profile_v0.json +0 -8
- package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/mcp_servers_v0.json +0 -4
- package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/skills_catalog_v0.json +0 -11
- package/.tmp_test_zip/out1/mova/claude_import/v0/episode_import_run.json +0 -80
- package/.tmp_test_zip/out1/mova/claude_import/v0/export_manifest_v0.json +0 -32
- package/.tmp_test_zip/out1/mova/claude_import/v0/import_manifest.json +0 -33
- package/.tmp_test_zip/out1/mova/claude_import/v0/input_policy_report_v0.json +0 -38
- package/.tmp_test_zip/out1/mova/claude_import/v0/lint_report_v0.json +0 -6
- package/.tmp_test_zip/out1/mova/claude_import/v0/redaction_report.json +0 -4
- package/.tmp_test_zip/out2/.claude/agents/example_agent.md +0 -3
- package/.tmp_test_zip/out2/.claude/commands/example_command.md +0 -3
- package/.tmp_test_zip/out2/.claude/commands/mova_context.md +0 -4
- package/.tmp_test_zip/out2/.claude/commands/mova_lint.md +0 -4
- package/.tmp_test_zip/out2/.claude/commands/mova_proof.md +0 -6
- package/.tmp_test_zip/out2/.claude/hooks/example_hook.sh +0 -2
- package/.tmp_test_zip/out2/.claude/output-styles/example_style.md +0 -3
- package/.tmp_test_zip/out2/.claude/settings.json +0 -30
- package/.tmp_test_zip/out2/.claude/settings.local.example.json +0 -3
- package/.tmp_test_zip/out2/.claude/skills/a/SKILL.md +0 -1
- package/.tmp_test_zip/out2/.claude/skills/mova-control-v0/SKILL.md +0 -11
- package/.tmp_test_zip/out2/.claude/skills/mova-layer-v0/SKILL.md +0 -8
- package/.tmp_test_zip/out2/.mcp.json +0 -3
- package/.tmp_test_zip/out2/CLAUDE.md +0 -4
- package/.tmp_test_zip/out2/MOVA.md +0 -10
- package/.tmp_test_zip/out2/export.zip +0 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/VERSION.json +0 -10
- package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/instruction_profile_v0.json +0 -8
- package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/mcp_servers_v0.json +0 -4
- package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/skills_catalog_v0.json +0 -11
- package/.tmp_test_zip/out2/mova/claude_import/v0/episode_import_run.json +0 -80
- package/.tmp_test_zip/out2/mova/claude_import/v0/export_manifest_v0.json +0 -32
- package/.tmp_test_zip/out2/mova/claude_import/v0/import_manifest.json +0 -33
- package/.tmp_test_zip/out2/mova/claude_import/v0/input_policy_report_v0.json +0 -38
- package/.tmp_test_zip/out2/mova/claude_import/v0/lint_report_v0.json +0 -6
- package/.tmp_test_zip/out2/mova/claude_import/v0/redaction_report.json +0 -4
- package/.tmp_test_zip/proj/.claude/skills/a.md +0 -1
- package/.tmp_test_zip/proj/.mcp.json +0 -1
- package/.tmp_test_zip/proj/CLAUDE.md +0 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import Ajv from "ajv";
|
|
4
|
+
import addFormats from "ajv-formats";
|
|
5
|
+
import { CONTROL_V0_SCHEMA_ID } from "./control_v0.js";
|
|
6
|
+
|
|
7
|
+
type ValidationResult = {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
errors: any[] | null | undefined;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
async function loadControlSchema() {
|
|
13
|
+
const schemaPath = fileURLToPath(new URL("../schemas/mova.control_v0.schema.json", import.meta.url));
|
|
14
|
+
const raw = await fs.readFile(schemaPath, "utf8");
|
|
15
|
+
return JSON.parse(raw);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function validateControlV0Schema(control: any): Promise<ValidationResult> {
|
|
19
|
+
const schema = await loadControlSchema();
|
|
20
|
+
const ajv = new (Ajv as any)({ allErrors: true, strict: true, validateSchema: false });
|
|
21
|
+
(addFormats as any)(ajv);
|
|
22
|
+
ajv.addSchema(schema, CONTROL_V0_SCHEMA_ID);
|
|
23
|
+
const validate = ajv.compile(schema);
|
|
24
|
+
const ok = Boolean(validate(control));
|
|
25
|
+
return { ok, errors: validate.errors };
|
|
26
|
+
}
|
package/src/init_v0.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { getAnthropicProfileV0Files } from "./anthropic_profile_v0.js";
|
|
|
4
4
|
import { stableStringify } from "./stable_json.js";
|
|
5
5
|
import { createExportZipV0 } from "./export_zip_v0.js";
|
|
6
6
|
import { writeCleanClaudeProfileScaffoldV0 } from "./claude_profile_scaffold_v0.js";
|
|
7
|
+
import { defaultControlV0, type ControlV0 } from "./control_v0.js";
|
|
7
8
|
|
|
8
9
|
type InitResult = {
|
|
9
10
|
createdFiles: string[];
|
|
@@ -11,6 +12,11 @@ type InitResult = {
|
|
|
11
12
|
zipSha256?: string;
|
|
12
13
|
};
|
|
13
14
|
|
|
15
|
+
type InitOptions = {
|
|
16
|
+
controlOverride?: ControlV0;
|
|
17
|
+
assetsRoot?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
14
20
|
async function writeTextFile(absPath: string, content: string) {
|
|
15
21
|
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
16
22
|
await fs.writeFile(absPath, content, "utf8");
|
|
@@ -21,7 +27,38 @@ async function writeJsonFile(absPath: string, obj: any) {
|
|
|
21
27
|
await fs.writeFile(absPath, stableStringify(obj) + "\n", "utf8");
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
|
|
30
|
+
async function copyPresetAssets(control: ControlV0, assetsRoot: string, outRoot: string): Promise<string[]> {
|
|
31
|
+
const created: string[] = [];
|
|
32
|
+
const assets = [
|
|
33
|
+
...control.assets.skills,
|
|
34
|
+
...control.assets.agents,
|
|
35
|
+
...control.assets.commands,
|
|
36
|
+
...control.assets.rules,
|
|
37
|
+
...control.assets.hooks,
|
|
38
|
+
...control.assets.workflows,
|
|
39
|
+
...control.assets.docs,
|
|
40
|
+
...control.assets.dotfiles,
|
|
41
|
+
...control.assets.schemas,
|
|
42
|
+
];
|
|
43
|
+
for (const asset of assets) {
|
|
44
|
+
const sourceRel = asset.source_path ?? asset.path;
|
|
45
|
+
const source = path.isAbsolute(sourceRel) ? sourceRel : path.join(assetsRoot, sourceRel);
|
|
46
|
+
const target = path.join(outRoot, asset.path);
|
|
47
|
+
try {
|
|
48
|
+
await fs.stat(source);
|
|
49
|
+
} catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
53
|
+
if (source !== target) {
|
|
54
|
+
await fs.copyFile(source, target);
|
|
55
|
+
}
|
|
56
|
+
created.push(asset.path.replace(/\\/g, "/"));
|
|
57
|
+
}
|
|
58
|
+
return created.sort();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function initProfileV0(outRoot: string, emitZip: boolean, options?: InitOptions): Promise<InitResult> {
|
|
25
62
|
const createdFiles: string[] = [];
|
|
26
63
|
await writeCleanClaudeProfileScaffoldV0(outRoot);
|
|
27
64
|
const profileFiles = getAnthropicProfileV0Files();
|
|
@@ -31,6 +68,16 @@ export async function initProfileV0(outRoot: string, emitZip: boolean): Promise<
|
|
|
31
68
|
createdFiles.push(rel);
|
|
32
69
|
}
|
|
33
70
|
|
|
71
|
+
const controlRel = path.join("mova", "control_v0.json").replace(/\\/g, "/");
|
|
72
|
+
const control = options?.controlOverride ?? defaultControlV0();
|
|
73
|
+
await writeJsonFile(path.join(outRoot, controlRel), control);
|
|
74
|
+
createdFiles.push(controlRel);
|
|
75
|
+
|
|
76
|
+
if (options?.assetsRoot) {
|
|
77
|
+
const assetFiles = await copyPresetAssets(control, options.assetsRoot, outRoot);
|
|
78
|
+
createdFiles.push(...assetFiles);
|
|
79
|
+
}
|
|
80
|
+
|
|
34
81
|
const movaBase = path.join(outRoot, "mova", "claude_import", "v0");
|
|
35
82
|
const initManifestRel = path.join("mova", "claude_import", "v0", "init_manifest_v0.json").replace(/\\/g, "/");
|
|
36
83
|
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
export function getMovaObserveScriptV0(): string {
|
|
2
|
+
return [
|
|
3
|
+
"#!/usr/bin/env node",
|
|
4
|
+
"import fs from \"node:fs/promises\";",
|
|
5
|
+
"import path from \"node:path\";",
|
|
6
|
+
"import crypto from \"node:crypto\";",
|
|
7
|
+
"const KEY_RE = /(api[_-]?key|token|secret|password|authorization|bearer)/i;",
|
|
8
|
+
"const INLINE_SECRET_RE = /(sk-[a-zA-Z0-9]{8,})/g;",
|
|
9
|
+
"const PLACEHOLDER_RE = /^\\$\\{[A-Z0-9_]+(?::-?[^}]+)?\\}$/;",
|
|
10
|
+
"const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();",
|
|
11
|
+
"const args = process.argv.slice(2);",
|
|
12
|
+
"const readArg = (name) => {",
|
|
13
|
+
" const idx = args.indexOf(name);",
|
|
14
|
+
" if (idx === -1) return undefined;",
|
|
15
|
+
" return args[idx + 1];",
|
|
16
|
+
"};",
|
|
17
|
+
"const eventType = readArg(\"--event\") || process.env.CLAUDE_HOOK_EVENT || \"post_tool_use\";",
|
|
18
|
+
"const stdoutTailBytes = Number(readArg(\"--stdout-tail-bytes\") || process.env.MOVA_OBS_STDOUT_TAIL_BYTES || 4000);",
|
|
19
|
+
"const stderrTailBytes = Number(readArg(\"--stderr-tail-bytes\") || process.env.MOVA_OBS_STDERR_TAIL_BYTES || 4000);",
|
|
20
|
+
"const maxEventBytes = Number(readArg(\"--max-event-bytes\") || process.env.MOVA_OBS_MAX_EVENT_BYTES || 20000);",
|
|
21
|
+
"const tailLines = Number(readArg(\"--tail-lines\") || process.env.MOVA_OBS_TAIL_LINES || 50);",
|
|
22
|
+
"const outputDir = readArg(\"--output-dir\") || process.env.MOVA_OBS_OUTPUT_DIR || \".mova/episodes\";",
|
|
23
|
+
"const now = new Date();",
|
|
24
|
+
"const iso = now.toISOString();",
|
|
25
|
+
"const env = process.env;",
|
|
26
|
+
"const sessionId = env.CLAUDE_SESSION_ID || env.CLAUDE_RUN_ID;",
|
|
27
|
+
"async function ensureRunId(root) {",
|
|
28
|
+
" if (sessionId) return sessionId;",
|
|
29
|
+
" const currentPath = path.join(root, \".current_run_id\");",
|
|
30
|
+
" try {",
|
|
31
|
+
" const raw = await fs.readFile(currentPath, \"utf8\");",
|
|
32
|
+
" if (raw.trim()) return raw.trim();",
|
|
33
|
+
" } catch {}",
|
|
34
|
+
" const runId = `run_${Date.now()}_${crypto.randomBytes(4).toString(\"hex\")}`;",
|
|
35
|
+
" await fs.mkdir(root, { recursive: true });",
|
|
36
|
+
" await fs.writeFile(currentPath, runId, \"utf8\");",
|
|
37
|
+
" return runId;",
|
|
38
|
+
"}",
|
|
39
|
+
"function tailLinesFn(text, maxLines) {",
|
|
40
|
+
" const lines = text.split(/\\r?\\n/);",
|
|
41
|
+
" if (lines.length <= maxLines) return text;",
|
|
42
|
+
" return lines.slice(-maxLines).join(\"\\n\");",
|
|
43
|
+
"}",
|
|
44
|
+
"function tailBytesFn(text, maxBytes) {",
|
|
45
|
+
" if (!maxBytes || maxBytes <= 0) return \"\";",
|
|
46
|
+
" const buf = Buffer.from(text, \"utf8\");",
|
|
47
|
+
" if (buf.length <= maxBytes) return text;",
|
|
48
|
+
" return buf.slice(buf.length - maxBytes).toString(\"utf8\");",
|
|
49
|
+
"}",
|
|
50
|
+
"function trimText(text, maxBytes, maxLines) {",
|
|
51
|
+
" return tailBytesFn(tailLinesFn(text, maxLines), maxBytes);",
|
|
52
|
+
"}",
|
|
53
|
+
"function redactText(input) {",
|
|
54
|
+
" let out = input;",
|
|
55
|
+
" out = out.replace(INLINE_SECRET_RE, () => \"[REDACTED_TOKEN]\");",
|
|
56
|
+
" out = out.replace(/^([A-Z0-9_]{3,80})\\s*=\\s*(.+)$/gmi, (line, k, v) => {",
|
|
57
|
+
" if (!KEY_RE.test(k)) return line;",
|
|
58
|
+
" if (PLACEHOLDER_RE.test(String(v).trim())) return line;",
|
|
59
|
+
" return `${k}=[REDACTED_VALUE_LEN_${String(v).length}]`;",
|
|
60
|
+
" });",
|
|
61
|
+
" return out;",
|
|
62
|
+
"}",
|
|
63
|
+
"function stableStringify(value) {",
|
|
64
|
+
" if (Array.isArray(value)) return `[${value.map(stableStringify).join(\",\")}]`;",
|
|
65
|
+
" if (value && typeof value === \"object\") {",
|
|
66
|
+
" const keys = Object.keys(value).sort();",
|
|
67
|
+
" const parts = keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`);",
|
|
68
|
+
" return `{${parts.join(\",\")}}`;",
|
|
69
|
+
" }",
|
|
70
|
+
" return JSON.stringify(value);",
|
|
71
|
+
"}",
|
|
72
|
+
"function sha(text) {",
|
|
73
|
+
" return crypto.createHash(\"sha256\").update(text, \"utf8\").digest(\"hex\");",
|
|
74
|
+
"}",
|
|
75
|
+
"async function run() {",
|
|
76
|
+
" const root = path.join(projectDir, outputDir);",
|
|
77
|
+
" const runId = await ensureRunId(root);",
|
|
78
|
+
" const runDir = path.join(root, runId);",
|
|
79
|
+
" await fs.mkdir(runDir, { recursive: true });",
|
|
80
|
+
" const event = {",
|
|
81
|
+
" ts: iso,",
|
|
82
|
+
" event_type: String(eventType),",
|
|
83
|
+
" tool_name: env.CLAUDE_TOOL_NAME || null,",
|
|
84
|
+
" ok: env.CLAUDE_TOOL_STATUS ? env.CLAUDE_TOOL_STATUS === \"0\" : null,",
|
|
85
|
+
" durations: env.CLAUDE_TOOL_DURATION_MS ? { tool_ms: Number(env.CLAUDE_TOOL_DURATION_MS) } : null,",
|
|
86
|
+
" paths: {",
|
|
87
|
+
" input: env.CLAUDE_TOOL_INPUT_FILE_PATH || null,",
|
|
88
|
+
" output: env.CLAUDE_TOOL_OUTPUT_FILE_PATH || null",
|
|
89
|
+
" },",
|
|
90
|
+
" stdout_tail: null,",
|
|
91
|
+
" stderr_tail: null,",
|
|
92
|
+
" mcp: { server_name: env.CLAUDE_MCP_SERVER || null },",
|
|
93
|
+
" hashes: { input_hash: null, output_hash: null }",
|
|
94
|
+
" };",
|
|
95
|
+
" const stdoutRaw = env.CLAUDE_TOOL_STDOUT || env.CLAUDE_TOOL_OUTPUT || \"\";",
|
|
96
|
+
" const stderrRaw = env.CLAUDE_TOOL_STDERR || \"\";",
|
|
97
|
+
" let outputHashPayload = \"\";",
|
|
98
|
+
" if (stdoutRaw) {",
|
|
99
|
+
" const trimmed = trimText(stdoutRaw, stdoutTailBytes, tailLines);",
|
|
100
|
+
" const redacted = redactText(trimmed);",
|
|
101
|
+
" event.stdout_tail = redacted;",
|
|
102
|
+
" outputHashPayload += redacted;",
|
|
103
|
+
" }",
|
|
104
|
+
" if (stderrRaw) {",
|
|
105
|
+
" const trimmed = trimText(stderrRaw, stderrTailBytes, tailLines);",
|
|
106
|
+
" const redacted = redactText(trimmed);",
|
|
107
|
+
" event.stderr_tail = redacted;",
|
|
108
|
+
" outputHashPayload += `\\n${redacted}`;",
|
|
109
|
+
" }",
|
|
110
|
+
" if (outputHashPayload) {",
|
|
111
|
+
" event.hashes.output_hash = sha(outputHashPayload);",
|
|
112
|
+
" }",
|
|
113
|
+
" const inputRaw = env.CLAUDE_TOOL_INPUT || \"\";",
|
|
114
|
+
" if (inputRaw) {",
|
|
115
|
+
" const trimmed = trimText(inputRaw, stdoutTailBytes, tailLines);",
|
|
116
|
+
" const redacted = redactText(trimmed);",
|
|
117
|
+
" event.hashes.input_hash = sha(redacted);",
|
|
118
|
+
" }",
|
|
119
|
+
" let line = stableStringify(event);",
|
|
120
|
+
" if (line.length > maxEventBytes) {",
|
|
121
|
+
" event.stdout_tail = null;",
|
|
122
|
+
" event.stderr_tail = null;",
|
|
123
|
+
" line = stableStringify(event);",
|
|
124
|
+
" }",
|
|
125
|
+
" await fs.appendFile(path.join(runDir, \"events.jsonl\"), line + \"\\n\", \"utf8\");",
|
|
126
|
+
" const summaryPath = path.join(runDir, \"summary.json\");",
|
|
127
|
+
" let summary = {",
|
|
128
|
+
" run_id: runId,",
|
|
129
|
+
" started_at: iso,",
|
|
130
|
+
" last_event_at: iso,",
|
|
131
|
+
" counts: {},",
|
|
132
|
+
" tools: {}",
|
|
133
|
+
" };",
|
|
134
|
+
" try {",
|
|
135
|
+
" const raw = await fs.readFile(summaryPath, \"utf8\");",
|
|
136
|
+
" summary = JSON.parse(raw);",
|
|
137
|
+
" } catch {}",
|
|
138
|
+
" summary.last_event_at = iso;",
|
|
139
|
+
" summary.counts[event.event_type] = (summary.counts[event.event_type] || 0) + 1;",
|
|
140
|
+
" if (event.tool_name) {",
|
|
141
|
+
" summary.tools[event.tool_name] = (summary.tools[event.tool_name] || 0) + 1;",
|
|
142
|
+
" }",
|
|
143
|
+
" await fs.writeFile(summaryPath, stableStringify(summary) + \"\\n\", \"utf8\");",
|
|
144
|
+
" await fs.appendFile(path.join(root, \"index.jsonl\"), stableStringify({ ts: iso, run_id: runId, event_type: event.event_type }) + \"\\n\", \"utf8\");",
|
|
145
|
+
" if (String(event.event_type).toLowerCase() === \"stop\") {",
|
|
146
|
+
" const currentPath = path.join(root, \".current_run_id\");",
|
|
147
|
+
" try { await fs.rm(currentPath); } catch {}",
|
|
148
|
+
" }",
|
|
149
|
+
"}",
|
|
150
|
+
"run().catch((err) => {",
|
|
151
|
+
" const msg = String(err?.message || err || \"failed\");",
|
|
152
|
+
" process.stdout.write(JSON.stringify({ feedback: `MOVA observe: ${msg}`, suppressOutput: true }));",
|
|
153
|
+
" process.exit(0);",
|
|
154
|
+
"});",
|
|
155
|
+
"",
|
|
156
|
+
].join("\n");
|
|
157
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
type RunSummary = {
|
|
5
|
+
run_id?: string;
|
|
6
|
+
started_at?: string;
|
|
7
|
+
last_event_at?: string;
|
|
8
|
+
counts?: Record<string, number>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
async function exists(p: string): Promise<boolean> {
|
|
12
|
+
try {
|
|
13
|
+
await fs.stat(p);
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function readJson(p: string) {
|
|
21
|
+
const raw = await fs.readFile(p, "utf8");
|
|
22
|
+
return JSON.parse(raw);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sumCounts(summary: RunSummary | null): number {
|
|
26
|
+
if (!summary?.counts) return 0;
|
|
27
|
+
return Object.values(summary.counts).reduce((acc, value) => acc + Number(value || 0), 0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function listObservabilityRuns(projectDir: string) {
|
|
31
|
+
const root = path.join(projectDir, ".mova", "episodes");
|
|
32
|
+
if (!(await exists(root))) return [];
|
|
33
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
34
|
+
const runs = [];
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (!entry.isDirectory()) continue;
|
|
37
|
+
const runId = entry.name;
|
|
38
|
+
const summaryPath = path.join(root, runId, "summary.json");
|
|
39
|
+
const summary = (await exists(summaryPath)) ? ((await readJson(summaryPath)) as RunSummary) : null;
|
|
40
|
+
const lastEventAt = summary?.last_event_at ?? null;
|
|
41
|
+
runs.push({
|
|
42
|
+
run_id: runId,
|
|
43
|
+
last_event_at: lastEventAt,
|
|
44
|
+
started_at: summary?.started_at ?? null,
|
|
45
|
+
events: sumCounts(summary),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
runs.sort((a, b) => String(b.last_event_at ?? "").localeCompare(String(a.last_event_at ?? "")));
|
|
49
|
+
return runs;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function tailObservabilityEvents(projectDir: string, runId: string, limit = 20) {
|
|
53
|
+
const eventsPath = path.join(projectDir, ".mova", "episodes", runId, "events.jsonl");
|
|
54
|
+
if (!(await exists(eventsPath))) return [];
|
|
55
|
+
const raw = await fs.readFile(eventsPath, "utf8");
|
|
56
|
+
const lines = raw.trim().split(/\r?\n/).filter(Boolean);
|
|
57
|
+
return lines.slice(-limit);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function readObservabilitySummary(projectDir: string, runId: string) {
|
|
61
|
+
const summaryPath = path.join(projectDir, ".mova", "episodes", runId, "summary.json");
|
|
62
|
+
if (!(await exists(summaryPath))) return null;
|
|
63
|
+
return await readJson(summaryPath);
|
|
64
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
type PresetInfo = {
|
|
6
|
+
name: string;
|
|
7
|
+
root: string;
|
|
8
|
+
control_path: string;
|
|
9
|
+
assets_root: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
async function exists(p: string): Promise<boolean> {
|
|
13
|
+
try {
|
|
14
|
+
await fs.stat(p);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getPresetsRoot(): string {
|
|
22
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const pkgRoot = path.resolve(here, "..");
|
|
24
|
+
return path.join(pkgRoot, "presets");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function listPresets(): Promise<string[]> {
|
|
28
|
+
const root = getPresetsRoot();
|
|
29
|
+
if (!(await exists(root))) return [];
|
|
30
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
31
|
+
const names: string[] = [];
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (!entry.isDirectory()) continue;
|
|
34
|
+
const controlPath = path.join(root, entry.name, "control_v0.json");
|
|
35
|
+
if (await exists(controlPath)) names.push(entry.name);
|
|
36
|
+
}
|
|
37
|
+
return names.sort();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function resolvePreset(name: string): Promise<PresetInfo | null> {
|
|
41
|
+
const root = getPresetsRoot();
|
|
42
|
+
const presetRoot = path.join(root, name);
|
|
43
|
+
const controlPath = path.join(presetRoot, "control_v0.json");
|
|
44
|
+
const assetsRoot = path.join(presetRoot, "assets");
|
|
45
|
+
if (!(await exists(controlPath))) return null;
|
|
46
|
+
return {
|
|
47
|
+
name,
|
|
48
|
+
root: presetRoot,
|
|
49
|
+
control_path: controlPath,
|
|
50
|
+
assets_root: assetsRoot,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function readPresetControlRaw(name: string): Promise<string | null> {
|
|
55
|
+
const preset = await resolvePreset(name);
|
|
56
|
+
if (!preset) return null;
|
|
57
|
+
return await fs.readFile(preset.control_path, "utf8");
|
|
58
|
+
}
|
package/src/redaction.ts
CHANGED
|
@@ -8,6 +8,11 @@ export type RedactionHit = {
|
|
|
8
8
|
|
|
9
9
|
const KEY_RE = /(api[_-]?key|token|secret|password|authorization|bearer)/i;
|
|
10
10
|
const INLINE_SECRET_RE = /(sk-[a-zA-Z0-9]{8,})/g; // best‑effort
|
|
11
|
+
const PLACEHOLDER_RE = /^\$\{[A-Z0-9_]+(?::-?[^}]+)?\}$/;
|
|
12
|
+
|
|
13
|
+
function isPlaceholderValue(value: string): boolean {
|
|
14
|
+
return PLACEHOLDER_RE.test(value.trim());
|
|
15
|
+
}
|
|
11
16
|
|
|
12
17
|
export function redactText(input: string): { redacted: string; hits: RedactionHit[] } {
|
|
13
18
|
const hits: RedactionHit[] = [];
|
|
@@ -43,7 +48,7 @@ export function redactJson(obj: unknown): { redacted: unknown; hits: RedactionHi
|
|
|
43
48
|
if (typeof x === "object") {
|
|
44
49
|
const out: any = {};
|
|
45
50
|
for (const [k, v] of Object.entries(x)) {
|
|
46
|
-
if (KEY_RE.test(k) && typeof v === "string") {
|
|
51
|
+
if (KEY_RE.test(k) && typeof v === "string" && !isPlaceholderValue(v)) {
|
|
47
52
|
hits.push({ rule_id: "json_secret_field", key: k, len: v.length });
|
|
48
53
|
out[k] = `[REDACTED_VALUE_LEN_${v.length}]`;
|
|
49
54
|
} else {
|
package/src/run_import.ts
CHANGED
|
@@ -11,15 +11,25 @@ import { getAnthropicProfileV0Files } from "./anthropic_profile_v0.js";
|
|
|
11
11
|
import { lintV0, type LintReportV0 } from "./lint_v0.js";
|
|
12
12
|
import { stableStringify } from "./stable_json.js";
|
|
13
13
|
import { createExportZipV0 } from "./export_zip_v0.js";
|
|
14
|
-
import { buildMovaOverlayV0, buildMovaControlEntryV0
|
|
14
|
+
import { buildMovaOverlayV0, buildMovaControlEntryV0 } from "./mova_overlay_v0.js";
|
|
15
15
|
import { scanInputPolicyV0 } from "./input_policy_v0.js";
|
|
16
16
|
import { EvidenceWriter } from "@leryk1981/mova-core-engine";
|
|
17
17
|
import { MOVA_SPEC_BINDINGS_V0 } from "./mova_spec_bindings_v0.js";
|
|
18
18
|
import { writeCleanClaudeProfileScaffoldV0 } from "./claude_profile_scaffold_v0.js";
|
|
19
|
+
import {
|
|
20
|
+
controlFromSettingsV0,
|
|
21
|
+
controlToMcpJson,
|
|
22
|
+
controlToSettingsV0,
|
|
23
|
+
normalizeControlV0,
|
|
24
|
+
type ControlV0,
|
|
25
|
+
} from "./control_v0.js";
|
|
26
|
+
import { getMovaObserveScriptV0 } from "./observability_writer_v0.js";
|
|
19
27
|
|
|
20
28
|
type Found = {
|
|
21
29
|
claudeMdPath?: string;
|
|
22
30
|
mcpJsonPath?: string;
|
|
31
|
+
settingsPath?: string;
|
|
32
|
+
controlPath?: string;
|
|
23
33
|
skillFiles: string[];
|
|
24
34
|
skipped: Array<{ path: string; reason: string }>;
|
|
25
35
|
};
|
|
@@ -33,6 +43,10 @@ async function exists(p: string): Promise<boolean> {
|
|
|
33
43
|
}
|
|
34
44
|
}
|
|
35
45
|
|
|
46
|
+
function isObject(value: any): value is Record<string, any> {
|
|
47
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
async function sha256File(p: string): Promise<string> {
|
|
37
51
|
const buf = await fs.readFile(p);
|
|
38
52
|
return crypto.createHash("sha256").update(buf).digest("hex");
|
|
@@ -68,6 +82,12 @@ async function scanProject(opts: ImportOptions): Promise<Found> {
|
|
|
68
82
|
const mcpJson = path.join(projectDir, ".mcp.json");
|
|
69
83
|
if (await exists(mcpJson)) found.mcpJsonPath = mcpJson;
|
|
70
84
|
|
|
85
|
+
const settingsJson = path.join(projectDir, ".claude", "settings.json");
|
|
86
|
+
if (await exists(settingsJson)) found.settingsPath = settingsJson;
|
|
87
|
+
|
|
88
|
+
const controlJson = path.join(projectDir, "mova", "control_v0.json");
|
|
89
|
+
if (await exists(controlJson)) found.controlPath = controlJson;
|
|
90
|
+
|
|
71
91
|
const skillsRoot = path.join(projectDir, ".claude", "skills");
|
|
72
92
|
if (await exists(skillsRoot)) {
|
|
73
93
|
const stack = [skillsRoot];
|
|
@@ -127,10 +147,15 @@ async function loadAndRedactJson(p: string) {
|
|
|
127
147
|
return { raw, parsed, redacted, hits };
|
|
128
148
|
}
|
|
129
149
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
150
|
+
function updateClaudeBlock(content: string, marker: string, block: string): string {
|
|
151
|
+
if (content.includes(marker)) {
|
|
152
|
+
const idx = content.indexOf(marker);
|
|
153
|
+
const after = content.slice(idx);
|
|
154
|
+
const split = after.split("\n\n");
|
|
155
|
+
split[0] = block.trimEnd();
|
|
156
|
+
return content.slice(0, idx) + split.join("\n\n");
|
|
157
|
+
}
|
|
158
|
+
return `${block}\n${content}`;
|
|
134
159
|
}
|
|
135
160
|
|
|
136
161
|
function orderedObject(obj: any) {
|
|
@@ -177,26 +202,75 @@ export async function runImport(opts: ImportOptions): Promise<ImportResult> {
|
|
|
177
202
|
return redacted;
|
|
178
203
|
}
|
|
179
204
|
|
|
205
|
+
/** Process a JSON file – redact and record */
|
|
206
|
+
async function processJsonFile(rel: string, absPath: string) {
|
|
207
|
+
const { parsed, hits } = await loadAndRedactJson(absPath);
|
|
208
|
+
inputs.push({ rel, sha256: await sha256File(absPath) });
|
|
209
|
+
redactionHits.push(...hits);
|
|
210
|
+
return parsed;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const controlRel = "mova/control_v0.json";
|
|
214
|
+
let controlOutput: ControlV0 | null = null;
|
|
215
|
+
let controlDefaults: string[] = [];
|
|
216
|
+
let controlSource: "control_file" | "migrated" = "migrated";
|
|
217
|
+
let controlMigrationReport: any | null = null;
|
|
218
|
+
|
|
219
|
+
if (found.controlPath) {
|
|
220
|
+
const controlParsed = await processJsonFile(controlRel, found.controlPath);
|
|
221
|
+
const normalized = normalizeControlV0(controlParsed);
|
|
222
|
+
controlOutput = normalized.control;
|
|
223
|
+
controlDefaults = normalized.defaults;
|
|
224
|
+
controlSource = "control_file";
|
|
225
|
+
} else {
|
|
226
|
+
let settingsParsed: any | undefined;
|
|
227
|
+
let mcpParsed: any | undefined;
|
|
228
|
+
let settingsFound = false;
|
|
229
|
+
let mcpFound = false;
|
|
230
|
+
|
|
231
|
+
if (found.settingsPath) {
|
|
232
|
+
settingsParsed = await processJsonFile(".claude/settings.json", found.settingsPath);
|
|
233
|
+
settingsFound = true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (found.mcpJsonPath) {
|
|
237
|
+
mcpParsed = await processJsonFile(".mcp.json", found.mcpJsonPath);
|
|
238
|
+
mcpFound = true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const migrated = controlFromSettingsV0(settingsParsed, mcpParsed);
|
|
242
|
+
controlOutput = migrated.control;
|
|
243
|
+
controlDefaults = migrated.defaults;
|
|
244
|
+
controlSource = "migrated";
|
|
245
|
+
|
|
246
|
+
controlMigrationReport = {
|
|
247
|
+
profile_version: "v0",
|
|
248
|
+
control_version: "control_v0",
|
|
249
|
+
project_dir: projectDir,
|
|
250
|
+
control_path: controlRel,
|
|
251
|
+
found: {
|
|
252
|
+
settings_json: settingsFound,
|
|
253
|
+
mcp_json: mcpFound,
|
|
254
|
+
},
|
|
255
|
+
sources: {
|
|
256
|
+
claude_md: "default",
|
|
257
|
+
overlay: "default",
|
|
258
|
+
policy: settingsFound ? "settings_json" : "default",
|
|
259
|
+
mcp: mcpFound ? "mcp_json" : "default",
|
|
260
|
+
},
|
|
261
|
+
defaults_used: controlDefaults,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const control = controlOutput ?? normalizeControlV0({}).control;
|
|
266
|
+
const mcpJsonParsed = controlToMcpJson(control);
|
|
267
|
+
|
|
180
268
|
// CLAUDE.md
|
|
181
269
|
let claudeMdRedacted = "";
|
|
182
270
|
if (found.claudeMdPath) {
|
|
183
271
|
claudeMdRedacted = await processTextFile("CLAUDE.md", found.claudeMdPath);
|
|
184
272
|
}
|
|
185
273
|
|
|
186
|
-
// .mcp.json
|
|
187
|
-
let mcpJsonRedacted = "";
|
|
188
|
-
let mcpJsonParsed: any | undefined;
|
|
189
|
-
if (found.mcpJsonPath) {
|
|
190
|
-
const { parsed, redacted, hits } = await loadAndRedactJson(found.mcpJsonPath);
|
|
191
|
-
mcpJsonParsed = parsed;
|
|
192
|
-
mcpJsonRedacted = stableStringify(redacted);
|
|
193
|
-
redactionHits.push(...hits);
|
|
194
|
-
inputs.push({
|
|
195
|
-
rel: ".mcp.json",
|
|
196
|
-
sha256: await sha256File(found.mcpJsonPath),
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
274
|
// skill files
|
|
201
275
|
const skillRedactedMap: Record<string, string> = {};
|
|
202
276
|
for (const f of found.skillFiles) {
|
|
@@ -240,6 +314,14 @@ export async function runImport(opts: ImportOptions): Promise<ImportResult> {
|
|
|
240
314
|
};
|
|
241
315
|
await writeEvidenceArtifact(evidenceWriter, movaBase, "VERSION.json", versionInfo);
|
|
242
316
|
await writeEvidenceArtifact(evidenceWriter, movaBase, "input_policy_report_v0.json", inputPolicy);
|
|
317
|
+
if (controlMigrationReport) {
|
|
318
|
+
await writeEvidenceArtifact(
|
|
319
|
+
evidenceWriter,
|
|
320
|
+
movaBase,
|
|
321
|
+
"control_migration_report_v0.json",
|
|
322
|
+
controlMigrationReport
|
|
323
|
+
);
|
|
324
|
+
}
|
|
243
325
|
}
|
|
244
326
|
|
|
245
327
|
if (opts.strict && !inputPolicy.ok) {
|
|
@@ -351,11 +433,16 @@ export async function runImport(opts: ImportOptions): Promise<ImportResult> {
|
|
|
351
433
|
|
|
352
434
|
if (!opts.dryRun && opts.emitProfile) {
|
|
353
435
|
await writeCleanClaudeProfileScaffoldV0(outRoot);
|
|
354
|
-
|
|
436
|
+
const overlayEnabled = opts.emitOverlay && control.overlay.enable;
|
|
437
|
+
if (overlayEnabled) {
|
|
355
438
|
const controlEntry = buildMovaControlEntryV0(overlayParams);
|
|
356
439
|
const claudePath = path.join(outRoot, "CLAUDE.md");
|
|
357
440
|
if (await exists(claudePath)) {
|
|
358
|
-
|
|
441
|
+
if (control.claude_md.inject_control_entry) {
|
|
442
|
+
const raw = await fs.readFile(claudePath, "utf8");
|
|
443
|
+
const updated = updateClaudeBlock(raw, control.claude_md.marker, controlEntry);
|
|
444
|
+
await fs.writeFile(claudePath, updated, "utf8");
|
|
445
|
+
}
|
|
359
446
|
}
|
|
360
447
|
const overlayFiles = buildMovaOverlayV0(overlayParams);
|
|
361
448
|
for (const [rel, content] of Object.entries(overlayFiles)) {
|
|
@@ -367,8 +454,13 @@ export async function runImport(opts: ImportOptions): Promise<ImportResult> {
|
|
|
367
454
|
if (rel === "CLAUDE.md" || rel === ".claude/settings.json") continue;
|
|
368
455
|
await writeTextFile(path.join(outRoot, rel), content);
|
|
369
456
|
}
|
|
370
|
-
|
|
371
|
-
|
|
457
|
+
await writeJsonFile(path.join(outRoot, ".mcp.json"), mcpJsonParsed);
|
|
458
|
+
await writeJsonFile(path.join(outRoot, ".claude", "settings.json"), controlToSettingsV0(control));
|
|
459
|
+
await writeJsonFile(path.join(outRoot, controlRel), control);
|
|
460
|
+
if (control.observability.enable && control.observability.writer?.script_path) {
|
|
461
|
+
const scriptPath = path.join(outRoot, control.observability.writer.script_path);
|
|
462
|
+
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
|
|
463
|
+
await fs.writeFile(scriptPath, getMovaObserveScriptV0(), "utf8");
|
|
372
464
|
}
|
|
373
465
|
for (const skill of normalizedSkills) {
|
|
374
466
|
const outRel = path.join(".claude", "skills", skill.normDir, "SKILL.md");
|
|
@@ -396,9 +488,22 @@ export async function runImport(opts: ImportOptions): Promise<ImportResult> {
|
|
|
396
488
|
})),
|
|
397
489
|
};
|
|
398
490
|
|
|
491
|
+
const mcpServersRaw = isObject(mcpJsonParsed?.mcpServers)
|
|
492
|
+
? mcpJsonParsed?.mcpServers
|
|
493
|
+
: mcpJsonParsed?.servers;
|
|
494
|
+
const mcpServersList = Array.isArray(mcpServersRaw)
|
|
495
|
+
? mcpServersRaw
|
|
496
|
+
: isObject(mcpServersRaw)
|
|
497
|
+
? Object.entries(mcpServersRaw).map(([name, server]) => ({
|
|
498
|
+
name,
|
|
499
|
+
command: typeof server?.command === "string" ? server.command : "unknown",
|
|
500
|
+
args: Array.isArray(server?.args) ? server.args : [],
|
|
501
|
+
env_keys: isObject(server?.env) ? Object.keys(server.env).sort() : [],
|
|
502
|
+
}))
|
|
503
|
+
: [];
|
|
399
504
|
const mcpServers = {
|
|
400
505
|
profile_version: "v0",
|
|
401
|
-
servers:
|
|
506
|
+
servers: mcpServersList,
|
|
402
507
|
};
|
|
403
508
|
|
|
404
509
|
if (!opts.dryRun) {
|
|
@@ -449,6 +554,7 @@ export async function runImport(opts: ImportOptions): Promise<ImportResult> {
|
|
|
449
554
|
},
|
|
450
555
|
};
|
|
451
556
|
|
|
557
|
+
const mcpSourcePresent = controlSource === "control_file" || Boolean(found.mcpJsonPath);
|
|
452
558
|
const manifest = {
|
|
453
559
|
tool: "mova-claude-import",
|
|
454
560
|
version: "v0",
|
|
@@ -458,7 +564,7 @@ export async function runImport(opts: ImportOptions): Promise<ImportResult> {
|
|
|
458
564
|
inputs: inputs.sort((a, b) => a.rel.localeCompare(b.rel)),
|
|
459
565
|
imported: {
|
|
460
566
|
claude_md: Boolean(found.claudeMdPath),
|
|
461
|
-
mcp_json:
|
|
567
|
+
mcp_json: mcpSourcePresent,
|
|
462
568
|
skills_count: normalizedSkills.length,
|
|
463
569
|
},
|
|
464
570
|
skipped: found.skipped,
|
|
@@ -498,7 +604,7 @@ export async function runImport(opts: ImportOptions): Promise<ImportResult> {
|
|
|
498
604
|
lintReport = await lintV0({
|
|
499
605
|
outRoot,
|
|
500
606
|
emitProfile: opts.emitProfile,
|
|
501
|
-
mcpExpected:
|
|
607
|
+
mcpExpected: mcpSourcePresent,
|
|
502
608
|
});
|
|
503
609
|
await writeEvidenceArtifact(evidenceWriter, movaBase, "lint_report_v0.json", lintReport);
|
|
504
610
|
}
|