tunestra 0.1.0 → 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 +203 -19
- package/dist/index.mjs +1 -1
- package/dist/{src-DSbrKtiC.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. */
|
|
@@ -143,10 +149,22 @@ async function readFileIfExists(path) {
|
|
|
143
149
|
throw error;
|
|
144
150
|
}
|
|
145
151
|
}
|
|
152
|
+
/** Removes a directory only when it has nothing inside; a missing directory is fine too. */
|
|
153
|
+
async function removeDirIfEmpty(dir) {
|
|
154
|
+
try {
|
|
155
|
+
if ((await readdir(dir)).length === 0) await rmdir(dir);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (isMissingPathError(error)) return;
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
146
161
|
//#endregion
|
|
147
162
|
//#region src/shared/constants/agents.ts
|
|
148
163
|
/** Agents that tunestra generates configuration for. */
|
|
149
|
-
const Agent = {
|
|
164
|
+
const Agent = {
|
|
165
|
+
Claude: "claude",
|
|
166
|
+
Codex: "codex"
|
|
167
|
+
};
|
|
150
168
|
//#endregion
|
|
151
169
|
//#region src/init/render-config.ts
|
|
152
170
|
const EXTENDS_EXAMPLE = "# - github:<owner>/<repo>/<path>#<ref>";
|
|
@@ -437,13 +455,14 @@ async function loadCommands(dir) {
|
|
|
437
455
|
async function loadCommand(dir, source) {
|
|
438
456
|
const file = join(dir, source);
|
|
439
457
|
const content = await readFile(file, "utf8");
|
|
440
|
-
const { data } = parseFrontmatter(content, file);
|
|
458
|
+
const { data, body } = parseFrontmatter(content, file);
|
|
441
459
|
const frontmatter = validate(data, commandFrontmatterSchema, `frontmatter in ${file}`);
|
|
442
460
|
return {
|
|
443
461
|
key: basename(source, ".md"),
|
|
444
462
|
source,
|
|
445
463
|
description: frontmatter.description,
|
|
446
|
-
content
|
|
464
|
+
content,
|
|
465
|
+
body
|
|
447
466
|
};
|
|
448
467
|
}
|
|
449
468
|
//#endregion
|
|
@@ -932,7 +951,8 @@ function planClaude(context) {
|
|
|
932
951
|
`${COMMANDS_DIR}/`,
|
|
933
952
|
MCP_FILE
|
|
934
953
|
],
|
|
935
|
-
protectedPaths: [CLAUDE_FILE]
|
|
954
|
+
protectedPaths: [CLAUDE_FILE],
|
|
955
|
+
notes: []
|
|
936
956
|
};
|
|
937
957
|
}
|
|
938
958
|
function copyRule(ruleFile) {
|
|
@@ -952,9 +972,147 @@ function linkSkill(projectDir, skill) {
|
|
|
952
972
|
};
|
|
953
973
|
}
|
|
954
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
|
|
955
1110
|
//#region src/agents/adapters.ts
|
|
956
|
-
/** Every supported agent
|
|
957
|
-
const AGENT_ADAPTERS = {
|
|
1111
|
+
/** Every supported agent. */
|
|
1112
|
+
const AGENT_ADAPTERS = {
|
|
1113
|
+
[Agent.Claude]: claudeAdapter,
|
|
1114
|
+
[Agent.Codex]: codexAdapter
|
|
1115
|
+
};
|
|
958
1116
|
//#endregion
|
|
959
1117
|
//#region src/shared/utils/markdown.ts
|
|
960
1118
|
/**
|
|
@@ -1040,7 +1198,7 @@ function renderLevelLine(rule) {
|
|
|
1040
1198
|
//#endregion
|
|
1041
1199
|
//#region src/generate/index-sections.ts
|
|
1042
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.";
|
|
1043
|
-
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.";
|
|
1044
1202
|
const SKILLS_INTRO = "Use a skill when the task matches its description.";
|
|
1045
1203
|
const CHANGING_SECTION = [
|
|
1046
1204
|
"## Changing this setup",
|
|
@@ -1102,8 +1260,8 @@ function renderSkills(skills) {
|
|
|
1102
1260
|
}));
|
|
1103
1261
|
return renderSection("Skills", SKILLS_INTRO, table);
|
|
1104
1262
|
}
|
|
1105
|
-
function renderCommands(commands) {
|
|
1106
|
-
|
|
1263
|
+
function renderCommands(commands, agents) {
|
|
1264
|
+
const table = renderTable([
|
|
1107
1265
|
"Command",
|
|
1108
1266
|
"Level",
|
|
1109
1267
|
"Description"
|
|
@@ -1115,7 +1273,18 @@ function renderCommands(commands) {
|
|
|
1115
1273
|
origin,
|
|
1116
1274
|
description
|
|
1117
1275
|
];
|
|
1118
|
-
}))
|
|
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
|
+
}
|
|
1119
1288
|
}
|
|
1120
1289
|
function renderMcpServers(servers) {
|
|
1121
1290
|
return renderSection("MCP servers", void 0, renderTable([
|
|
@@ -1180,7 +1349,7 @@ function renderAgentsIndex(resolution) {
|
|
|
1180
1349
|
sections.push(skills);
|
|
1181
1350
|
}
|
|
1182
1351
|
if (hasCommands) {
|
|
1183
|
-
const commands = renderCommands(resolution.commands);
|
|
1352
|
+
const commands = renderCommands(resolution.commands, resolution.chain.project.agents);
|
|
1184
1353
|
sections.push(commands);
|
|
1185
1354
|
}
|
|
1186
1355
|
if (hasMcpServers) {
|
|
@@ -1209,6 +1378,7 @@ function planSetup(resolution) {
|
|
|
1209
1378
|
];
|
|
1210
1379
|
const ignoredPaths = [`${AGENTS_DIR}/`];
|
|
1211
1380
|
const protectedPaths = [INSTRUCTIONS_FILE];
|
|
1381
|
+
const notes = [];
|
|
1212
1382
|
const context = {
|
|
1213
1383
|
resolution,
|
|
1214
1384
|
ruleFiles
|
|
@@ -1218,6 +1388,7 @@ function planSetup(resolution) {
|
|
|
1218
1388
|
outputs.push(...agentPlan.outputs);
|
|
1219
1389
|
ignoredPaths.push(...agentPlan.ignoredPaths);
|
|
1220
1390
|
protectedPaths.push(...agentPlan.protectedPaths);
|
|
1391
|
+
notes.push(...agentPlan.notes);
|
|
1221
1392
|
}
|
|
1222
1393
|
ignoredPaths.push(`${STATE_DIR}/`);
|
|
1223
1394
|
const problems = findPathConflicts(outputs);
|
|
@@ -1225,7 +1396,8 @@ function planSetup(resolution) {
|
|
|
1225
1396
|
return {
|
|
1226
1397
|
outputs,
|
|
1227
1398
|
ignoredPaths,
|
|
1228
|
-
protectedPaths
|
|
1399
|
+
protectedPaths,
|
|
1400
|
+
notes
|
|
1229
1401
|
};
|
|
1230
1402
|
}
|
|
1231
1403
|
function planRuleFile(projectDir, rule) {
|
|
@@ -1567,11 +1739,16 @@ async function inspectPath(path) {
|
|
|
1567
1739
|
return {
|
|
1568
1740
|
type,
|
|
1569
1741
|
hash: hashContent(content),
|
|
1570
|
-
isMarked: content
|
|
1742
|
+
isMarked: hasGeneratedMarker(content)
|
|
1571
1743
|
};
|
|
1572
1744
|
}
|
|
1573
1745
|
return { type };
|
|
1574
1746
|
}
|
|
1747
|
+
function hasGeneratedMarker(content) {
|
|
1748
|
+
return GENERATED_MARKERS.some((marker) => {
|
|
1749
|
+
return content.subarray(0, marker.length).toString("utf8") === marker;
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1575
1752
|
/** A file matches a record by its hash, a symlink by its target. */
|
|
1576
1753
|
function matchesRecord(entry, record) {
|
|
1577
1754
|
if (entry.type === PathType.File && "hash" in record) return entry.hash === record.hash;
|
|
@@ -1719,6 +1896,7 @@ async function runSetup(projectDir, options) {
|
|
|
1719
1896
|
const sync = await planSync(chain.project.dir, plan, previous);
|
|
1720
1897
|
const report = {
|
|
1721
1898
|
resolution,
|
|
1899
|
+
plan,
|
|
1722
1900
|
sync
|
|
1723
1901
|
};
|
|
1724
1902
|
if (options.dryRun) return report;
|
|
@@ -1763,26 +1941,30 @@ function measureColumn(rows, column) {
|
|
|
1763
1941
|
//#region src/cli/commands/setup.ts
|
|
1764
1942
|
async function describeSetup(projectDir, options) {
|
|
1765
1943
|
const report = await runSetup(projectDir, options);
|
|
1766
|
-
const summary = describeSync(report.sync, options);
|
|
1944
|
+
const summary = describeSync(report.plan, report.sync, options);
|
|
1767
1945
|
if (options.verbose) return `${describeResolution(report.resolution)}
|
|
1768
1946
|
|
|
1769
1947
|
${summary}`;
|
|
1770
1948
|
return summary;
|
|
1771
1949
|
}
|
|
1772
|
-
function describeSync(sync, options) {
|
|
1950
|
+
function describeSync(plan, sync, options) {
|
|
1773
1951
|
const warningIcon = pc.yellow("⚠");
|
|
1774
1952
|
const lines = sync.warnings.map((warning) => `${warningIcon} ${warning}`);
|
|
1953
|
+
const noteIcon = pc.cyan("ℹ");
|
|
1954
|
+
const notes = plan.notes.map((note) => `${noteIcon} ${note}`);
|
|
1775
1955
|
const writeCount = sync.outputs.length;
|
|
1776
1956
|
const removeCount = sync.removals.length;
|
|
1777
1957
|
if (options.dryRun) {
|
|
1778
1958
|
const writes = sync.outputs.map((output) => ` write ${output.path}`);
|
|
1779
1959
|
const removals = sync.removals.map((path) => ` remove ${path}`);
|
|
1780
|
-
lines.push(...writes, ...removals, "", "Nothing was written (--dry-run).");
|
|
1960
|
+
lines.push(...writes, ...removals, ...notes, "", "Nothing was written (--dry-run).");
|
|
1781
1961
|
return lines.join("\n");
|
|
1782
1962
|
}
|
|
1783
1963
|
const check = pc.green("✔");
|
|
1964
|
+
const committedFiles = plan.protectedPaths.join(" and ");
|
|
1784
1965
|
lines.push(`${check} Generated ${writeCount} files, removed ${removeCount} stale files.`);
|
|
1785
|
-
lines.push(
|
|
1966
|
+
lines.push(...notes);
|
|
1967
|
+
lines.push(`Commit ${committedFiles}; the rest is in .gitignore.`);
|
|
1786
1968
|
return lines.join("\n");
|
|
1787
1969
|
}
|
|
1788
1970
|
//#endregion
|
|
@@ -1815,12 +1997,14 @@ async function runUninstall(projectDir, options) {
|
|
|
1815
1997
|
warnings
|
|
1816
1998
|
};
|
|
1817
1999
|
const stateDir = join(projectDir, STATE_DIR);
|
|
2000
|
+
const tunestraDir = join(projectDir, PROJECT_DIR);
|
|
1818
2001
|
await removeOutputs(projectDir, removals);
|
|
1819
2002
|
await removeGitignoreBlockFile(projectDir);
|
|
1820
2003
|
await rm(stateDir, {
|
|
1821
2004
|
recursive: true,
|
|
1822
2005
|
force: true
|
|
1823
2006
|
});
|
|
2007
|
+
await removeDirIfEmpty(tunestraDir);
|
|
1824
2008
|
return {
|
|
1825
2009
|
removals,
|
|
1826
2010
|
warnings
|
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",
|