tunestra 0.1.1 → 0.2.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/README.md CHANGED
@@ -9,8 +9,8 @@
9
9
  > Tune your AI agents to play as one orchestra.
10
10
 
11
11
  > [!WARNING]
12
- > **Early version.** v0.1 supports Claude Code; Codex comes in v0.2. The formats may still
13
- > change before v1.0.
12
+ > **Early version.** tunestra supports Claude Code and Codex. The formats may still change
13
+ > before v1.0.
14
14
 
15
15
  tunestra gives Claude Code, Codex and other AI coding agents **one shared source** of rules,
16
16
  skills, commands and MCP servers, and generates each agent's native configuration from it.
@@ -33,7 +33,7 @@ specific one wins.
33
33
 
34
34
  ```sh
35
35
  npx tunestra init --extends github:<organisation_name>/<presets_repo>/team#v1
36
- npx tunestra setup # generates AGENTS.md, CLAUDE.md, .claude/, .agents/, .mcp.json
36
+ npx tunestra setup # generates AGENTS.md and the configuration of every agent
37
37
  ```
38
38
 
39
39
  `init` creates `tunestra.yaml`:
@@ -41,7 +41,7 @@ npx tunestra setup # generates AGENTS.md, CLAUDE.md, .claude/, .agents/, .mcp
41
41
  ```yaml
42
42
  extends:
43
43
  - github:<organisation_name>/<presets_repo>/team#v1
44
- agents: [claude]
44
+ agents: [claude] # add codex: [claude, codex]
45
45
  ```
46
46
 
47
47
  Commit `tunestra.yaml`, `.tunestra/`, `AGENTS.md` and `CLAUDE.md`. Everything else that
@@ -97,7 +97,7 @@ as a preset.
97
97
  name: my-service # optional, defaults to the directory name
98
98
  extends:
99
99
  - ../presets/team
100
- agents: [claude] # v0.1 supports claude
100
+ agents: [claude, codex] # claude, codex; defaults to [claude]
101
101
  exclude: {}
102
102
  overrides: {}
103
103
  ```
@@ -116,7 +116,7 @@ repository with a relative path, like `../base`. A relative path cannot leave th
116
116
  Git is not needed: repositories are downloaded as archives over HTTPS.
117
117
 
118
118
  For a private repository, set the `TUNESTRA_GIT_TOKEN` environment variable to a token with
119
- read access. Lockfiles and npm sources are planned for v0.3.
119
+ read access. Lockfiles and npm sources are planned for v0.4.
120
120
 
121
121
  ## Inheritance
122
122
 
@@ -153,20 +153,42 @@ are merged by key (the specific value wins); `command`, `args` and `url` are rep
153
153
 
154
154
  All problems are reported at once, in the order of the files, so you can fix them in one pass.
155
155
 
156
- ## What gets generated (Claude Code)
156
+ ## What gets generated
157
+
158
+ For every agent:
157
159
 
158
160
  | Path | Content | Committed |
159
161
  |---|---|---|
160
162
  | `AGENTS.md` | Index: hierarchy, rules, skills, commands, MCP servers | yes |
161
- | `CLAUDE.md` | `@AGENTS.md` | yes |
162
163
  | `.agents/rules/<NN>-<level>-<key>.md` | Rule texts, ordered from specific to general | no |
163
164
  | `.agents/skills/<key>/` | Skills | no |
165
+ | `.tunestra/state/` | What tunestra generated last time | no |
166
+ | `.gitignore` | A block between `# >>> tunestra` and `# <<< tunestra` | yes |
167
+
168
+ ### Claude Code
169
+
170
+ | Path | Content | Committed |
171
+ |---|---|---|
172
+ | `CLAUDE.md` | `@AGENTS.md` | yes |
164
173
  | `.claude/rules/tunestra/<NN>-<level>-<key>.md` | Copies of the rules; Claude Code loads them in every session | no |
165
174
  | `.claude/skills/<key>` | Symlinks to `.agents/skills/<key>` | no |
166
175
  | `.claude/commands/<key>.md` | Commands | no |
167
176
  | `.mcp.json` | MCP servers | no |
168
- | `.tunestra/state/` | What tunestra generated last time | no |
169
- | `.gitignore` | A block between `# >>> tunestra` and `# <<< tunestra` | yes |
177
+
178
+ ### Codex
179
+
180
+ Codex reads `AGENTS.md` and `.agents/skills/` by itself. It does not follow the links in
181
+ `AGENTS.md`, so tunestra gives it the rule texts in its config.
182
+
183
+ | Path | Content | Committed |
184
+ |---|---|---|
185
+ | `.codex/config.toml` | `developer_instructions` with every rule, from specific to general, and `[mcp_servers]` | no |
186
+ | `.agents/skills/<command>/` | Each command as a skill that runs only when you call it: `$release-notes` | no |
187
+
188
+ Codex reads `.codex/config.toml` only in a trusted project: trust the project when Codex asks.
189
+ Codex has no `type` for MCP servers and does not support SSE: an SSE server is skipped with a
190
+ note, and `headers` become `http_headers`. tunestra owns the whole `.codex/config.toml`; keep
191
+ your personal Codex settings in `~/.codex/config.toml`.
170
192
 
171
193
  `<NN>` orders rules from specific to general: `10` project, `20` team, `30` domain, `40` base.
172
194
  Level instructions get the key `instructions`, and the preset name when it differs from the
@@ -180,7 +202,9 @@ Every generated rule starts with its origin:
180
202
  > Level: team, preset: team. Overrides: base.
181
203
  ```
182
204
 
183
- Skills and commands are copied as is: their frontmatter must stay on the first line.
205
+ Skills and commands are copied as is: their frontmatter must stay on the first line. For
206
+ Codex, a command gets only `name` and `description` in its frontmatter; other fields, like
207
+ `argument-hint`, are for Claude Code.
184
208
 
185
209
  ## Running setup again
186
210
 
@@ -214,14 +238,15 @@ and tells you where to move their text.
214
238
 
215
239
  | Release | Scope |
216
240
  |---|---|
217
- | v0.1 | Inheritance for Claude Code (this document) |
218
- | v0.2 | Codex |
219
- | v0.3 | Spaces, npm sources, lockfile |
220
- | v0.4 | `required` / `locked` policies, CI check |
221
- | v0.5 | Hooks |
222
- | v0.6 | LLM gateway, permissions |
223
- | v0.7 | Local plugins |
224
- | v0.8 | Isolated remote plugins |
241
+ | v0.1 | Inheritance for Claude Code |
242
+ | v0.2 | Codex (this document) |
243
+ | v0.3 | Subagents, a personal `local` level, `env` and permissions |
244
+ | v0.4 | Spaces, npm sources, lockfile |
245
+ | v0.5 | `required` / `locked` policies, CI check |
246
+ | v0.6 | Hooks |
247
+ | v0.7 | LLM gateway |
248
+ | v0.8 | Local plugins |
249
+ | v0.9 | Isolated remote plugins |
225
250
 
226
251
  ## License
227
252
 
package/dist/cli.mjs CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { t as VERSION } from "./src-Dmt0_5_W.mjs";
2
+ import { t as VERSION } from "./src-jfoAZbqo.mjs";
3
3
  import { Command } from "@commander-js/extra-typings";
4
4
  import pc from "picocolors";
5
5
  import { copyFile, lstat, mkdir, readFile, readdir, readlink, rename, rm, rmdir, stat, symlink, unlink, writeFile } from "node:fs/promises";
6
6
  import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
7
7
  import { glob } from "tinyglobby";
8
8
  import { prettifyError, z } from "zod";
9
- import { parse } from "yaml";
9
+ import { parse, stringify } from "yaml";
10
10
  import { downloadTemplate } from "giget";
11
+ import { stringify as stringify$1 } from "smol-toml";
11
12
  import { createHash } from "node:crypto";
12
13
  //#region src/shared/constants/files.ts
13
14
  /** Preset manifest, in the root of every preset. */
@@ -32,6 +33,11 @@ const OutputKind = {
32
33
  /** First line of every generated markdown file; tunestra recognizes its files by it. */
33
34
  const GENERATED_MARKER = "<!-- Generated by tunestra";
34
35
  const GENERATED_NOTICE = `${GENERATED_MARKER}. Do not edit: change the sources and run \`tunestra setup\`. -->`;
36
+ /** The same marker for generated TOML and YAML files, where a comment starts with `#`. */
37
+ const GENERATED_HASH_MARKER = "# Generated by tunestra";
38
+ const GENERATED_HASH_NOTICE = `${GENERATED_HASH_MARKER}. Do not edit: change the sources and run \`tunestra setup\`.`;
39
+ /** Every marker that tunestra puts on the first line of a generated file. */
40
+ const GENERATED_MARKERS = [GENERATED_MARKER, GENERATED_HASH_MARKER];
35
41
  /** Shared output that any agent can read. */
36
42
  const AGENTS_DIR = ".agents";
37
43
  /** Rules for every agent, ordered from specific to general by their file names. */
@@ -155,7 +161,10 @@ async function removeDirIfEmpty(dir) {
155
161
  //#endregion
156
162
  //#region src/shared/constants/agents.ts
157
163
  /** Agents that tunestra generates configuration for. */
158
- const Agent = { Claude: "claude" };
164
+ const Agent = {
165
+ Claude: "claude",
166
+ Codex: "codex"
167
+ };
159
168
  //#endregion
160
169
  //#region src/init/render-config.ts
161
170
  const EXTENDS_EXAMPLE = "# - github:<owner>/<repo>/<path>#<ref>";
@@ -446,13 +455,14 @@ async function loadCommands(dir) {
446
455
  async function loadCommand(dir, source) {
447
456
  const file = join(dir, source);
448
457
  const content = await readFile(file, "utf8");
449
- const { data } = parseFrontmatter(content, file);
458
+ const { data, body } = parseFrontmatter(content, file);
450
459
  const frontmatter = validate(data, commandFrontmatterSchema, `frontmatter in ${file}`);
451
460
  return {
452
461
  key: basename(source, ".md"),
453
462
  source,
454
463
  description: frontmatter.description,
455
- content
464
+ content,
465
+ body
456
466
  };
457
467
  }
458
468
  //#endregion
@@ -941,7 +951,8 @@ function planClaude(context) {
941
951
  `${COMMANDS_DIR}/`,
942
952
  MCP_FILE
943
953
  ],
944
- protectedPaths: [CLAUDE_FILE]
954
+ protectedPaths: [CLAUDE_FILE],
955
+ notes: []
945
956
  };
946
957
  }
947
958
  function copyRule(ruleFile) {
@@ -961,9 +972,147 @@ function linkSkill(projectDir, skill) {
961
972
  };
962
973
  }
963
974
  //#endregion
975
+ //#region src/agents/codex/command-skill.ts
976
+ /** Codex starts the skill only when people call it, like a command. */
977
+ const OPENAI_YAML = [
978
+ GENERATED_HASH_NOTICE,
979
+ "policy:",
980
+ " allow_implicit_invocation: false",
981
+ ""
982
+ ].join("\n");
983
+ /**
984
+ * Codex has no project commands, so a command becomes a skill with the same name that runs
985
+ * only on `$<name>`. The skill gets `name` and `description`; other frontmatter of the
986
+ * command belongs to Claude Code.
987
+ */
988
+ function planCommandSkill(projectDir, command) {
989
+ const { key, description, body } = command.artifact;
990
+ const dir = `${AGENTS_SKILLS_DIR}/${key}`;
991
+ const source = describeArtifactSource(projectDir, command.origin, command.artifact.source);
992
+ const frontmatter = {
993
+ name: key,
994
+ description: description ?? `The /${key} command`
995
+ };
996
+ const skill = `---\n${stringify(frontmatter)}---\n${body}`;
997
+ const skillPath = `${dir}/SKILL.md`;
998
+ const policyPath = `${dir}/agents/openai.yaml`;
999
+ return [createFileOutput(skillPath, skill, source), createFileOutput(policyPath, OPENAI_YAML, source)];
1000
+ }
1001
+ //#endregion
1002
+ //#region src/agents/codex/config-toml.ts
1003
+ /** Starts and ends a multi-line literal string, where TOML escapes nothing. */
1004
+ const LITERAL_DELIMITER = "'''";
1005
+ /**
1006
+ * Control characters that a TOML literal string cannot hold. Tab and line feed are allowed;
1007
+ * a carriage return is in the list, because only `\r\n` would be valid.
1008
+ *
1009
+ * Matches: "\u0000", "\r", "\u001B" (escape), "\u007F" (delete)
1010
+ * Does not match: "\t", "\n", "é", "→"
1011
+ */
1012
+ const CONTROL_CHARACTER_PATTERN = /[\u0000-\u0008\u000B-\u001F\u007F]/;
1013
+ /**
1014
+ * `.codex/config.toml`: the rules as `developer_instructions`, which Codex adds to every
1015
+ * session, and the MCP servers. The rules are a literal string, so that people can read them.
1016
+ */
1017
+ function renderConfigToml(instructions, servers) {
1018
+ const parts = [GENERATED_HASH_NOTICE];
1019
+ if (typeof instructions === "string") {
1020
+ const line = renderInstructions$1(instructions);
1021
+ parts.push(line);
1022
+ }
1023
+ if (servers.length > 0) {
1024
+ const entries = servers.map((server) => {
1025
+ const config = convertMcpConfig(server.artifact.config);
1026
+ return [server.artifact.key, config];
1027
+ });
1028
+ const content = { mcp_servers: Object.fromEntries(entries) };
1029
+ const tables = stringify$1(content).trimEnd();
1030
+ parts.push(tables);
1031
+ }
1032
+ return `${parts.join("\n\n")}\n`;
1033
+ }
1034
+ function renderInstructions$1(text) {
1035
+ const hasDelimiter = text.includes(LITERAL_DELIMITER);
1036
+ const hasControlCharacter = CONTROL_CHARACTER_PATTERN.test(text);
1037
+ if (hasDelimiter || hasControlCharacter) return stringify$1({ developer_instructions: text });
1038
+ return `developer_instructions = ${LITERAL_DELIMITER}\n${text}${LITERAL_DELIMITER}`;
1039
+ }
1040
+ /** Codex picks the transport by `command` or `url` and has no `type`. */
1041
+ function convertMcpConfig(config) {
1042
+ const { type: _type, headers, ...server } = config;
1043
+ if (headers === void 0) return server;
1044
+ return {
1045
+ ...server,
1046
+ http_headers: headers
1047
+ };
1048
+ }
1049
+ //#endregion
1050
+ //#region src/agents/codex/codex-adapter.ts
1051
+ const CONFIG_FILE = ".codex/config.toml";
1052
+ const CONFIG_SOURCE = "the rules/ and mcp/ folders of tunestra.yaml and its presets";
1053
+ const INSTRUCTIONS_INTRO = "Rules of this project, ordered from specific to general. When two rules conflict, follow the one that comes first.";
1054
+ const TRUST_NOTE = "Codex reads rules and MCP servers from .codex/config.toml only in a trusted project: trust the project when Codex asks.";
1055
+ /**
1056
+ * Codex reads AGENTS.md and the skills in `.agents/skills` by itself. Rules go to
1057
+ * `developer_instructions` in `.codex/config.toml`, because Codex does not follow the links
1058
+ * in AGENTS.md; MCP servers go to the same file. Commands become skills.
1059
+ */
1060
+ const codexAdapter = {
1061
+ agent: Agent.Codex,
1062
+ plan: planCodex
1063
+ };
1064
+ function planCodex(context) {
1065
+ const { resolution, ruleFiles } = context;
1066
+ const projectDir = resolution.chain.project.dir;
1067
+ const notes = [];
1068
+ const outputs = [];
1069
+ const commandSkills = resolution.commands.filter((command) => {
1070
+ if (skillName.safeParse(command.artifact.key).success) return true;
1071
+ notes.push(`Codex skips the command "${command.artifact.key}": a skill name needs lowercase letters, digits and single hyphens.`);
1072
+ return false;
1073
+ }).flatMap((command) => planCommandSkill(projectDir, command));
1074
+ outputs.push(...commandSkills);
1075
+ const configFile = planConfigFile(ruleFiles, resolution.mcp.filter((server) => {
1076
+ if (server.artifact.config.type === "sse") {
1077
+ notes.push(`Codex skips the MCP server "${server.artifact.key}": Codex does not support SSE.`);
1078
+ return false;
1079
+ }
1080
+ return true;
1081
+ }));
1082
+ if (configFile === void 0) return {
1083
+ outputs,
1084
+ ignoredPaths: [],
1085
+ protectedPaths: [],
1086
+ notes
1087
+ };
1088
+ outputs.push(configFile);
1089
+ notes.push(TRUST_NOTE);
1090
+ return {
1091
+ outputs,
1092
+ ignoredPaths: [CONFIG_FILE],
1093
+ protectedPaths: [],
1094
+ notes
1095
+ };
1096
+ }
1097
+ function planConfigFile(ruleFiles, servers) {
1098
+ const hasRules = ruleFiles.length > 0;
1099
+ const hasServers = servers.length > 0;
1100
+ if (hasRules === false && hasServers === false) return;
1101
+ const content = renderConfigToml(hasRules ? renderInstructions(ruleFiles) : void 0, servers);
1102
+ return createFileOutput(CONFIG_FILE, content, CONFIG_SOURCE);
1103
+ }
1104
+ /** The rule files as they are, with the header that names the source of each. */
1105
+ function renderInstructions(ruleFiles) {
1106
+ const rules = ruleFiles.map((file) => file.content.trimEnd());
1107
+ return `${[INSTRUCTIONS_INTRO, ...rules].join("\n\n")}\n`;
1108
+ }
1109
+ //#endregion
964
1110
  //#region src/agents/adapters.ts
965
- /** Every supported agent; Codex comes in v0.2. */
966
- const AGENT_ADAPTERS = { [Agent.Claude]: claudeAdapter };
1111
+ /** Every supported agent. */
1112
+ const AGENT_ADAPTERS = {
1113
+ [Agent.Claude]: claudeAdapter,
1114
+ [Agent.Codex]: codexAdapter
1115
+ };
967
1116
  //#endregion
968
1117
  //#region src/shared/utils/markdown.ts
969
1118
  /**
@@ -1049,7 +1198,7 @@ function renderLevelLine(rule) {
1049
1198
  //#endregion
1050
1199
  //#region src/generate/index-sections.ts
1051
1200
  const HIERARCHY_INTRO = "Instructions are ordered from specific to general. When two instructions conflict, follow the one from the higher level in this list.";
1052
- const RULES_INTRO = "Always follow these rules. Claude Code loads them automatically; other agents should read them before starting work.";
1201
+ const RULES_INTRO = "Always follow these rules. Claude Code and Codex load them automatically; other agents should read them before starting work.";
1053
1202
  const SKILLS_INTRO = "Use a skill when the task matches its description.";
1054
1203
  const CHANGING_SECTION = [
1055
1204
  "## Changing this setup",
@@ -1111,8 +1260,8 @@ function renderSkills(skills) {
1111
1260
  }));
1112
1261
  return renderSection("Skills", SKILLS_INTRO, table);
1113
1262
  }
1114
- function renderCommands(commands) {
1115
- return renderSection("Commands", void 0, renderTable([
1263
+ function renderCommands(commands, agents) {
1264
+ const table = renderTable([
1116
1265
  "Command",
1117
1266
  "Level",
1118
1267
  "Description"
@@ -1124,7 +1273,18 @@ function renderCommands(commands) {
1124
1273
  origin,
1125
1274
  description
1126
1275
  ];
1127
- })));
1276
+ }));
1277
+ const hints = agents.map(describeCommandCall).filter(isString);
1278
+ if (hints.length > 0) return renderSection("Commands", hints.join(" "), table);
1279
+ return renderSection("Commands", void 0, table);
1280
+ }
1281
+ /** How to call a command in the agent, when it is not the usual `/<name>`. */
1282
+ function describeCommandCall(agent) {
1283
+ switch (agent) {
1284
+ case Agent.Claude: return;
1285
+ case Agent.Codex: return "In Codex, a command is a skill with the same name: call it as `$<name>`.";
1286
+ default: throw new TunestraError(`Unknown agent: ${agent}`);
1287
+ }
1128
1288
  }
1129
1289
  function renderMcpServers(servers) {
1130
1290
  return renderSection("MCP servers", void 0, renderTable([
@@ -1189,7 +1349,7 @@ function renderAgentsIndex(resolution) {
1189
1349
  sections.push(skills);
1190
1350
  }
1191
1351
  if (hasCommands) {
1192
- const commands = renderCommands(resolution.commands);
1352
+ const commands = renderCommands(resolution.commands, resolution.chain.project.agents);
1193
1353
  sections.push(commands);
1194
1354
  }
1195
1355
  if (hasMcpServers) {
@@ -1218,6 +1378,7 @@ function planSetup(resolution) {
1218
1378
  ];
1219
1379
  const ignoredPaths = [`${AGENTS_DIR}/`];
1220
1380
  const protectedPaths = [INSTRUCTIONS_FILE];
1381
+ const notes = [];
1221
1382
  const context = {
1222
1383
  resolution,
1223
1384
  ruleFiles
@@ -1227,6 +1388,7 @@ function planSetup(resolution) {
1227
1388
  outputs.push(...agentPlan.outputs);
1228
1389
  ignoredPaths.push(...agentPlan.ignoredPaths);
1229
1390
  protectedPaths.push(...agentPlan.protectedPaths);
1391
+ notes.push(...agentPlan.notes);
1230
1392
  }
1231
1393
  ignoredPaths.push(`${STATE_DIR}/`);
1232
1394
  const problems = findPathConflicts(outputs);
@@ -1234,7 +1396,8 @@ function planSetup(resolution) {
1234
1396
  return {
1235
1397
  outputs,
1236
1398
  ignoredPaths,
1237
- protectedPaths
1399
+ protectedPaths,
1400
+ notes
1238
1401
  };
1239
1402
  }
1240
1403
  function planRuleFile(projectDir, rule) {
@@ -1576,11 +1739,16 @@ async function inspectPath(path) {
1576
1739
  return {
1577
1740
  type,
1578
1741
  hash: hashContent(content),
1579
- isMarked: content.subarray(0, 26).toString("utf8") === GENERATED_MARKER
1742
+ isMarked: hasGeneratedMarker(content)
1580
1743
  };
1581
1744
  }
1582
1745
  return { type };
1583
1746
  }
1747
+ function hasGeneratedMarker(content) {
1748
+ return GENERATED_MARKERS.some((marker) => {
1749
+ return content.subarray(0, marker.length).toString("utf8") === marker;
1750
+ });
1751
+ }
1584
1752
  /** A file matches a record by its hash, a symlink by its target. */
1585
1753
  function matchesRecord(entry, record) {
1586
1754
  if (entry.type === PathType.File && "hash" in record) return entry.hash === record.hash;
@@ -1728,6 +1896,7 @@ async function runSetup(projectDir, options) {
1728
1896
  const sync = await planSync(chain.project.dir, plan, previous);
1729
1897
  const report = {
1730
1898
  resolution,
1899
+ plan,
1731
1900
  sync
1732
1901
  };
1733
1902
  if (options.dryRun) return report;
@@ -1772,26 +1941,30 @@ function measureColumn(rows, column) {
1772
1941
  //#region src/cli/commands/setup.ts
1773
1942
  async function describeSetup(projectDir, options) {
1774
1943
  const report = await runSetup(projectDir, options);
1775
- const summary = describeSync(report.sync, options);
1944
+ const summary = describeSync(report.plan, report.sync, options);
1776
1945
  if (options.verbose) return `${describeResolution(report.resolution)}
1777
1946
 
1778
1947
  ${summary}`;
1779
1948
  return summary;
1780
1949
  }
1781
- function describeSync(sync, options) {
1950
+ function describeSync(plan, sync, options) {
1782
1951
  const warningIcon = pc.yellow("⚠");
1783
1952
  const lines = sync.warnings.map((warning) => `${warningIcon} ${warning}`);
1953
+ const noteIcon = pc.cyan("ℹ");
1954
+ const notes = plan.notes.map((note) => `${noteIcon} ${note}`);
1784
1955
  const writeCount = sync.outputs.length;
1785
1956
  const removeCount = sync.removals.length;
1786
1957
  if (options.dryRun) {
1787
1958
  const writes = sync.outputs.map((output) => ` write ${output.path}`);
1788
1959
  const removals = sync.removals.map((path) => ` remove ${path}`);
1789
- lines.push(...writes, ...removals, "", "Nothing was written (--dry-run).");
1960
+ lines.push(...writes, ...removals, ...notes, "", "Nothing was written (--dry-run).");
1790
1961
  return lines.join("\n");
1791
1962
  }
1792
1963
  const check = pc.green("✔");
1964
+ const committedFiles = plan.protectedPaths.join(" and ");
1793
1965
  lines.push(`${check} Generated ${writeCount} files, removed ${removeCount} stale files.`);
1794
- lines.push("Commit AGENTS.md and CLAUDE.md; the rest is in .gitignore.");
1966
+ lines.push(...notes);
1967
+ lines.push(`Commit ${committedFiles}; the rest is in .gitignore.`);
1795
1968
  return lines.join("\n");
1796
1969
  }
1797
1970
  //#endregion
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- export { t as VERSION } from "./src-Dmt0_5_W.mjs";
1
+ export { t as VERSION } from "./src-jfoAZbqo.mjs";
@@ -1,5 +1,5 @@
1
1
  //#endregion
2
2
  //#region src/index.ts
3
- const VERSION = "0.1.1";
3
+ const VERSION = "0.2.0";
4
4
  //#endregion
5
5
  export { VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tunestra",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Tune your AI agents to play as one orchestra: one source of rules, skills, commands and MCP servers for Claude Code, Codex and more.",
5
5
  "keywords": [
6
6
  "ai",
@@ -44,7 +44,13 @@
44
44
  "test:watch": "vitest",
45
45
  "coverage": "vitest run --coverage",
46
46
  "check": "npm run typecheck && npm run lint && npm run test && npm run build",
47
- "prepublishOnly": "npm run check"
47
+ "prepublishOnly": "npm run check",
48
+ "changeset": "changeset",
49
+ "version-packages": "changeset version && npm install --package-lock-only",
50
+ "release": "changeset publish"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
48
54
  },
49
55
  "engines": {
50
56
  "node": ">=22.18"
@@ -54,12 +60,14 @@
54
60
  "commander": "^15.0.0",
55
61
  "giget": "^3.3.1",
56
62
  "picocolors": "^1.1.1",
63
+ "smol-toml": "^1.9.1",
57
64
  "tinyglobby": "^0.2.17",
58
65
  "yaml": "^2.9.1",
59
66
  "zod": "^4.6.7"
60
67
  },
61
68
  "devDependencies": {
62
69
  "@biomejs/biome": "^2.5.15",
70
+ "@changesets/cli": "^3.0.3",
63
71
  "@types/node": "^22.20.5",
64
72
  "@vitest/coverage-v8": "^5.0.5",
65
73
  "tsdown": "^0.23.1",