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 +44 -19
- package/dist/cli.mjs +192 -19
- package/dist/index.mjs +1 -1
- package/dist/{src-Dmt0_5_W.mjs → src-jfoAZbqo.mjs} +1 -1
- package/package.json +10 -2
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.**
|
|
13
|
-
>
|
|
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
|
|
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]
|
|
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.
|
|
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
|
|
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
|
-
|
|
169
|
-
|
|
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
|
|
218
|
-
| v0.2 | Codex |
|
|
219
|
-
| v0.3 |
|
|
220
|
-
| v0.4 |
|
|
221
|
-
| v0.5 |
|
|
222
|
-
| v0.6 |
|
|
223
|
-
| v0.7 |
|
|
224
|
-
| v0.8 |
|
|
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-
|
|
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 = {
|
|
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
|
|
966
|
-
const AGENT_ADAPTERS = {
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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-
|
|
1
|
+
export { t as VERSION } from "./src-jfoAZbqo.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tunestra",
|
|
3
|
-
"version": "0.
|
|
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",
|