@noimosai/platform-mcp 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +17 -48
  2. package/bin/noimos-platform-mcp.mjs +4 -5
  3. package/dist/cli/cli.js +567 -0
  4. package/dist/cli/command-help.js +395 -0
  5. package/dist/cli/download.js +72 -0
  6. package/dist/cli/index.js +3 -0
  7. package/dist/cli/input-error.js +48 -0
  8. package/dist/cli/local-agents.js +199 -0
  9. package/dist/cli/mcp-supervisor.js +136 -0
  10. package/dist/cli/noimos-cli.js +72 -0
  11. package/dist/cli/project-archive.js +27 -0
  12. package/dist/cli/project-files.js +25 -0
  13. package/dist/cli/project-sync.js +259 -0
  14. package/dist/cli/setup-lock.js +1 -0
  15. package/dist/cli/setup.js +347 -0
  16. package/dist/cli/skills.js +142 -0
  17. package/dist/cli/team-harness-targets.js +337 -0
  18. package/dist/cli/team-harness.js +1562 -0
  19. package/dist/cli/team-resources.js +222 -0
  20. package/dist/cli/team-skills.js +457 -0
  21. package/dist/cli/update-bootstrap.js +437 -0
  22. package/dist/cli/update-release.js +212 -0
  23. package/dist/cli/version.js +1 -0
  24. package/dist/cli/wallet.js +85 -0
  25. package/dist/client/agent-run.js +1425 -0
  26. package/dist/client/browser.js +905 -0
  27. package/dist/client/distribution.js +2 -0
  28. package/dist/client/evm-wallet.js +214 -0
  29. package/dist/client/execution-context.js +14 -0
  30. package/dist/client/failure-recovery.js +128 -0
  31. package/dist/client/index.js +4 -225
  32. package/dist/client/local-input.js +33 -12
  33. package/dist/client/local-profile-lock.js +52 -0
  34. package/dist/client/local-profile.js +181 -0
  35. package/dist/client/local-projects.js +131 -0
  36. package/dist/client/local-setup.js +93 -0
  37. package/dist/client/realtime-voice.js +184 -0
  38. package/dist/client/tool-name-hints.js +48 -0
  39. package/dist/client/wallet-access.js +214 -0
  40. package/dist/mcp/agent-server.js +345 -0
  41. package/dist/mcp/context.js +86 -0
  42. package/dist/mcp/host-schema.js +76 -0
  43. package/dist/mcp/icon.js +1 -0
  44. package/dist/mcp/instructions.js +26 -0
  45. package/dist/mcp/local-projects.js +42 -0
  46. package/dist/mcp/local-upload.js +7 -13
  47. package/dist/mcp/noimos-runtime.js +18 -0
  48. package/dist/mcp/noimos-server.js +38 -0
  49. package/dist/mcp/operations.js +40 -0
  50. package/dist/mcp/parallel-result.js +17 -0
  51. package/dist/mcp/project-sync.js +105 -0
  52. package/dist/mcp/results-reply.js +15 -0
  53. package/dist/mcp/runtime.js +65 -11
  54. package/dist/mcp/server.js +382 -40
  55. package/dist/mcp/team-skill.js +63 -0
  56. package/dist/mcp/update-notice.js +75 -0
  57. package/dist/mcp/version.js +1 -0
  58. package/dist/schema/accountRequestMethods.js +1 -0
  59. package/dist/schema/boundedBody.js +30 -0
  60. package/dist/schema/cronValidator.js +150 -0
  61. package/dist/schema/platformApiKey.js +14 -0
  62. package/dist/schema/platformAppAuth.js +11 -0
  63. package/dist/schema/platformBillingConstants.js +4 -0
  64. package/dist/schema/platformCapabilityFamilies.js +293 -0
  65. package/dist/schema/platformConnectionCatalog.js +39 -0
  66. package/dist/schema/platformConnectionPolicy.js +33 -0
  67. package/dist/schema/platformCustomTools.js +148 -0
  68. package/dist/schema/platformEndUserApi.js +62 -0
  69. package/dist/schema/platformFunding.js +10 -0
  70. package/dist/schema/platformHttpConstraints.js +12 -0
  71. package/dist/schema/platformLocalSetup.js +25 -0
  72. package/dist/schema/platformMcpTools.js +7 -0
  73. package/dist/schema/platformMetaTools.js +63 -0
  74. package/dist/schema/platformMonitorSchedule.js +123 -0
  75. package/dist/schema/platformMonitors.js +159 -0
  76. package/dist/schema/platformOAuth.js +13 -0
  77. package/dist/schema/platformProjectPermissions.js +33 -0
  78. package/dist/schema/platformPublicExecution.js +202 -21
  79. package/dist/schema/platformRoutes.js +122 -0
  80. package/dist/schema/platformSkills.js +11 -0
  81. package/dist/schema/platformTeamSecretName.js +3 -0
  82. package/dist/schema/platformTeamSkillSlug.js +1 -0
  83. package/dist/schema/platformTeamSkills.js +302 -0
  84. package/dist/schema/platformToolAccess.js +62 -0
  85. package/dist/schema/platformTriggers.js +153 -0
  86. package/dist/schema/platformWorkflows.js +100 -0
  87. package/dist/schema/scheduleTimeZone.js +15 -0
  88. package/dist/schema/toolDiscovery.js +71 -0
  89. package/dist/schema/toolInputRepair.js +122 -0
  90. package/dist/schema/toolResultContext.js +51 -0
  91. package/dist/schema/untrustedContent.js +35 -0
  92. package/npm-shrinkwrap.json +73 -0
  93. package/package.json +5 -3
@@ -0,0 +1,142 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { lstat, open, readdir, rename, unlink } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { z } from "zod";
6
+ import { platformSkillsBundleSchema } from "../client/index.js";
7
+ import { safeDirectory, readPrivateJson, readPrivateText, writePrivateJson } from "../client/local-profile.js";
8
+ const digest = (text) => createHash("sha256").update(text, "utf8").digest("hex");
9
+ const manifestSchema = z.object({ managedBy: z.literal("relvo-cli"), version: z.literal(1), bundleVersion: z.string().regex(/^[a-f0-9]{64}$/),
10
+ skills: z.array(z.object({ name: z.string().regex(/^relvo(?:-[a-z0-9]+)*$/), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict()).max(32) }).strict();
11
+ export function verifiedSkillBundle(input) {
12
+ const bundle = platformSkillsBundleSchema.parse(input);
13
+ const skills = [...bundle.skills].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
14
+ if (new TextEncoder().encode(JSON.stringify(bundle)).byteLength > 1048576 || skills.some((skill) => new TextEncoder().encode(skill.content).byteLength > 65536 || digest(skill.content) !== skill.sha256) ||
15
+ digest(JSON.stringify(skills.map(({ name, description, sha256 }) => ({ name, description, sha256 })))) !== bundle.version)
16
+ throw new Error("Relvo guide integrity verification failed. No local guides were changed.");
17
+ return { ...bundle, skills };
18
+ }
19
+ const journalSchema = z.object({ managedBy: z.literal("relvo-cli"), version: z.literal(1), transactionId: z.uuid(), previous: manifestSchema.nullable(), bundle: platformSkillsBundleSchema,
20
+ preserved: z.array(z.string()).optional() }).strict();
21
+ const journalMaximum = 1114112;
22
+ function nextManifest(previous, bundle, preserved = []) {
23
+ const installed = bundle.skills.filter((skill) => !preserved.includes(skill.name));
24
+ return manifestSchema.parse({ managedBy: "relvo-cli", version: 1, bundleVersion: bundle.version,
25
+ skills: [...(previous?.skills.filter((old) => !installed.some((skill) => skill.name === old.name)) ?? []), ...installed.map(({ name, sha256 }) => ({ name, sha256 }))] });
26
+ }
27
+ const guideFiles = async (folder) => (await readdir(folder)).filter((file) => file !== ".DS_Store");
28
+ const missing = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
29
+ async function readManifest(root) {
30
+ try {
31
+ return manifestSchema.parse(await readPrivateJson(join(root, ".relvo-managed.json")));
32
+ }
33
+ catch (error) {
34
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
35
+ return null;
36
+ throw new Error("Relvo guide manifest is unsafe or invalid. No guides were overwritten.");
37
+ }
38
+ }
39
+ async function finishJournal(root, journal) {
40
+ const bundle = verifiedSkillBundle(journal.bundle), preserved = new Set(journal.preserved), current = await readManifest(root);
41
+ if (![JSON.stringify(journal.previous), JSON.stringify(nextManifest(journal.previous, bundle, journal.preserved))].includes(JSON.stringify(current)))
42
+ throw new Error("Relvo guide manifest changed during an interrupted update. It was preserved.");
43
+ const stageName = `.relvo-stage-${journal.transactionId}`;
44
+ for (const skill of bundle.skills) {
45
+ if (preserved.has(skill.name))
46
+ continue;
47
+ const folder = join(root, skill.name), previous = journal.previous?.skills.find((entry) => entry.name === skill.name);
48
+ try {
49
+ await safeDirectory(folder, true, true);
50
+ const files = await guideFiles(folder);
51
+ if (files.some((file) => !["SKILL.md", stageName].includes(file)))
52
+ throw new Error("Unknown files");
53
+ if (files.includes(stageName)) {
54
+ await readPrivateText(join(folder, stageName));
55
+ await unlink(join(folder, stageName));
56
+ }
57
+ if (files.includes("SKILL.md")) {
58
+ const hash = digest(await readPrivateText(join(folder, "SKILL.md")));
59
+ if (hash !== skill.sha256 && hash !== previous?.sha256)
60
+ throw new Error("Edited guide");
61
+ }
62
+ }
63
+ catch {
64
+ preserved.add(skill.name);
65
+ }
66
+ }
67
+ const installed = bundle.skills.filter((skill) => !preserved.has(skill.name));
68
+ try {
69
+ for (const skill of installed) {
70
+ const stage = join(root, skill.name, stageName), handle = await open(stage, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
71
+ try {
72
+ await handle.writeFile(skill.content);
73
+ await handle.sync();
74
+ }
75
+ finally {
76
+ await handle.close();
77
+ }
78
+ }
79
+ for (const skill of installed) {
80
+ const folder = join(root, skill.name);
81
+ await rename(join(folder, stageName), join(folder, "SKILL.md"));
82
+ const directory = await open(folder, constants.O_RDONLY);
83
+ try {
84
+ await directory.sync();
85
+ }
86
+ finally {
87
+ await directory.close();
88
+ }
89
+ }
90
+ await writePrivateJson(join(root, ".relvo-managed.json"), nextManifest(journal.previous, bundle, [...preserved]), false);
91
+ await unlink(join(root, ".relvo-update.json"));
92
+ }
93
+ catch (error) {
94
+ for (const skill of installed)
95
+ await unlink(join(root, skill.name, stageName)).catch(() => { });
96
+ throw error;
97
+ }
98
+ return [...preserved];
99
+ }
100
+ export async function installSkills(home, clients, input) {
101
+ const bundle = verifiedSkillBundle(input);
102
+ const targets = [];
103
+ for (const client of [...new Set(clients)]) {
104
+ const parent = join(home, client === "claude" ? ".claude" : ".agents"), root = join(parent, "skills");
105
+ await safeDirectory(home);
106
+ await safeDirectory(parent, true);
107
+ await safeDirectory(root, true);
108
+ try {
109
+ await finishJournal(root, journalSchema.parse(await readPrivateJson(join(root, ".relvo-update.json"), journalMaximum)));
110
+ }
111
+ catch (error) {
112
+ if (!missing(error))
113
+ throw error;
114
+ }
115
+ const manifest = await readManifest(root), preserved = [];
116
+ for (const skill of bundle.skills) {
117
+ const folder = join(root, skill.name), existing = manifest?.skills.find((entry) => entry.name === skill.name);
118
+ try {
119
+ const info = await lstat(folder);
120
+ if (!existing || !info.isDirectory() || info.isSymbolicLink())
121
+ throw new Error("Foreign guide");
122
+ await safeDirectory(folder, false, true);
123
+ if (JSON.stringify(await guideFiles(folder)) !== '["SKILL.md"]' || digest(await readPrivateText(join(folder, "SKILL.md"))) !== existing.sha256)
124
+ throw new Error("Edited guide");
125
+ }
126
+ catch (error) {
127
+ if (!missing(error))
128
+ preserved.push(skill.name);
129
+ }
130
+ }
131
+ nextManifest(manifest, bundle, preserved);
132
+ targets.push({ client, root, manifest, preserved });
133
+ }
134
+ const results = [];
135
+ for (const target of targets) {
136
+ const journal = { managedBy: "relvo-cli", version: 1, transactionId: randomUUID(), previous: target.manifest, bundle, preserved: target.preserved };
137
+ await writePrivateJson(join(target.root, ".relvo-update.json"), journal, false, journalMaximum);
138
+ const preserved = await finishJournal(target.root, { ...journal, preserved: [...target.preserved] });
139
+ results.push({ client: target.client, version: bundle.version, count: bundle.skills.length - preserved.length, preserved });
140
+ }
141
+ return results;
142
+ }
@@ -0,0 +1,337 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, open } from "node:fs/promises";
3
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { z } from "zod";
5
+ import { TEAM_HARNESSES, TEAM_HARNESS_NAMES } from "../schema/platformTeamSkills.js";
6
+ import { localProfilePath } from "../client/local-profile.js";
7
+ export { TEAM_HARNESSES };
8
+ export const harnessNames = TEAM_HARNESS_NAMES;
9
+ const under = (value, fallback) => value && isAbsolute(value) ? resolve(value) : fallback;
10
+ export function harnessRoots(home, env = {}, custom = []) {
11
+ const agents = join(home, ".agents"), geminiHome = under(env.GEMINI_CLI_HOME, home);
12
+ const dirs = { claude: join(home, ".claude"), codex: under(env.CODEX_HOME, join(home, ".codex")),
13
+ pi: under(env.PI_CODING_AGENT_DIR, join(home, ".pi", "agent")), dsh: under(env.DSH_HOME, join(home, ".dsh")),
14
+ opencode: under(env.OPENCODE_CONFIG_DIR, join(under(env.XDG_CONFIG_HOME, join(home, ".config")), "opencode")), gemini: join(geminiHome, ".gemini"),
15
+ kimi: under(env.KIMI_CODE_HOME, join(home, ".kimi-code")), qwen: under(env.QWEN_HOME, join(home, ".qwen")), cursor: join(home, ".cursor"),
16
+ copilot: under(env.COPILOT_HOME, join(home, ".copilot")), amp: join(home, ".config", "amp"), droid: join(home, ".factory") };
17
+ return { home, dirs, custom, agents: { claude: agents, codex: agents, pi: agents, dsh: under(env.DSH_AGENTS_HOME, agents), opencode: agents, gemini: join(geminiHome, ".agents"),
18
+ kimi: agents, qwen: agents, cursor: agents, copilot: agents, amp: agents, droid: agents } };
19
+ }
20
+ const customFile = z.object({ harnesses: z.array(z.object({ name: z.string().regex(/^[a-z][a-z0-9-]{1,30}$/), like: z.enum(TEAM_HARNESSES), dir: z.string().min(1).max(500) }).strict())
21
+ .max(20) }).strict();
22
+ export const customHarnessesPath = (home) => join(resolve(localProfilePath(home), ".."), "harnesses.json");
23
+ export async function readCustomHarnesses(home) {
24
+ const path = customHarnessesPath(home);
25
+ let handle;
26
+ try {
27
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
28
+ }
29
+ catch (error) {
30
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
31
+ return [];
32
+ throw new Error(`${path} is a link or cannot be read.`);
33
+ }
34
+ let text;
35
+ try {
36
+ const info = await handle.stat();
37
+ if (!info.isFile() || info.size > 65536 || typeof process.getuid === "function" && info.uid !== process.getuid())
38
+ throw new Error(`${path} is not a plain file of yours.`);
39
+ text = await handle.readFile("utf8");
40
+ }
41
+ finally {
42
+ await handle.close();
43
+ }
44
+ let parsed;
45
+ try {
46
+ parsed = customFile.parse(JSON.parse(text));
47
+ }
48
+ catch {
49
+ throw new Error(`${path} must be {"harnesses": [{"name", "like", "dir"}]} with lowercase names and a listed agent as like.`);
50
+ }
51
+ const builtIn = harnessRoots(home), relvo = resolve(localProfilePath(home), "..");
52
+ const custom = parsed.harnesses.map(entry => ({ ...entry, dir: entry.dir === "~" || entry.dir.startsWith("~/") ? join(home, entry.dir.slice(1)) : resolve(entry.dir) }));
53
+ for (const [index, entry] of custom.entries()) {
54
+ const inside = relative(home, entry.dir);
55
+ if (!inside || inside.startsWith("..") || inside.startsWith(sep) || isAbsolute(inside))
56
+ throw new Error(`${path}: ${entry.name}'s dir must be a folder inside your home folder.`);
57
+ if (entry.dir === relvo || entry.dir.startsWith(`${relvo}${sep}`))
58
+ throw new Error(`${path}: ${entry.name}'s dir must not be inside Relvo's own folder.`);
59
+ if (TEAM_HARNESSES.includes(entry.name) || custom.some((other, at) => at < index && other.name === entry.name))
60
+ throw new Error(`${path}: the name ${entry.name} is taken.`);
61
+ if (Object.values(builtIn.dirs).includes(entry.dir) || custom.some((other, at) => at < index && other.dir === entry.dir))
62
+ throw new Error(`${path}: ${entry.name}'s dir is another agent's folder.`);
63
+ }
64
+ return custom;
65
+ }
66
+ export function harnessView(roots, name) {
67
+ if (TEAM_HARNESSES.includes(name))
68
+ return { name, like: z.enum(TEAM_HARNESSES).parse(name), custom: false, roots };
69
+ const entry = roots.custom.find(item => item.name === name);
70
+ if (!entry)
71
+ throw new Error(`Unknown agent ${name}. Use one of ${[...TEAM_HARNESSES, ...roots.custom.map(item => item.name)].join(", ")}.`);
72
+ return { name, like: entry.like, custom: true, roots: { ...roots, dirs: { ...roots.dirs, [entry.like]: entry.dir } } };
73
+ }
74
+ export const harnessLabel = (view) => view.custom ? view.name : harnessNames[view.like];
75
+ export async function harnessesPresent(roots) {
76
+ const present = [];
77
+ for (const harness of TEAM_HARNESSES) {
78
+ if (harness === "claude" || harness === "codex") {
79
+ present.push(harness);
80
+ continue;
81
+ }
82
+ try {
83
+ if ((await lstat(roots.dirs[harness])).isDirectory())
84
+ present.push(harness);
85
+ }
86
+ catch { }
87
+ }
88
+ return [...present, ...roots.custom.map(entry => entry.name)];
89
+ }
90
+ export const skillRoot = (roots, harness) => harness === "claude" ? join(roots.dirs.claude, "skills") : join(roots.agents[harness], "skills");
91
+ export function ownSkillRoot(roots, harness) {
92
+ if (harness === "claude")
93
+ return join(roots.dirs.claude, "skills");
94
+ return harness === "codex" ? null : join(roots.dirs[harness], "skills");
95
+ }
96
+ const none = (reason) => ({ reason });
97
+ export const HARNESS_AGENT_SETTINGS = {
98
+ codex: { model: "text", model_reasoning_effort: "text", model_reasoning_summary: "text", model_verbosity: "text", personality: "text", service_tier: "text" },
99
+ opencode: { model: "text", variant: "text", steps: "count", hidden: "boolean", color: "text" },
100
+ gemini: { model: "text", display_name: "text", temperature: "number", max_turns: "count", timeout_mins: "count" },
101
+ kimi: { whenToUse: "text" },
102
+ qwen: { model: "text", maxTurns: "count", background: "boolean" },
103
+ copilot: { model: "text", "disable-model-invocation": "boolean", "user-invocable": "boolean" },
104
+ cursor: { model: "text", readonly: "boolean", is_background: "boolean" },
105
+ droid: { model: "text", reasoningEffort: "text" }
106
+ };
107
+ export function agentTarget(roots, harness) {
108
+ const { dirs } = roots, md = (dir, suffix = ".md") => (name) => join(dir, `${name}${suffix}`);
109
+ switch (harness) {
110
+ case "claude": return { format: "claude", path: md(join(dirs.claude, "agents")) };
111
+ case "codex": return { format: "codex", path: md(join(dirs.codex, "agents"), ".toml") };
112
+ case "opencode": return { format: "markdown", path: md(join(dirs.opencode, "agents")), named: false, fields: { mode: "subagent" } };
113
+ case "gemini": return { format: "markdown", path: md(join(dirs.gemini, "agents")), named: true };
114
+ case "kimi": return { format: "markdown", path: md(join(dirs.kimi, "agents")), named: true };
115
+ case "qwen": return { format: "markdown", path: md(join(dirs.qwen, "agents")), named: true };
116
+ case "copilot": return { format: "markdown", path: md(join(dirs.copilot, "agents"), ".agent.md"), named: true };
117
+ case "cursor": return { format: "markdown", path: md(join(dirs.cursor, "agents")), named: true };
118
+ case "droid": return { format: "markdown", path: md(join(dirs.droid, "droids")), named: true };
119
+ case "pi": return none("Pi has no subagents of its own");
120
+ case "dsh": return none("deepseek-harness defines agents as YAML presets of plugins, not as instructions");
121
+ case "amp": return none("Amp adds agents only through plugins");
122
+ }
123
+ }
124
+ export function ruleTarget(roots, harness) {
125
+ const { dirs, agents } = roots, block = (path, imports, options = {}) => ({ format: "block", path, before: options.before ?? [], after: options.after ?? [], emptyShadows: options.emptyShadows ?? true, imports,
126
+ ...options.own ? { own: { path: options.own, imports: options.ownImports ?? imports } } : {} });
127
+ switch (harness) {
128
+ case "claude": return { format: "file", path: name => join(dirs.claude, "rules", `${name}.md`), imports: "outside-code" };
129
+ case "qwen": return { format: "file", path: name => join(dirs.qwen, "rules", `${name}.md`), imports: false };
130
+ case "codex": return block(join(dirs.codex, "AGENTS.md"), false, { before: [join(dirs.codex, "AGENTS.override.md")], emptyShadows: false });
131
+ case "pi": return block(join(dirs.pi, "AGENTS.md"), false, { before: [join(dirs.pi, "AGENTS.override.md")], after: ["AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"].map(name => join(dirs.pi, name)) });
132
+ case "opencode": return block(join(dirs.opencode, "AGENTS.md"), false);
133
+ case "gemini": return block(join(dirs.gemini, "GEMINI.md"), "anywhere");
134
+ case "copilot": return block(join(dirs.copilot, "copilot-instructions.md"), "anywhere");
135
+ case "amp": return block(join(dirs.amp, "AGENTS.md"), "anywhere", { after: [join(roots.home, ".config", "AGENTS.md")] });
136
+ case "dsh": return block(join(agents.dsh, "AGENTS.md"), "anywhere", { own: join(dirs.dsh, "AGENTS.md"), ownImports: false });
137
+ case "kimi": return block(join(agents.kimi, "AGENTS.md"), "anywhere", { after: [join(agents.kimi, "agents.md")], own: join(dirs.kimi, "AGENTS.md"), ownImports: false });
138
+ case "droid": return block(join(agents.droid, "AGENTS.md"), "anywhere", { own: join(dirs.droid, "AGENTS.md"), ownImports: "anywhere" });
139
+ case "cursor": return none("Cursor keeps user rules in its settings, not in a file");
140
+ }
141
+ }
142
+ export function instructionsTarget(roots, harness) {
143
+ if (harness === "claude" || harness === "qwen")
144
+ return { format: "block", path: join(roots.dirs[harness], harness === "claude" ? "CLAUDE.md" : "QWEN.md"),
145
+ imports: harness === "claude" ? "outside-code" : "anywhere", before: [], after: [], emptyShadows: true };
146
+ return ruleTarget(roots, harness);
147
+ }
148
+ const EMAIL_BEFORE = /[A-Za-z0-9._+-]+[@@][A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]*[A-Za-z0-9])+$/;
149
+ function referenceAt(text, at) {
150
+ if (at + 1 >= text.length || /\s/.test(text[at + 1]))
151
+ return false;
152
+ return at === 0 || !/[A-Za-z0-9]/.test(text[at - 1]) || EMAIL_BEFORE.test(text.slice(Math.max(0, at - 320), at));
153
+ }
154
+ const lineStarts = (text) => [0, ...[...text.matchAll(/\n/g)].map(match => match.index + 1)];
155
+ function claudeCode(text) {
156
+ const starts = lineStarts(text), lines = starts.map((start, index) => text.slice(start, index + 1 < starts.length ? starts[index + 1] - 1 : text.length));
157
+ const blank = (line) => /^[ \t]*$/.test(line), fenceLike = /^[ \t>*+\-0-9.)]*(`{3,}|~{3,})/, htmlStart = /^[ \t>*+\-0-9.)]*</;
158
+ const ranges = [], prose = [[]];
159
+ for (let index = 0; index < lines.length; index++) {
160
+ const line = lines[index];
161
+ if (htmlStart.test(line))
162
+ return [];
163
+ if (fenceLike.test(line)) {
164
+ const open = /^(`{3,})[^`]*$|^(~{3,})/.exec(line);
165
+ if (!open || index > 0 && !blank(lines[index - 1]))
166
+ return [];
167
+ const fence = open[1] ?? open[2], close = lines.findIndex((next, at) => at > index && /^ {0,3}(`{3,}|~{3,})/.test(next));
168
+ if (close < 0 || !new RegExp(`^ {0,3}${fence[0]}{${fence.length},} *$`).test(lines[close]))
169
+ return [];
170
+ ranges.push([starts[index], starts[close] + lines[close].length]);
171
+ prose.push([]);
172
+ index = close;
173
+ continue;
174
+ }
175
+ if (blank(line))
176
+ prose.push([]);
177
+ else
178
+ prose[prose.length - 1].push(index);
179
+ }
180
+ for (const paragraph of prose) {
181
+ const body = paragraph.map(index => lines[index]);
182
+ if (body.some(line => /[<\\[~]|:\/\/|www\./.test(line) || /``/.test(line) || (line.match(/`/g)?.length ?? 0) % 2))
183
+ continue;
184
+ for (const index of paragraph)
185
+ for (const span of lines[index].matchAll(/`[^`]+`/g))
186
+ ranges.push([starts[index] + span.index, starts[index] + span.index + span[0].length]);
187
+ }
188
+ return ranges;
189
+ }
190
+ export function escapeImports(text, reading) {
191
+ if (!reading)
192
+ return { text, escaped: false };
193
+ const code = reading === "outside-code" ? claudeCode(text) : [];
194
+ let escaped = false;
195
+ const result = text.replace(/@/g, (match, at) => {
196
+ if (!referenceAt(text, at) || code.some(([start, end]) => at >= start && at < end))
197
+ return match;
198
+ escaped = true;
199
+ return "@";
200
+ });
201
+ return { text: result, escaped };
202
+ }
203
+ export const unescapeImports = (text, original = "") => text.replace(/@/g, (match, at) => referenceAt(text, at) && !original.includes(text.slice(at).split(/\s/u, 1)[0]) ? "@" : match);
204
+ const GEMINI_EVENTS = { PreToolUse: "BeforeTool", PostToolUse: "AfterTool", UserPromptSubmit: "BeforeAgent", Stop: "AfterAgent",
205
+ SessionStart: "SessionStart", SessionEnd: "SessionEnd", PreCompact: "PreCompress", Notification: "Notification" };
206
+ const GEMINI_TOOLS = { Edit: "replace", Bash: "run_shell_command", Read: "read_file", Write: "write_file", Glob: "glob", Grep: "grep", LS: "ls" };
207
+ const DROID_TOOLS = { Bash: "Execute", Write: "Create", WebFetch: "FetchUrl" };
208
+ const renamed = (tools) => (matcher) => Object.entries(tools).reduce((text, [claude, own]) => text.replace(new RegExp(`\\b${claude}\\b`, "g"), own), matcher);
209
+ const set = (...values) => new Set(values);
210
+ const among = (events) => (event) => events.has(event) ? event : null;
211
+ export function hookTarget(roots, harness) {
212
+ const { dirs } = roots, scripts = join(dirs[harness], "relvo-team-hooks");
213
+ switch (harness) {
214
+ case "claude": return { format: "settings", path: join(dirs.claude, "settings.json"), scripts };
215
+ case "codex": return { format: "settings", path: join(dirs.codex, "hooks.json"), scripts, trust: true,
216
+ events: among(set("SessionStart", "SessionEnd", "PreToolUse", "PermissionRequest", "PostToolUse", "PreCompact", "PostCompact", "UserPromptSubmit", "SubagentStart", "SubagentStop", "Stop", "Interrupt")),
217
+ handlerTypes: set("command", "mcp_tool"),
218
+ handlerFields: set("type", "command", "commandWindows", "command_windows", "timeout", "statusMessage", "async", "additionalContextLimit", "server", "tool", "input") };
219
+ case "qwen": return { format: "settings", path: join(dirs.qwen, "settings.json"), scripts,
220
+ events: among(set("PreToolUse", "PostToolUse", "PostToolUseFailure", "UserPromptSubmit", "SessionStart", "SessionEnd", "Stop", "StopFailure", "SubagentStart", "SubagentStop", "PreCompact", "PostCompact", "Notification", "PermissionRequest", "PermissionDenied")),
221
+ handlerTypes: set("command"), handlerFields: set("type", "command", "name", "description", "timeout", "async", "env", "shell", "statusMessage") };
222
+ case "gemini": return { format: "settings", path: join(dirs.gemini, "settings.json"), scripts, events: event => GEMINI_EVENTS[event] ?? null, matcher: renamed(GEMINI_TOOLS),
223
+ handlerTypes: set("command"), handlerFields: set("type", "command", "timeout"), command: command => command.replace(/\$CLAUDE_PROJECT_DIR/g, "$GEMINI_PROJECT_DIR") };
224
+ case "droid": return { format: "settings", path: join(dirs.droid, "settings.json"), topLevel: join(dirs.droid, "hooks.json"), scripts,
225
+ events: among(set("PreToolUse", "PostToolUse", "UserPromptSubmit", "Notification", "Stop", "SubagentStop", "PreCompact", "SessionStart", "SessionEnd")),
226
+ matcher: renamed(DROID_TOOLS), handlerTypes: set("command"), handlerFields: set("type", "command", "timeout"),
227
+ command: command => command.replace(/\$CLAUDE_PROJECT_DIR/g, "$FACTORY_PROJECT_DIR") };
228
+ case "copilot": return { format: "file", path: join(dirs.copilot, "hooks", "relvo-team.json"), scripts,
229
+ events: among(set("PreToolUse", "PostToolUse", "PostToolUseFailure", "UserPromptSubmit", "SessionStart", "SessionEnd", "Stop", "SubagentStop", "PreCompact")),
230
+ handlerTypes: set("command"), handlerFields: set("type", "command", "timeout"), fieldNames: { timeout: "timeoutSec" } };
231
+ case "pi": return { format: "plugin", folder: join(dirs.pi, "extensions"), entries: ["index.ts", "index.js", "package.json"],
232
+ shadows: [{ path: join(dirs.pi, "extensions", "index.ts") }, { path: join(dirs.pi, "extensions", "index.js") },
233
+ { path: join(dirs.pi, "extensions", "package.json"), when: json => z.object({ pi: z.object({ extensions: z.array(z.unknown()).min(1) }) }).safeParse(json).success }] };
234
+ case "opencode": return { format: "plugin", folder: join(dirs.opencode, "plugins"), shadows: [],
235
+ entries: ["package.json", ...["server", "index"].flatMap(name => [".ts", ".tsx", ".js"].map(extension => `${name}${extension}`))] };
236
+ case "amp": return { format: "plugin", folder: join(dirs.amp, "plugins"), entries: ["index.ts", "index.js"], shadows: [] };
237
+ case "cursor": return none("Cursor already runs the hooks sync writes for Claude Code in ~/.claude/settings.json, so a second copy would run each twice");
238
+ case "dsh": return none("deepseek-harness runs Claude Code hooks only through an experimental plugin");
239
+ case "kimi": return none(KIMI_CONFIG);
240
+ }
241
+ }
242
+ const KIMI_CONFIG = "one invalid entry makes Kimi Code drop every hook and permission rule of config.toml, the user's too, and its rewrites can drop the comments sync finds its own entries by";
243
+ const stdio = (server) => ({ command: server.command, args: server.args ?? [], ...(server.env ? { env: server.env } : {}) });
244
+ const headers = (server, variable) => {
245
+ const all = { ...server.headers, ...(server.bearerTokenEnvVar ? { Authorization: `Bearer ${variable(server.bearerTokenEnvVar)}` } : {}) };
246
+ return Object.keys(all).length ? { headers: all } : {};
247
+ };
248
+ const braced = (name) => `\${${name}}`;
249
+ export function mcpTarget(roots, harness) {
250
+ const { dirs } = roots, json = (paths, at, entry) => ({ format: "json", paths, at, entry });
251
+ switch (harness) {
252
+ case "claude":
253
+ case "codex": return { format: "cli" };
254
+ case "pi": return json([join(dirs.pi, "mcp.json")], "mcpServers", (_, server) => "url" in server ? { url: server.url, ...headers(server, braced) } : stdio(server));
255
+ case "opencode": return json([join(dirs.opencode, "opencode.jsonc"), join(dirs.opencode, "opencode.json")], "mcp", (_, server) => "url" in server
256
+ ? { type: "remote", url: server.url, ...headers(server, name => `{env:${name}}`) }
257
+ : { type: "local", command: [server.command, ...server.args ?? []], ...(server.env ? { environment: server.env } : {}) });
258
+ case "gemini":
259
+ case "qwen": return json([join(dirs[harness], "settings.json")], "mcpServers", (_, server) => "url" in server
260
+ ? { httpUrl: server.url, ...headers(server, braced) } : stdio(server));
261
+ case "kimi": return json([join(dirs.kimi, "mcp.json")], "mcpServers", (_, server) => "url" in server
262
+ ? { url: server.url, ...(server.headers ? { headers: server.headers } : {}), ...(server.bearerTokenEnvVar ? { bearerTokenEnvVar: server.bearerTokenEnvVar } : {}) } : stdio(server));
263
+ case "cursor": return json([join(dirs.cursor, "mcp.json")], "mcpServers", (_, server) => "url" in server
264
+ ? { url: server.url, ...headers(server, name => `\${env:${name}}`) } : stdio(server));
265
+ case "copilot": return json([join(dirs.copilot, "mcp-config.json")], "mcpServers", (_, server) => {
266
+ if (!("url" in server))
267
+ return { type: "local", ...stdio(server), tools: ["*"] };
268
+ if (server.bearerTokenEnvVar)
269
+ return "GitHub Copilot CLI does not document reading a token from the environment; add this server yourself";
270
+ return { type: "http", url: server.url, ...(server.headers ? { headers: server.headers } : {}), tools: ["*"] };
271
+ });
272
+ case "amp": return json([join(dirs.amp, "settings.json")], "amp.mcpServers", (_, server) => "url" in server ? { url: server.url, ...headers(server, braced) } : stdio(server));
273
+ case "droid": return json([join(dirs.droid, "mcp.json")], "mcpServers", (_, server) => "url" in server
274
+ ? { type: "http", url: server.url, ...headers(server, braced) } : { type: "stdio", ...stdio(server) });
275
+ case "dsh": return { format: "dsh", path: join(dirs.dsh, "cordis.patch.yml") };
276
+ }
277
+ }
278
+ export function dshMcpRow(name, server) {
279
+ if (name.length > 32)
280
+ return "deepseek-harness takes MCP server names of at most 32 characters";
281
+ if ("url" in server) {
282
+ if (server.bearerTokenEnvVar)
283
+ return "deepseek-harness reads a token from the environment only through code in its configuration; add this server yourself";
284
+ return { id: name, name: "@deepseek-ai/dsh-mcp-client", config: { serverName: name, transport: "streamable-http", url: server.url, headers: server.headers ?? {} } };
285
+ }
286
+ return { id: name, name: "@deepseek-ai/dsh-mcp-client", config: { serverName: name, transport: "stdio", command: server.command, args: server.args ?? [], env: server.env ?? {} } };
287
+ }
288
+ const bashCommand = (rule) => {
289
+ const match = /^Bash\((.*)\)$/s.exec(rule.trim());
290
+ if (!match)
291
+ return null;
292
+ const command = match[1].trim();
293
+ return command.endsWith(":*") || command.endsWith(" *") ? { command: command.slice(0, -2).trim(), prefix: true } : { command, prefix: false };
294
+ };
295
+ function opencodeShell(rule, list) {
296
+ const bash = bashCommand(rule);
297
+ if (!bash || !bash.command || bash.command.includes("?") || list === "allow" && bash.command.includes("*"))
298
+ return null;
299
+ return bash.prefix ? `${bash.command} *` : bash.command;
300
+ }
301
+ function cursorShell(rule, list) {
302
+ const bash = bashCommand(rule);
303
+ if (list === "ask" || !bash || !/^[A-Za-z0-9._/-]+$/.test(bash.command) || list === "allow" && !bash.prefix)
304
+ return null;
305
+ return `Shell(${bash.command})`;
306
+ }
307
+ const QWEN_TOOLS = set("Bash", "Read", "Edit", "Write", "Glob", "Grep", "NotebookEdit", "WebFetch");
308
+ function qwenRule(rule) {
309
+ const match = /^([A-Za-z]+)(?:\((.*)\))?$/s.exec(rule.trim());
310
+ if (!match)
311
+ return /^mcp__[A-Za-z0-9_-]+(?:__[A-Za-z0-9_-]+)?$/.test(rule.trim()) ? rule.trim() : null;
312
+ const [, tool, specifier] = match;
313
+ if (!QWEN_TOOLS.has(tool))
314
+ return null;
315
+ if (specifier === undefined)
316
+ return tool;
317
+ if (tool === "WebFetch")
318
+ return null;
319
+ return tool === "Bash" && specifier.endsWith(":*") ? `Bash(${specifier.slice(0, -2).trimEnd()} *)` : `${tool}(${specifier})`;
320
+ }
321
+ export function permissionTarget(roots, harness) {
322
+ const { dirs } = roots;
323
+ switch (harness) {
324
+ case "claude": return { format: "settings", path: join(dirs.claude, "settings.json"), translate: rule => rule };
325
+ case "qwen": return { format: "settings", path: join(dirs.qwen, "settings.json"), translate: qwenRule };
326
+ case "codex": return { format: "codex-rules", path: join(dirs.codex, "rules", "relvo-team.rules") };
327
+ case "gemini": return { format: "gemini-policy", path: join(dirs.gemini, "policies", "relvo-team.toml") };
328
+ case "pi": return none("Pi has no permission rules; its extensions decide");
329
+ case "dsh": return none("deepseek-harness has sandbox and approval modes, not rules per command");
330
+ case "opencode": return { format: "opencode", paths: [join(dirs.opencode, "opencode.jsonc"), join(dirs.opencode, "opencode.json")], translate: opencodeShell };
331
+ case "cursor": return { format: "settings", path: join(dirs.cursor, "cli-config.json"), translate: cursorShell, existing: true };
332
+ case "droid": return { format: "droid", path: join(dirs.droid, "settings.json") };
333
+ case "kimi": return none(KIMI_CONFIG);
334
+ case "copilot": return none("GitHub Copilot CLI takes permissions only as command-line flags");
335
+ case "amp": return none("Amp has no rules per command, only for MCP servers");
336
+ }
337
+ }