oneagent 0.1.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/package.json +33 -0
- package/src/commands/generate.ts +33 -0
- package/src/commands/init.ts +186 -0
- package/src/commands/status.ts +47 -0
- package/src/index.ts +16 -0
- package/src/utils.ts +13 -0
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oneagent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "One source of truth for AI agent rules — distributed via symlinks to Claude, Cursor, Windsurf, Copilot, OpenCode",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/moskalakamil/oneagent"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"oneagent": "./src/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"module": "./src/index.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./src/index.ts"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"typecheck": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@clack/prompts": "latest",
|
|
26
|
+
"@moskala/oneagent-core": "0.1.0",
|
|
27
|
+
"citty": "latest"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/bun": "latest",
|
|
31
|
+
"typescript": "^5"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { spinner } from "@clack/prompts";
|
|
3
|
+
import { readConfig, generate } from "@moskala/oneagent-core";
|
|
4
|
+
|
|
5
|
+
export default defineCommand({
|
|
6
|
+
meta: {
|
|
7
|
+
name: "generate",
|
|
8
|
+
description: "Generate symlinks and agent-specific files",
|
|
9
|
+
},
|
|
10
|
+
async run() {
|
|
11
|
+
const root = process.cwd();
|
|
12
|
+
|
|
13
|
+
let config;
|
|
14
|
+
try {
|
|
15
|
+
config = await readConfig(root);
|
|
16
|
+
} catch {
|
|
17
|
+
console.error("Error: No .oneagent/config.yml found. Run `oneagent init` first.");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const s = spinner();
|
|
22
|
+
s.start("Generating...");
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await generate(root, config);
|
|
26
|
+
s.stop("Generated successfully.");
|
|
27
|
+
} catch (error) {
|
|
28
|
+
s.stop("Generation failed.");
|
|
29
|
+
console.error(error);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import {
|
|
3
|
+
intro,
|
|
4
|
+
outro,
|
|
5
|
+
confirm,
|
|
6
|
+
multiselect,
|
|
7
|
+
select,
|
|
8
|
+
spinner,
|
|
9
|
+
note,
|
|
10
|
+
isCancel,
|
|
11
|
+
} from "@clack/prompts";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import fs from "fs/promises";
|
|
14
|
+
import { timeAgo } from "../utils.ts";
|
|
15
|
+
import {
|
|
16
|
+
configExists,
|
|
17
|
+
writeConfig,
|
|
18
|
+
detectExistingFiles,
|
|
19
|
+
filesHaveSameContent,
|
|
20
|
+
generate,
|
|
21
|
+
type AgentTarget,
|
|
22
|
+
type Config,
|
|
23
|
+
type DetectedFile,
|
|
24
|
+
} from "@moskala/oneagent-core";
|
|
25
|
+
|
|
26
|
+
const DOTAI_META_RULE = `---
|
|
27
|
+
applyTo: "**"
|
|
28
|
+
---
|
|
29
|
+
# oneagent
|
|
30
|
+
|
|
31
|
+
This project uses [oneagent](https://github.com/moskalakamil/oneagent) to manage AI agent configuration.
|
|
32
|
+
|
|
33
|
+
Rules are stored in \`.oneagent/rules/\` and distributed to agents automatically via symlinks or generated files.
|
|
34
|
+
|
|
35
|
+
To add a new rule, create a \`.md\` file in \`.oneagent/rules/\` with optional frontmatter:
|
|
36
|
+
|
|
37
|
+
\`\`\`md
|
|
38
|
+
---
|
|
39
|
+
applyTo: "**/*.ts"
|
|
40
|
+
---
|
|
41
|
+
# Rule name
|
|
42
|
+
|
|
43
|
+
Rule content here.
|
|
44
|
+
\`\`\`
|
|
45
|
+
|
|
46
|
+
Then run \`dotai generate\` to distribute the rule to all configured agents.
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
function cancelAndExit(): never {
|
|
50
|
+
outro("Cancelled.");
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function chooseContent(detected: DetectedFile[]): Promise<string> {
|
|
55
|
+
if (detected.length === 0) return "";
|
|
56
|
+
|
|
57
|
+
if (detected.length === 1) {
|
|
58
|
+
const file = detected[0]!;
|
|
59
|
+
const result = await confirm({
|
|
60
|
+
message: `Found ${file.relativePath} (${timeAgo(file.modifiedAt)}). Import its content into .oneagent/instructions.md?`,
|
|
61
|
+
});
|
|
62
|
+
if (isCancel(result)) cancelAndExit();
|
|
63
|
+
return result ? file.content : "";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (filesHaveSameContent(detected)) {
|
|
67
|
+
const result = await confirm({
|
|
68
|
+
message: `Found ${detected.length} files with identical content. Import?`,
|
|
69
|
+
});
|
|
70
|
+
if (isCancel(result)) cancelAndExit();
|
|
71
|
+
return result ? detected[0]!.content : "";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Scenario D — multiple files, different content
|
|
75
|
+
note(
|
|
76
|
+
detected.map((f) => ` • ${f.relativePath} ${timeAgo(f.modifiedAt)}`).join("\n"),
|
|
77
|
+
"Multiple files with different content found",
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const result = await select({
|
|
81
|
+
message: "How would you like to handle existing content?",
|
|
82
|
+
options: [
|
|
83
|
+
{ value: "merge", label: "Merge all files", hint: "Combine all content" },
|
|
84
|
+
...detected.map((f, i) => ({
|
|
85
|
+
value: `file:${i}`,
|
|
86
|
+
label: `Use ${f.relativePath}`,
|
|
87
|
+
hint: `${timeAgo(f.modifiedAt)}`,
|
|
88
|
+
})),
|
|
89
|
+
{ value: "skip", label: "Skip", hint: "Start with empty instructions" },
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (isCancel(result)) cancelAndExit();
|
|
94
|
+
|
|
95
|
+
if (result === "skip") return "";
|
|
96
|
+
if (result === "merge") return detected.map((f) => f.content).join("\n\n---\n\n");
|
|
97
|
+
|
|
98
|
+
const index = parseInt((result as string).replace("file:", ""), 10);
|
|
99
|
+
return detected[index]!.content;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function pickTargets(): Promise<AgentTarget[]> {
|
|
103
|
+
const result = await multiselect<AgentTarget>({
|
|
104
|
+
message: `Which AI agents do you want to support?
|
|
105
|
+
\x1b[90m · Space to toggle · Enter to confirm\x1b[39m`,
|
|
106
|
+
options: [
|
|
107
|
+
{ value: "claude", label: "Claude Code", hint: "CLAUDE.md + .claude/rules/" },
|
|
108
|
+
{ value: "cursor", label: "Cursor", hint: "AGENTS.md + .cursor/rules/" },
|
|
109
|
+
{ value: "windsurf", label: "Windsurf", hint: ".windsurfrules + .windsurf/rules/" },
|
|
110
|
+
{ value: "opencode", label: "OpenCode", hint: "AGENTS.md + opencode.json" },
|
|
111
|
+
{ value: "copilot", label: "GitHub Copilot", hint: ".github/instructions/*.instructions.md" },
|
|
112
|
+
],
|
|
113
|
+
initialValues: ["claude"],
|
|
114
|
+
required: true,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (isCancel(result)) cancelAndExit();
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function backupFiles(root: string, files: DetectedFile[]): Promise<void> {
|
|
122
|
+
if (files.length === 0) return;
|
|
123
|
+
const backupDir = path.join(root, ".oneagent/backup");
|
|
124
|
+
await fs.mkdir(backupDir, { recursive: true });
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
const safeName = file.relativePath.replace(/\//g, "_");
|
|
127
|
+
await Bun.write(path.join(backupDir, safeName), file.content);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default defineCommand({
|
|
132
|
+
meta: {
|
|
133
|
+
name: "init",
|
|
134
|
+
description: "Initialize oneagent in the current project",
|
|
135
|
+
},
|
|
136
|
+
async run() {
|
|
137
|
+
intro("oneagent init");
|
|
138
|
+
|
|
139
|
+
const root = process.cwd();
|
|
140
|
+
|
|
141
|
+
if (await configExists(root)) {
|
|
142
|
+
note("Already initialized. Run `oneagent generate` to sync.", "oneagent");
|
|
143
|
+
outro("Done.");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const detected = await detectExistingFiles(root);
|
|
148
|
+
const content = await chooseContent(detected);
|
|
149
|
+
const targets = await pickTargets();
|
|
150
|
+
|
|
151
|
+
const s = spinner();
|
|
152
|
+
s.start("Setting up .oneagent/ directory...");
|
|
153
|
+
|
|
154
|
+
await fs.mkdir(path.join(root, ".oneagent/rules"), { recursive: true });
|
|
155
|
+
await fs.mkdir(path.join(root, ".oneagent/skills"), { recursive: true });
|
|
156
|
+
|
|
157
|
+
await backupFiles(root, detected);
|
|
158
|
+
|
|
159
|
+
const config: Config = { version: 1, targets };
|
|
160
|
+
await writeConfig(root, config);
|
|
161
|
+
|
|
162
|
+
const instructionsContent =
|
|
163
|
+
content.trim() ? content : "# Project Instructions\n\nAdd your AI instructions here.\n";
|
|
164
|
+
await Bun.write(path.join(root, ".oneagent/instructions.md"), instructionsContent);
|
|
165
|
+
await Bun.write(path.join(root, ".oneagent/rules/oneagent.md"), DOTAI_META_RULE);
|
|
166
|
+
|
|
167
|
+
s.stop("Directory structure created.");
|
|
168
|
+
|
|
169
|
+
const s2 = spinner();
|
|
170
|
+
s2.start("Generating symlinks and agent files...");
|
|
171
|
+
await generate(root, config);
|
|
172
|
+
s2.stop("Done.");
|
|
173
|
+
|
|
174
|
+
const lines = [
|
|
175
|
+
"Created .oneagent/instructions.md",
|
|
176
|
+
"Created .oneagent/rules/oneagent.md",
|
|
177
|
+
...targets.map((t) => `Configured: ${t}`),
|
|
178
|
+
...(detected.length > 0
|
|
179
|
+
? [`Backed up ${detected.length} file(s) to .oneagent/backup/`]
|
|
180
|
+
: []),
|
|
181
|
+
];
|
|
182
|
+
note(lines.map((l) => ` • ${l}`).join("\n"), "Setup complete");
|
|
183
|
+
|
|
184
|
+
outro("Run `oneagent status` to verify your setup.");
|
|
185
|
+
},
|
|
186
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { readConfig, checkStatus } from "@moskala/oneagent-core";
|
|
3
|
+
|
|
4
|
+
export default defineCommand({
|
|
5
|
+
meta: {
|
|
6
|
+
name: "status",
|
|
7
|
+
description: "Check status of symlinks and generated files",
|
|
8
|
+
},
|
|
9
|
+
async run() {
|
|
10
|
+
const root = process.cwd();
|
|
11
|
+
|
|
12
|
+
let config;
|
|
13
|
+
try {
|
|
14
|
+
config = await readConfig(root);
|
|
15
|
+
} catch {
|
|
16
|
+
console.error("Error: No .oneagent/config.yml found. Run `oneagent init` first.");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const status = await checkStatus(root, config);
|
|
21
|
+
|
|
22
|
+
console.log("\nSymlinks:");
|
|
23
|
+
for (const s of status.symlinks) {
|
|
24
|
+
const icon = !s.exists ? "✗" : s.valid ? "✓" : "⚠";
|
|
25
|
+
const text = !s.exists ? "missing" : s.valid ? "valid" : "broken (wrong target)";
|
|
26
|
+
console.log(` ${icon} ${s.label} — ${text}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (status.generatedFiles.length > 0) {
|
|
30
|
+
console.log("\nGenerated files (Copilot):");
|
|
31
|
+
for (const f of status.generatedFiles) {
|
|
32
|
+
const icon = !f.exists ? "✗" : f.upToDate ? "✓" : "⚠";
|
|
33
|
+
const text = !f.exists ? "missing" : f.upToDate ? "up to date" : "outdated";
|
|
34
|
+
console.log(` ${icon} ${f.path} — ${text}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (config.targets.includes("opencode")) {
|
|
39
|
+
const { opencode } = status;
|
|
40
|
+
const icon = !opencode.exists ? "✗" : opencode.valid ? "✓" : "⚠";
|
|
41
|
+
const text = !opencode.exists ? "missing" : opencode.valid ? "valid" : "invalid";
|
|
42
|
+
console.log(`\nOpenCode:\n ${icon} opencode.json — ${text}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log();
|
|
46
|
+
},
|
|
47
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { defineCommand, runMain } from "citty";
|
|
3
|
+
|
|
4
|
+
const main = defineCommand({
|
|
5
|
+
meta: {
|
|
6
|
+
name: "oneagent",
|
|
7
|
+
description: "One source of truth for AI agent rules",
|
|
8
|
+
},
|
|
9
|
+
subCommands: {
|
|
10
|
+
init: () => import("./commands/init.ts").then((r) => r.default),
|
|
11
|
+
generate: () => import("./commands/generate.ts").then((r) => r.default),
|
|
12
|
+
status: () => import("./commands/status.ts").then((r) => r.default),
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
runMain(main);
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function timeAgo(date: Date): string {
|
|
2
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
3
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
4
|
+
const minutes = Math.floor(seconds / 60);
|
|
5
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
6
|
+
const hours = Math.floor(minutes / 60);
|
|
7
|
+
if (hours < 24) return `${hours}h ago`;
|
|
8
|
+
const days = Math.floor(hours / 24);
|
|
9
|
+
if (days < 30) return `${days}d ago`;
|
|
10
|
+
const months = Math.floor(days / 30);
|
|
11
|
+
if (months < 12) return `${months}mo ago`;
|
|
12
|
+
return `${Math.floor(months / 12)}y ago`;
|
|
13
|
+
}
|