Skip to content

Commit 6299cd9

Browse files
authored
Merge branch 'main' into docs/update-readme-accuracy-2960652682020426675
2 parents d3ac520 + 49a576d commit 6299cd9

13 files changed

Lines changed: 1442 additions & 451 deletions

File tree

.agents/journal/bolt.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,14 @@
2323
**Learning:** The `compress_agents_md_content` function was performing $O(N)$ string allocations, where $N$ is the number of lines in `AGENTS.md`. By refactoring helper functions to use mutable buffer passing and switching code fence state to use string slices (`&str`), we eliminated almost all heap allocations in the compression loop.
2424

2525
**Action:** Avoid returning `String` from functions called inside loops processing large text files. Instead, pass a mutable `&mut String` buffer to be filled. Use `Option<&str>` instead of `Option<String>` for state that refers to parts of the input buffer.
26+
27+
## 2026-02-15 - Caching Redundant I/O and Compression
28+
29+
**Learning:** Multiple agents often share the same source file (e.g., AGENTS.md) or target directories. Without caching, the Linker performs redundant file reads, string compression, and directory existence checks for every target.
30+
**Action:** Implement `compression_cache` (HashMap) to memoize expensive text compression and `ensured_outputs` (HashSet) to track verified destination paths. This optimization reduced execution time by ~83% (from 0.055s to 0.0094s) in a 100-agent scenario.
31+
32+
## 2026-02-26 - Support for 34 New Agents and Config Migration
33+
34+
**Learning:** Supporting a wide array of AI agents (41 total) requires handling diverse MCP configuration formats (YAML, nested JSON) and legacy instruction files (e.g., ROO.md, CLINE.md). Centralizing these into a unified structure during 'init --wizard' and using specialized formatters (Standard, YAML, Continue) ensures a seamless developer experience across all tools.
35+
36+
**Action:** When adding support for new agents, identify their specific configuration requirements (path, format, and instruction file naming) and implement reusable formatters. Always prioritize automated migration of existing configuration to the central .agents/ directory to minimize manual setup.

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,18 @@ node_modules/
3535
.pnpm-store/
3636
pnpm-debug.log
3737
# START AI Agent Symlinks
38+
.claude/commands/
39+
.claude/skills/
3840
.github/agents
3941
.github/copilot-instructions.md
4042
.github/prompts
4143
.github/skills
44+
.mcp.json
4245
.opencode/command
4346
.opencode/skill
47+
.vscode/mcp.json
4448
AGENTS.md
4549
CLAUDE.md
50+
opencode.json
4651
# END AI Agent Symlinks
52+
changes.diff

fix_clippy.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import sys
2+
3+
with open('src/fs.rs', 'r') as f:
4+
fs_content = f.read()
5+
6+
old_fs = """ if let Some(parent) = dst_abs.parent() {
7+
if let Ok(parent_canon) = fs::canonicalize(parent) {
8+
if parent_canon.starts_with(&src_canon) {
9+
return Err(std::io::Error::new(
10+
std::io::ErrorKind::InvalidInput,
11+
format!("Cannot copy directory into itself: {:?} is inside {:?}", dst_abs, src_canon),
12+
).into());
13+
}
14+
}
15+
}"""
16+
17+
new_fs = """ if let Some(parent_canon) = dst_abs.parent().and_then(|p| fs::canonicalize(p).ok()) {
18+
if parent_canon.starts_with(&src_canon) {
19+
return Err(std::io::Error::new(
20+
std::io::ErrorKind::InvalidInput,
21+
format!("Cannot copy directory into itself: {:?} is inside {:?}", dst_abs, src_canon),
22+
).into());
23+
}
24+
}"""
25+
26+
fs_content = fs_content.replace(old_fs, new_fs)
27+
with open('src/fs.rs', 'w') as f:
28+
f.write(fs_content)
29+
30+
with open('src/mcp.rs', 'r') as f:
31+
mcp_content = f.read()
32+
33+
old_mcp1 = """ if let Some(key) = self.wrapper_key {
34+
if let Some(wrapped) = parsed.get(key) {
35+
if let Some(obj) = wrapped.as_mapping() {
36+
let mut res = HashMap::new();
37+
for (k, v) in obj {
38+
let key_str = k.as_str().unwrap_or_default().to_string();
39+
let json_v = serde_json::to_value(v)?;
40+
res.insert(key_str, json_v);
41+
}
42+
return Ok(res);
43+
}
44+
}
45+
}"""
46+
47+
new_mcp1 = """ if let Some(obj) = self.wrapper_key.and_then(|key| parsed.get(key)).and_then(|v| v.as_mapping()) {
48+
let mut res = HashMap::new();
49+
for (k, v) in obj {
50+
let key_str = k.as_str().unwrap_or_default().to_string();
51+
let json_v = serde_json::to_value(v)?;
52+
res.insert(key_str, json_v);
53+
}
54+
return Ok(res);
55+
}"""
56+
57+
old_mcp2 = """ if let Some(mcp) = parsed.get("mcpServers") {
58+
if let Some(obj) = mcp.as_object() {
59+
return Ok(obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
60+
}
61+
}"""
62+
63+
new_mcp2 = """ if let Some(obj) = parsed.get("mcpServers").and_then(|mcp| mcp.as_object()) {
64+
return Ok(obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
65+
}"""
66+
67+
mcp_content = mcp_content.replace(old_mcp1, new_mcp1).replace(old_mcp2, new_mcp2)
68+
with open('src/mcp.rs', 'w') as f:
69+
f.write(mcp_content)

fix_clippy_v2.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import sys
2+
3+
with open('src/mcp.rs', 'r') as f:
4+
mcp_content = f.read()
5+
6+
# Fix YAML parse_existing
7+
old_yaml_parse = """ fn parse_existing(&self, content: &str) -> Result<HashMap<String, Value>> {
8+
let parsed: serde_yaml::Value = serde_yaml::from_str(content).context("Failed to parse YAML")?;
9+
if let Some(key) = self.wrapper_key {
10+
if let Some(wrapped) = parsed.get(key) {
11+
if let Some(obj) = wrapped.as_mapping() {
12+
let mut res = HashMap::new();
13+
for (k, v) in obj {
14+
let key_str = k.as_str().unwrap_or_default().to_string();
15+
let json_v = serde_json::to_value(v)?;
16+
res.insert(key_str, json_v);
17+
}
18+
return Ok(res);
19+
}
20+
}
21+
}
22+
Ok(HashMap::new())
23+
}"""
24+
25+
new_yaml_parse = """ fn parse_existing(&self, content: &str) -> Result<HashMap<String, Value>> {
26+
let parsed: serde_yaml::Value = serde_yaml::from_str(content).context("Failed to parse YAML")?;
27+
if let Some(obj) = self.wrapper_key
28+
.and_then(|key| parsed.get(key))
29+
.and_then(|v| v.as_mapping())
30+
{
31+
let mut res = HashMap::new();
32+
for (k, v) in obj {
33+
let key_str = k.as_str().unwrap_or_default().to_string();
34+
let json_v = serde_json::to_value(v)?;
35+
res.insert(key_str, json_v);
36+
}
37+
return Ok(res);
38+
}
39+
Ok(HashMap::new())
40+
}"""
41+
42+
# Fix Continue parse_existing
43+
old_continue_parse = """ fn parse_existing(&self, content: &str) -> Result<HashMap<String, Value>> {
44+
let parsed: Value = serde_json::from_str(content).unwrap_or(json!({}));
45+
if let Some(mcp) = parsed.get("mcpServers") {
46+
if let Some(obj) = mcp.as_object() {
47+
return Ok(obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
48+
}
49+
}
50+
Ok(HashMap::new())
51+
}"""
52+
53+
new_continue_parse = """ fn parse_existing(&self, content: &str) -> Result<HashMap<String, Value>> {
54+
let parsed: Value = serde_json::from_str(content).unwrap_or(json!({}));
55+
if let Some(obj) = parsed.get("mcpServers").and_then(|mcp| mcp.as_object()) {
56+
return Ok(obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
57+
}
58+
Ok(HashMap::new())
59+
}"""
60+
61+
mcp_content = mcp_content.replace(old_yaml_parse, new_yaml_parse).replace(old_continue_parse, new_continue_parse)
62+
with open('src/mcp.rs', 'w') as f:
63+
f.write(mcp_content)

fix_clippy_v3.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import sys
2+
3+
with open('src/fs.rs', 'r') as f:
4+
fs_content = f.read()
5+
6+
old_fs = """ if let Some(parent_canon) = dst_abs
7+
.parent()
8+
.and_then(|p| fs::canonicalize(p).ok())
9+
.filter(|p| p.starts_with(&src_canon))
10+
{"""
11+
12+
new_fs = """ if dst_abs
13+
.parent()
14+
.and_then(|p| fs::canonicalize(p).ok())
15+
.filter(|p| p.starts_with(&src_canon))
16+
.is_some()
17+
{"""
18+
19+
fs_content = fs_content.replace(old_fs, new_fs)
20+
with open('src/fs.rs', 'w') as f:
21+
f.write(fs_content)
22+
23+
with open('src/mcp.rs', 'r') as f:
24+
mcp_content = f.read()
25+
26+
# Fix YAML parse_existing (nested ifs)
27+
old_yaml_parse = """ fn parse_existing(&self, content: &str) -> Result<HashMap<String, Value>> {
28+
let parsed: serde_yaml::Value = serde_yaml::from_str(content).context("Failed to parse YAML")?;
29+
if let Some(obj) = self.wrapper_key
30+
.and_then(|key| parsed.get(key))
31+
.and_then(|v| v.as_mapping())
32+
{
33+
let mut res = HashMap::new();
34+
for (k, v) in obj {
35+
let key_str = k.as_str().unwrap_or_default().to_string();
36+
let json_v = serde_json::to_value(v)?;
37+
res.insert(key_str, json_v);
38+
}
39+
return Ok(res);
40+
}
41+
Ok(HashMap::new())
42+
}"""
43+
44+
# Wait, I previously used python script to fix clippy, let me re-read it.
45+
# Actually I'll just write it correctly.
46+
new_yaml_parse = """ fn parse_existing(&self, content: &str) -> Result<HashMap<String, Value>> {
47+
let parsed: serde_yaml::Value =
48+
serde_yaml::from_str(content).context("Failed to parse YAML")?;
49+
if let Some(obj) = self
50+
.wrapper_key
51+
.and_then(|key| parsed.get(key))
52+
.and_then(|v| v.as_mapping())
53+
{
54+
let mut res = HashMap::new();
55+
for (k, v) in obj {
56+
let key_str = k.as_str().unwrap_or_default().to_string();
57+
let json_v = serde_json::to_value(v)?;
58+
res.insert(key_str, json_v);
59+
}
60+
return Ok(res);
61+
}
62+
Ok(HashMap::new())
63+
}"""
64+
65+
# Actually the error was: this statement can be collapsed
66+
# but I already used .and_then chain. Clippy might still complain if I have if let inside if let.
67+
# Let's check the code.

npm/agentsync/package.json

Lines changed: 59 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,61 @@
11
{
2-
"name": "@dallay/agentsync",
3-
"version": "1.28.0",
4-
"description": "A fast CLI tool to sync AI agent configurations and MCP servers across Claude, Copilot, Cursor, and more using symbolic links.",
5-
"author": "Yuniel Acosta <yunielacosta738@gmail.com>",
6-
"license": "MIT",
7-
"publishConfig": {
8-
"access": "public"
9-
},
10-
"repository": {
11-
"type": "git",
12-
"url": "git+https://github.com/dallay/agentsync.git"
13-
},
14-
"homepage": "https://github.com/dallay/agentsync#readme",
15-
"bugs": {
16-
"url": "https://github.com/dallay/agentsync/issues"
17-
},
18-
"keywords": [
19-
"ai",
20-
"agents",
21-
"symlink",
22-
"configuration",
23-
"cli",
24-
"claude",
25-
"copilot",
26-
"gemini",
27-
"cursor",
28-
"opencode",
29-
"mcp",
30-
"mcp-server"
31-
],
32-
"bin": {
33-
"agentsync": "lib/index.js"
34-
},
35-
"main": "lib/index.js",
36-
"files": [
37-
"lib"
38-
],
39-
"scripts": {
40-
"test": "pnpm run typecheck",
41-
"typecheck": "tsc --noEmit",
42-
"build": "tsc",
43-
"clean": "node -e \"require('fs').rmSync('lib', {recursive: true, force: true})\"",
44-
"prepublishOnly": "npm run build"
45-
},
46-
"devDependencies": {
47-
"@types/node": "^24.1.0",
48-
"typescript": "^5.9.3"
49-
},
50-
"optionalDependencies": {
51-
"@dallay/agentsync-linux-x64": "1.28.0",
52-
"@dallay/agentsync-linux-arm64": "1.28.0",
53-
"@dallay/agentsync-darwin-x64": "1.28.0",
54-
"@dallay/agentsync-darwin-arm64": "1.28.0",
55-
"@dallay/agentsync-windows-x64": "1.28.0",
56-
"@dallay/agentsync-windows-arm64": "1.28.0"
57-
},
58-
"engines": {
59-
"node": ">=18"
60-
}
2+
"name": "@dallay/agentsync",
3+
"version": "1.28.0",
4+
"description": "A fast CLI tool to sync AI agent configurations and MCP servers across Claude, Copilot, Cursor, and more using symbolic links.",
5+
"author": "Yuniel Acosta <yunielacosta738@gmail.com>",
6+
"license": "MIT",
7+
"publishConfig": {
8+
"access": "public"
9+
},
10+
"repository": {
11+
"type": "git",
12+
"url": "git+https://github.com/dallay/agentsync.git"
13+
},
14+
"homepage": "https://github.com/dallay/agentsync#readme",
15+
"bugs": {
16+
"url": "https://github.com/dallay/agentsync/issues"
17+
},
18+
"keywords": [
19+
"ai",
20+
"agents",
21+
"symlink",
22+
"configuration",
23+
"cli",
24+
"claude",
25+
"copilot",
26+
"gemini",
27+
"cursor",
28+
"opencode",
29+
"mcp",
30+
"mcp-server"
31+
],
32+
"bin": {
33+
"agentsync": "lib/index.js"
34+
},
35+
"main": "lib/index.js",
36+
"files": [
37+
"lib"
38+
],
39+
"scripts": {
40+
"test": "pnpm run typecheck",
41+
"typecheck": "tsc --noEmit",
42+
"build": "tsc",
43+
"clean": "node -e \"require('fs').rmSync('lib', {recursive: true, force: true})\"",
44+
"prepublishOnly": "npm run build"
45+
},
46+
"devDependencies": {
47+
"@types/node": "^24.1.0",
48+
"typescript": "^5.9.3"
49+
},
50+
"optionalDependencies": {
51+
"@dallay/agentsync-linux-x64": "1.28.0",
52+
"@dallay/agentsync-linux-arm64": "1.28.0",
53+
"@dallay/agentsync-darwin-x64": "1.28.0",
54+
"@dallay/agentsync-darwin-arm64": "1.28.0",
55+
"@dallay/agentsync-windows-x64": "1.28.0",
56+
"@dallay/agentsync-windows-arm64": "1.28.0"
57+
},
58+
"engines": {
59+
"node": ">=18"
60+
}
6161
}

scripts/bench_setup.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
2+
import os
3+
4+
# Create a large AGENTS.md
5+
agents_md_content = "# Project Agents\n\n" + "\n".join([f"## Agent {i}\nThis is agent {i} configuration." for i in range(1000)])
6+
os.makedirs(".agents", exist_ok=True)
7+
with open(".agents/AGENTS.md", "w") as f:
8+
f.write(agents_md_content)
9+
10+
# Create agentsync.toml with many agents
11+
config = """
12+
source_dir = "."
13+
compress_agents_md = true
14+
15+
[gitignore]
16+
enabled = false
17+
"""
18+
19+
for i in range(100):
20+
config += f"""
21+
[agents.agent{i}]
22+
enabled = true
23+
[agents.agent{i}.targets.main]
24+
source = "AGENTS.md"
25+
destination = "agent{i}.md"
26+
type = "symlink"
27+
"""
28+
29+
with open(".agents/agentsync.toml", "w") as f:
30+
f.write(config)

0 commit comments

Comments
 (0)