@sayansr26/agent-os 0.6.2 → 0.6.3
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +24 -0
- package/README.md +26 -2
- package/cursor/hooks/git-guard.mjs +40 -0
- package/cursor/skills/agent-os-init/SKILL.md +116 -0
- package/package.json +4 -4
- package/plugins/agent-os/.claude-plugin/plugin.json +3 -3
- package/plugins/agent-os/hooks/pre-edit.mjs +36 -7
- package/plugins/agent-os/hooks/session-resume.mjs +29 -15
- package/src/cli.mjs +52 -9
- package/src/cursor.mjs +167 -0
- package/src/selftest.mjs +61 -1
- package/src/targets.mjs +18 -11
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,30 @@ All notable changes to this project are documented here.
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
6
6
|
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.6.3] — 2026-10-31
|
|
9
|
+
|
|
10
|
+
agent-os plugin 0.5.1 (hooks gain a Cursor mode; Claude Code behaviour
|
|
11
|
+
unchanged), auto-route unchanged (0.1.0), auto-handoff unchanged (0.8.6).
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **The agent-os layer for Cursor.** With `cursor` as a target, `init` and
|
|
15
|
+
`sync` compile the seven agents to `.cursor/agents/agent-os-<name>.md`, the
|
|
16
|
+
three skills to `/agent-os-init`, `/agent-os-map` and `/agent-os-memory`, the
|
|
17
|
+
session-start and pre-edit hooks plus git write protection to
|
|
18
|
+
`.cursor/hooks/agent-os/` (registered in `.cursor/hooks.json`, merged), and
|
|
19
|
+
the agent contract to an always-on `.cursor/rules/agent-os-agents.mdc`.
|
|
20
|
+
Subagent memory uses `.claude/agent-memory/agent-os-<name>/`, the directories
|
|
21
|
+
Claude Code's plugin agents use. `{ "cursor": { "agentOs": false } }` or
|
|
22
|
+
`{ "cursor": { "gitProtection": false } }` in `.agent-os/config.json` opt out.
|
|
23
|
+
- `session-resume.mjs --cursor` and `pre-edit.mjs --cursor` answer in Cursor's
|
|
24
|
+
hook protocol (`additional_context`; `permission` with an explicit `allow`).
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- **`init` in a project that does not target Claude Code** no longer prints
|
|
28
|
+
Claude Code's SETUP block or ends `fresh → repair` with a to-do list of
|
|
29
|
+
CLAUDE.md, `.claude/settings.json` and plugin items — including one telling
|
|
30
|
+
the user to run `init` again. It now ends with what it compiled for.
|
|
31
|
+
|
|
8
32
|
## [0.6.2] — 2026-10-22
|
|
9
33
|
|
|
10
34
|
agent-os plugin unchanged (0.5.0), auto-route unchanged (0.1.0). New plugin
|
package/README.md
CHANGED
|
@@ -110,14 +110,38 @@ Generated files carry a banner. Edit `.agent-os/`, run `sync`, never edit the ou
|
|
|
110
110
|
|
|
111
111
|
Being straight about this matters more than the feature list.
|
|
112
112
|
|
|
113
|
-
**Hooks don't port.** Every tool differs in events *and* control protocol — Cursor returns JSON `permission`, Windsurf uses exit codes, Claude Code uses JSON. There is no honest common denominator, so `agent-os` doesn't invent one.
|
|
113
|
+
**Hooks don't port generically.** Every tool differs in events *and* control protocol — Cursor returns JSON `permission`, Windsurf uses exit codes, Claude Code uses JSON. There is no honest common denominator, so `agent-os` doesn't invent one for your hooks. Its own hooks are written once per tool: Claude Code (the plugin) and Cursor (see below).
|
|
114
114
|
|
|
115
|
-
**Memory doesn't port.** Only Claude Code (auto memory) and Windsurf have native per-session memory. Kilo deprecated its Memory Bank in favour of `AGENTS.md`; Cline's is a community methodology, not a feature. So the architecture map — the thing that makes *"change the login flow from email to OTP"* run off known structure — works in Claude Code and nowhere else yet.
|
|
115
|
+
**Memory doesn't port generically.** Only Claude Code (auto memory) and Windsurf have native per-session memory. Kilo deprecated its Memory Bank in favour of `AGENTS.md`; Cline's is a community methodology, not a feature. So the architecture map — the thing that makes *"change the login flow from email to OTP"* run off known structure — works in Claude Code and Cursor, whose subagents are told where their memory lives, and nowhere else yet.
|
|
116
116
|
|
|
117
117
|
**Antigravity's rules target is best effort.** Its docs state a rule is "simply a Markdown file" and never show frontmatter; glob activation is configured in the Customizations panel. So `agent-os` writes valid Markdown and states the intended scope in a comment, rather than inventing a `globs:` key that may silently do nothing.
|
|
118
118
|
|
|
119
119
|
---
|
|
120
120
|
|
|
121
|
+
## Cursor
|
|
122
|
+
|
|
123
|
+
When Cursor is a target, `init` and `sync` also compile the agent-os layer into the places Cursor reads it — the same agents, skills and hooks the Claude Code plugin ships:
|
|
124
|
+
|
|
125
|
+
| Claude Code (plugin) | Cursor (generated) |
|
|
126
|
+
|---|---|
|
|
127
|
+
| agents `agent-os:<name>` | subagents `.cursor/agents/agent-os-<name>.md` |
|
|
128
|
+
| `/agent-os:init`, `/agent-os:map`, `/agent-os:memory` | `/agent-os-init`, `/agent-os-map`, `/agent-os-memory` in `.cursor/skills/` |
|
|
129
|
+
| SessionStart hook | `sessionStart` hook → `additional_context` |
|
|
130
|
+
| PreToolUse guard on generated files + cartographer reminder | `preToolUse` hook on `Write` |
|
|
131
|
+
| git write protection in `permissions.deny` | `beforeShellExecution` hook refusing the same git commands |
|
|
132
|
+
| "Agents in this project" section of `CLAUDE.md` | `.cursor/rules/agent-os-agents.mdc`, always applied |
|
|
133
|
+
| per-agent memory, `.claude/agent-memory/agent-os-<name>/` | the same directories — one map for both tools |
|
|
134
|
+
|
|
135
|
+
Hook scripts live in `.cursor/hooks/agent-os/` and are registered in `.cursor/hooks.json`, merged so your own hooks stay. Everything carries the generated banner, so `check` catches drift and the pre-edit hook blocks hand edits; `sync` after upgrading the package is the Cursor equivalent of `claude plugin update`. Reload the Cursor window after `init`, then run `/agent-os-init` in Agent chat.
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{ "targets": ["cursor"], "cursor": { "agentOs": true, "gitProtection": true } }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`agentOs: false` stops generating the layer and removes its hooks from `hooks.json` (delete the `agent-os-*` files yourself); `gitProtection: false` drops only the git hook. What does not carry over: Claude Code's auto memory, task tools (Cursor has its own to-do list), and the auto-route and auto-handoff mods.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
121
145
|
## The Claude Code plugin
|
|
122
146
|
|
|
123
147
|
The deeper context-engineering work — the architecture map, seven coordinated agents, per-agent memory, and the hooks that make Claude use them — ships as a Claude Code plugin in this repo:
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Cursor beforeShellExecution hook — git write protection.
|
|
4
|
+
*
|
|
5
|
+
* Claude Code gets this as `permissions.deny` rules in .claude/settings.json.
|
|
6
|
+
* Cursor has no deny list, so the same rules (GIT_DENY, generated next to this
|
|
7
|
+
* file from settings.mjs) are enforced here: any git command that changes the
|
|
8
|
+
* repository is denied, read-only inspection is allowed.
|
|
9
|
+
*
|
|
10
|
+
* The hook is registered with a `git` matcher, so it only sees commands that
|
|
11
|
+
* mention git. Cursor reads a missing or invalid response as a block, so every
|
|
12
|
+
* exit answers explicitly.
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
import { GIT_DENY } from "../lib/git-deny.mjs";
|
|
16
|
+
|
|
17
|
+
const answer = (o) => { process.stdout.write(JSON.stringify(o)); process.exit(0); };
|
|
18
|
+
|
|
19
|
+
// "Bash(git push *)" -> "git push" plus anything after a space;
|
|
20
|
+
// "Bash(git -C*)" -> anything starting "git -C".
|
|
21
|
+
const RULES = GIT_DENY.map((r) => r.replace(/^Bash\(/, "").replace(/\)$/, "")).map((r) =>
|
|
22
|
+
r.endsWith(" *") ? { prefix: r.slice(0, -2), spaced: true } : { prefix: r.replace(/\*$/, ""), spaced: false });
|
|
23
|
+
|
|
24
|
+
const denied = (segment) => {
|
|
25
|
+
const cmd = segment.trim().replace(/\s+/g, " ").replace(/^(?:\w+=\S*\s+)+/, "");
|
|
26
|
+
return RULES.find((r) => r.spaced ? cmd === r.prefix || cmd.startsWith(`${r.prefix} `) : cmd.startsWith(r.prefix));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let command = "";
|
|
30
|
+
try { command = String(JSON.parse(readFileSync(0, "utf8") || "{}").command ?? ""); } catch { answer({ permission: "allow" }); }
|
|
31
|
+
|
|
32
|
+
// Check every command in a chain: `cd x && git push` is still a push.
|
|
33
|
+
for (const segment of command.split(/&&|\|\||[;|\n]|\$\(|`/)) {
|
|
34
|
+
const hit = denied(segment);
|
|
35
|
+
if (!hit) continue;
|
|
36
|
+
const text = `agent-os blocks git commands that change the repository (${hit.prefix}${hit.spaced ? " …" : "…"}). ` +
|
|
37
|
+
"Read-only git (status, log, diff, show, blame) is allowed. Ask the user to run this one themselves.";
|
|
38
|
+
answer({ permission: "deny", user_message: text, agent_message: text });
|
|
39
|
+
}
|
|
40
|
+
answer({ permission: "allow" });
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-os-init
|
|
3
|
+
description: Set up or repair this project's agent-os context layer in Cursor. Checks the generated rules, subagents and hooks against .agent-os/, inspects agent memory, then establishes, maps, migrates or maintains the layer. Use for "set up agent-os", "agent-os init", "initialise this project", "bootstrap my context setup", "migrate off memory-bank or .cursorrules", "my AGENTS.md is too big", or when starting work in a repo with no setup.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# agent-os for Cursor — set up or repair
|
|
7
|
+
|
|
8
|
+
The Cursor counterpart of Claude Code's `/agent-os:init`. Same layout, same
|
|
9
|
+
agents, same memory — only the file names differ.
|
|
10
|
+
|
|
11
|
+
| Tier | Lives in | Startup cost | Holds |
|
|
12
|
+
|---|---|---|---|
|
|
13
|
+
| Always-loaded | `AGENTS.md` (from `.agent-os/AGENTS.md`), ≤200 lines | small, fixed | Only what is true in *every* session |
|
|
14
|
+
| On-demand | `.cursor/rules/*.mdc` with `globs:` (from `.agent-os/rules/*.md` with `paths:`) | **zero** | Durable facts scoped to some files |
|
|
15
|
+
| Explored | `.claude/agent-memory/agent-os-feature-cartographer/` | **zero** (subagent context) | How each feature is actually built |
|
|
16
|
+
| Task state | the project's own vault / handoff file | zero (read selectively) | What is in flight right now |
|
|
17
|
+
|
|
18
|
+
Agent memory stays under `.claude/agent-memory/` on purpose: Claude Code's
|
|
19
|
+
agent-os agents use the same directories, so a team on both tools shares one map.
|
|
20
|
+
|
|
21
|
+
**Storing a fact and loading a fact are different acts.** Anything that makes
|
|
22
|
+
them the same act — a handbook `AGENTS.md`, a memory bank read at every startup,
|
|
23
|
+
an always-apply rule that only matters for one directory — is the bug.
|
|
24
|
+
|
|
25
|
+
## Arguments
|
|
26
|
+
|
|
27
|
+
| Argument | Do |
|
|
28
|
+
|---|---|
|
|
29
|
+
| *(none)* | The full pass below |
|
|
30
|
+
| `audit` | Steps 1 and 3 only. Change nothing. |
|
|
31
|
+
|
|
32
|
+
`/agent-os-map` builds the architecture map; `/agent-os-memory` inspects and
|
|
33
|
+
repairs what the project remembers.
|
|
34
|
+
|
|
35
|
+
## Step 1 — Read the state
|
|
36
|
+
|
|
37
|
+
Three commands. Do not rediscover this with a dozen file reads — they are
|
|
38
|
+
deterministic:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx -y @sayansr26/agent-os detect
|
|
42
|
+
npx -y @sayansr26/agent-os check
|
|
43
|
+
npx -y @sayansr26/agent-os memory
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`check` lists every generated file — rules, `AGENTS.md`, the `agent-os-*`
|
|
47
|
+
subagents and skills, the hook scripts and `.cursor/hooks.json` — as `ok`,
|
|
48
|
+
`MISSING` or `DRIFTED`. `memory` lists every agent's memory, which features are
|
|
49
|
+
mapped, and a health list.
|
|
50
|
+
|
|
51
|
+
**Show the output to the user before you change anything.**
|
|
52
|
+
|
|
53
|
+
## Step 2 — Fresh setup or repair?
|
|
54
|
+
|
|
55
|
+
| State | Do |
|
|
56
|
+
|---|---|
|
|
57
|
+
| no `.agent-os/` | `npx -y @sayansr26/agent-os init`, then carry on with Step 3 |
|
|
58
|
+
| `check` reports `MISSING` or `DRIFTED` | `npx -y @sayansr26/agent-os sync` — it rewrites only what it generated |
|
|
59
|
+
| `sync` names files it did not generate | stop and ask: move their content into `.agent-os/` (preferred), or `--force` after a commit |
|
|
60
|
+
| everything `ok` | nothing to repair |
|
|
61
|
+
|
|
62
|
+
Never edit a generated file by hand. They carry a banner, the pre-edit hook
|
|
63
|
+
blocks the edit, and the next `sync` would revert it. Edit `.agent-os/` and sync.
|
|
64
|
+
|
|
65
|
+
## Step 3 — Decide what this run is for
|
|
66
|
+
|
|
67
|
+
Judge from what Step 1 printed and a look at the source tree:
|
|
68
|
+
|
|
69
|
+
| Mode | Evidence | Do |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `TOO-EARLY` | Barely any source (under ~5 files) | **Build nothing.** A layer over an empty project is invented conventions. |
|
|
72
|
+
| `ESTABLISH` | `.agent-os/AGENTS.md` is still the placeholder, or the only rule is `example.md` | `references/establishing.md` — build the layer *from the code* |
|
|
73
|
+
| `MAP` | Layer real, but `memory` shows no `_architecture.md` | `references/establishing.md`, "Map the architecture" — or `/agent-os-map` |
|
|
74
|
+
| `MIGRATE` | `.cursorrules`, `memory-bank/`, `.serena/memories/`, or an always-apply rule that only concerns part of the tree | fix it; `references/migrating.md` for legacy stores |
|
|
75
|
+
| `MAINTAIN` | Real layer, architecture mapped | report and stop |
|
|
76
|
+
|
|
77
|
+
`references/changing-a-feature.md` is the workflow that pays all this back —
|
|
78
|
+
point the user at it once the layer exists.
|
|
79
|
+
|
|
80
|
+
The references were written for Claude Code. Read them through this table:
|
|
81
|
+
|
|
82
|
+
| They say | In Cursor |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `CLAUDE.md` | `.agent-os/AGENTS.md` (compiled to `AGENTS.md`). Draft it yourself from the code — there is no `/init` to run first. |
|
|
85
|
+
| `.claude/rules/*.md` | `.agent-os/rules/*.md`, then `sync` (compiled to `.cursor/rules/*.mdc`) |
|
|
86
|
+
| `agent-os:<name>` | the `agent-os-<name>` subagent |
|
|
87
|
+
| `/agent-os:init`, `/agent-os:map`, `/agent-os:memory` | `/agent-os-init`, `/agent-os-map`, `/agent-os-memory` |
|
|
88
|
+
| `node "${CLAUDE_PLUGIN_ROOT}/…/<script>.mjs"` | `npx -y @sayansr26/agent-os <script>` |
|
|
89
|
+
| "Wire the agents into CLAUDE.md" (Step 4b) | already done: `.cursor/rules/agent-os-agents.mdc` is always applied — skip it |
|
|
90
|
+
| the settings pass, `permissions.deny`, task tools | see "Git write protection" below; Cursor has its own to-do list |
|
|
91
|
+
|
|
92
|
+
## Step 4 — Verify by re-running
|
|
93
|
+
|
|
94
|
+
Run `check` and `memory` again and show before and after: generated files all
|
|
95
|
+
`ok`, features mapped, health findings. Do not declare success on vibes — the
|
|
96
|
+
commands produce the numbers, so quote them.
|
|
97
|
+
|
|
98
|
+
## Git write protection
|
|
99
|
+
|
|
100
|
+
Claude Code gets git write protection as `permissions.deny` rules. Cursor has no
|
|
101
|
+
deny list, so `sync` registers a `beforeShellExecution` hook in
|
|
102
|
+
`.cursor/hooks.json` (`.cursor/hooks/agent-os/hooks/git-guard.mjs`) that refuses
|
|
103
|
+
the same commands: every git command that changes the repository. Read-only git
|
|
104
|
+
stays available. `references/git-permissions.md` explains each rule.
|
|
105
|
+
|
|
106
|
+
If the user wants it off: `{ "cursor": { "gitProtection": false } }` in
|
|
107
|
+
`.agent-os/config.json`, then `sync`. Say what that gives up.
|
|
108
|
+
|
|
109
|
+
## What this skill will not do
|
|
110
|
+
|
|
111
|
+
- It will not write facts it has not verified in the repo. An invented rule is
|
|
112
|
+
worse than a missing one, because it will be trusted.
|
|
113
|
+
- It will not delete a legacy store before its content has landed in `.agent-os/`.
|
|
114
|
+
- It will not hand-edit generated files, or another agent's memory.
|
|
115
|
+
- It will not add a memory MCP server. The common failure is that nothing was
|
|
116
|
+
*written*, which a graph does not fix and the cartographer's memory does.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sayansr26/agent-os",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "One source of truth for AI coding agent config. Write your rules once; compile them to Claude Code, Cursor, Cline, Windsurf, Antigravity, Gemini CLI, OpenCode and Kilo.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"bin",
|
|
35
35
|
"src",
|
|
36
36
|
"plugins",
|
|
37
|
+
"cursor",
|
|
37
38
|
".claude-plugin",
|
|
38
39
|
"README.md",
|
|
39
40
|
"LICENSE",
|
|
@@ -43,7 +44,6 @@
|
|
|
43
44
|
"access": "public"
|
|
44
45
|
},
|
|
45
46
|
"scripts": {
|
|
46
|
-
"test": "node scripts/validate-plugin.mjs && node src/selftest.mjs"
|
|
47
|
-
"prepublishOnly": "npm test"
|
|
47
|
+
"test": "node scripts/validate-plugin.mjs && node src/selftest.mjs"
|
|
48
48
|
}
|
|
49
|
-
}
|
|
49
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-os",
|
|
3
3
|
"description": "A context-engineered agent system for Claude Code. Ships a durable per-project memory layer, a cartographer that maps your codebase once and remembers it, and a coordinated agent set that reads your project's rules rather than hardcoding them.",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Sayan Choudhury",
|
|
7
7
|
"email": "sayan.choudhury.in@gmail.com"
|
|
8
8
|
},
|
|
9
9
|
"homepage": "https://github.com/sayansr26/agent-os",
|
|
10
10
|
"repository": "https://github.com/sayansr26/agent-os",
|
|
11
|
-
"privacyPolicyUrl": "https://
|
|
12
|
-
"supportUrl": "https://
|
|
11
|
+
"privacyPolicyUrl": "https://github.com/sayansr26/agent-os/blob/master/PRIVACY.md",
|
|
12
|
+
"supportUrl": "https://github.com/sayansr26/agent-os/issues",
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"keywords": [
|
|
15
15
|
"context-engineering",
|
|
@@ -10,11 +10,16 @@
|
|
|
10
10
|
* Deny, and name the source to edit instead.
|
|
11
11
|
* 2. Cartographer reminder (advisory, never blocks). The first edit to an
|
|
12
12
|
* existing feature with no cartographer map adds a note telling Claude to
|
|
13
|
-
* ask agent-os:feature-cartographer first
|
|
13
|
+
* ask agent-os:feature-cartographer first (agent-os-feature-cartographer
|
|
14
|
+
* in Cursor). Once per feature per session.
|
|
14
15
|
*
|
|
15
16
|
* Stays silent for new (untracked) features, paths outside a feature
|
|
16
17
|
* directory, and anything it cannot work out. Turn the reminder off with
|
|
17
18
|
* `{ "claude": { "cartographerReminder": false } }` in `.agent-os/config.json`.
|
|
19
|
+
*
|
|
20
|
+
* `--cursor` is the same hook as Cursor's preToolUse on Write. Cursor reads a
|
|
21
|
+
* missing or empty response as a block, so in that mode every exit answers
|
|
22
|
+
* explicitly: `allow` (with the reminder as `agent_message`) or `deny`.
|
|
18
23
|
*/
|
|
19
24
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
25
|
import { tmpdir } from "node:os";
|
|
@@ -24,17 +29,33 @@ import { claudeConfig, featureOf, findMap, mapFiles, tracked } from "../lib/feat
|
|
|
24
29
|
// Must stay a substring of BANNER in src/source.mjs.
|
|
25
30
|
const BANNER = "agent-os: generated from .agent-os/";
|
|
26
31
|
|
|
27
|
-
const
|
|
32
|
+
const CURSOR = process.argv.includes("--cursor");
|
|
33
|
+
const agent = (name) => `agent-os${CURSOR ? "-" : ":"}${name}`;
|
|
34
|
+
|
|
35
|
+
const quiet = () => {
|
|
36
|
+
if (CURSOR) process.stdout.write(JSON.stringify({ permission: "allow" }));
|
|
37
|
+
process.exit(0);
|
|
38
|
+
};
|
|
28
39
|
const emit = (hookSpecificOutput) => {
|
|
29
|
-
|
|
40
|
+
if (CURSOR) {
|
|
41
|
+
const deny = hookSpecificOutput.permissionDecision === "deny";
|
|
42
|
+
const text = deny ? hookSpecificOutput.permissionDecisionReason : hookSpecificOutput.additionalContext;
|
|
43
|
+
process.stdout.write(JSON.stringify(deny
|
|
44
|
+
? { permission: "deny", user_message: text, agent_message: text }
|
|
45
|
+
: { permission: "allow", agent_message: text }));
|
|
46
|
+
} else
|
|
47
|
+
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", ...hookSpecificOutput } }));
|
|
30
48
|
process.exit(0);
|
|
31
49
|
};
|
|
32
50
|
|
|
33
51
|
let payload;
|
|
34
52
|
try { payload = JSON.parse(readFileSync(0, "utf8") || "{}"); } catch { quiet(); }
|
|
35
53
|
|
|
36
|
-
|
|
37
|
-
const
|
|
54
|
+
// Claude Code may run inside Cursor's terminal, so CURSOR_PROJECT_DIR counts only in --cursor mode.
|
|
55
|
+
const root = (CURSOR && process.env.CURSOR_PROJECT_DIR) || process.env.CLAUDE_PROJECT_DIR || payload?.cwd || process.cwd();
|
|
56
|
+
// Cursor does not document its Write tool's input, so take the usual names.
|
|
57
|
+
const input = payload?.tool_input || {};
|
|
58
|
+
const raw = String(input.file_path ?? input.path ?? input.target_file ?? input.filePath ?? "");
|
|
38
59
|
if (!raw) quiet();
|
|
39
60
|
const abs = isAbsolute(raw) ? raw : join(root, raw);
|
|
40
61
|
const rel = relative(root, abs).replace(/\\/g, "/");
|
|
@@ -44,6 +65,14 @@ if (rel.startsWith("..")) quiet();
|
|
|
44
65
|
try {
|
|
45
66
|
if (existsSync(join(root, ".agent-os")) && existsSync(abs) && readFileSync(abs, "utf8").includes(BANNER)) {
|
|
46
67
|
let source = ".agent-os/";
|
|
68
|
+
// Cursor's agent-os layer is compiled from the package, not from .agent-os/.
|
|
69
|
+
if (/^\.cursor\/(?:agents\/agent-os-|skills\/agent-os-|hooks\/agent-os\/|rules\/agent-os-agents\.mdc$)/.test(rel))
|
|
70
|
+
emit({
|
|
71
|
+
permissionDecision: "deny",
|
|
72
|
+
permissionDecisionReason:
|
|
73
|
+
`${rel} is part of the agent-os layer for Cursor and the next sync would revert this edit. ` +
|
|
74
|
+
`To customise it, set \`{ "cursor": { "agentOs": false } }\` in .agent-os/config.json and keep your own copy.`,
|
|
75
|
+
});
|
|
47
76
|
const rule = rel.match(/^\.(?:claude|clinerules|cursor|windsurf|kilocode|gemini|agents?)\/(?:rules\/)?(.+?)\.(?:md|mdc)$/);
|
|
48
77
|
if (rel === "AGENTS.md") source = ".agent-os/AGENTS.md";
|
|
49
78
|
else if (rule && existsSync(join(root, ".agent-os/rules", `${basename(rule[1])}.md`)))
|
|
@@ -67,7 +96,7 @@ try {
|
|
|
67
96
|
|
|
68
97
|
const dir = join(tmpdir(), "agent-os-reminders");
|
|
69
98
|
mkdirSync(dir, { recursive: true });
|
|
70
|
-
const session = String(payload?.session_id ?? "nosession").replace(/[^\w-]/g, "");
|
|
99
|
+
const session = String(payload?.session_id ?? payload?.conversation_id ?? "nosession").replace(/[^\w-]/g, "");
|
|
71
100
|
const marker = join(dir, `${session}--${feature.dir.replace(/[^\w-]/g, "_")}`);
|
|
72
101
|
if (existsSync(marker)) quiet();
|
|
73
102
|
writeFileSync(marker, "");
|
|
@@ -75,7 +104,7 @@ try {
|
|
|
75
104
|
emit({
|
|
76
105
|
additionalContext:
|
|
77
106
|
`agent-os (advisory): ${feature.dir}/ is an existing feature with no cartographer map. ` +
|
|
78
|
-
`Before changing it further, ask agent
|
|
107
|
+
`Before changing it further, ask ${agent("feature-cartographer")} how "${feature.name}" is built — ` +
|
|
79
108
|
`it answers and files the map for every later session. This applies in plan mode too ` +
|
|
80
109
|
`(ask it read-only; it writes the map after plan mode ends). Shown once per feature per session.`,
|
|
81
110
|
});
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
* - The snapshot is hard-capped (MAX_LINES); the contract is fixed-size.
|
|
17
17
|
* - Fails silent: any error exits 0, so a broken hook never blocks a session.
|
|
18
18
|
* - Read-only. Runs git for reading only; never writes, never mutates.
|
|
19
|
+
*
|
|
20
|
+
* `--cursor` is the same hook as Cursor's sessionStart: agent names in Cursor's
|
|
21
|
+
* `agent-os-<name>` form, and the text wrapped as `{ additional_context }`.
|
|
19
22
|
*/
|
|
20
23
|
|
|
21
24
|
import { execSync } from "node:child_process";
|
|
@@ -24,7 +27,13 @@ import { join } from "node:path";
|
|
|
24
27
|
import { coverage } from "../lib/features.mjs";
|
|
25
28
|
|
|
26
29
|
const MAX_LINES = 40;
|
|
27
|
-
const
|
|
30
|
+
const CURSOR = process.argv.includes("--cursor");
|
|
31
|
+
// Claude Code may run inside Cursor's terminal, so CURSOR_PROJECT_DIR counts only in --cursor mode.
|
|
32
|
+
const cwd = (CURSOR && process.env.CURSOR_PROJECT_DIR) || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
33
|
+
// Claude Code namespaces plugin agents and skills with `:`; Cursor names allow
|
|
34
|
+
// only letters and hyphens, so its generated copies use `-`.
|
|
35
|
+
const agent = (name) => `agent-os${CURSOR ? "-" : ":"}${name}`;
|
|
36
|
+
const skill = (name) => `/agent-os${CURSOR ? "-" : ":"}${name}`;
|
|
28
37
|
const out = [];
|
|
29
38
|
|
|
30
39
|
const sh = (cmd) => {
|
|
@@ -135,13 +144,13 @@ function snapshot() {
|
|
|
135
144
|
|
|
136
145
|
// The agents and what triggers each. `subagent_type` is the namespaced name.
|
|
137
146
|
const AGENTS = [
|
|
138
|
-
["
|
|
139
|
-
["
|
|
140
|
-
["
|
|
141
|
-
["
|
|
142
|
-
["
|
|
143
|
-
["
|
|
144
|
-
["
|
|
147
|
+
["feature-cartographer", "BEFORE changing any existing feature — ask how it is built (files, state, API, blast radius). It answers from its map, or maps the feature and files the map."],
|
|
148
|
+
["architect", "before a new subsystem, a cross-module change, or a data model others will depend on."],
|
|
149
|
+
["builder", "to write the code once the shape is settled; it enforces the project's rules while writing."],
|
|
150
|
+
["tester", "after building, to verify the change actually works."],
|
|
151
|
+
["reviewer", "before calling any change done or opening a PR."],
|
|
152
|
+
["documenter", "once work is verified, to update the changelog, task state and docs the change invalidated."],
|
|
153
|
+
["orchestrator", "for work spanning several of the above, or that cannot be stated in one sentence."],
|
|
145
154
|
];
|
|
146
155
|
|
|
147
156
|
function contract() {
|
|
@@ -153,15 +162,17 @@ function contract() {
|
|
|
153
162
|
return [
|
|
154
163
|
"## agent-os",
|
|
155
164
|
"",
|
|
156
|
-
|
|
165
|
+
`agent-os is installed but this project is not set up. If the user starts feature work, suggest \`${skill("init")}\` once.`,
|
|
157
166
|
];
|
|
158
167
|
|
|
159
168
|
const lines = [
|
|
160
169
|
"## agent-os is active in this project",
|
|
161
170
|
"",
|
|
162
|
-
|
|
171
|
+
CURSOR
|
|
172
|
+
? "These subagents are how work is done here — delegate to them without being asked, by the names shown:"
|
|
173
|
+
: "These agents are how work is done here — use them without being asked (Agent tool, `subagent_type` as shown):",
|
|
163
174
|
];
|
|
164
|
-
for (const [name, when] of AGENTS) lines.push(`- \`${name}\` — ${when}`);
|
|
175
|
+
for (const [name, when] of AGENTS) lines.push(`- \`${agent(name)}\` — ${when}`);
|
|
165
176
|
|
|
166
177
|
const cov = coverage(cwd);
|
|
167
178
|
lines.push("");
|
|
@@ -173,12 +184,14 @@ function contract() {
|
|
|
173
184
|
} else if (cov.dir) {
|
|
174
185
|
lines.push(`Cartographer maps are indexed in \`${cov.dir.slice(cwd.length + 1)}/MEMORY.md\`.`);
|
|
175
186
|
}
|
|
176
|
-
if (!cov.architecture) lines.push(
|
|
187
|
+
if (!cov.architecture) lines.push(`No architecture map yet — \`${skill("map")}\` builds it.`);
|
|
177
188
|
|
|
178
189
|
lines.push("");
|
|
179
|
-
lines.push(
|
|
190
|
+
lines.push(CURSOR
|
|
191
|
+
? "Plan mode: ask the cartographer read-only during planning (it will not write), and let it file its map once plan mode ends."
|
|
192
|
+
: "Plan mode: its \"Explore agents only\" phase does not replace the cartographer. Ask the cartographer read-only during planning (it will not write), and let it file its map once plan mode ends.");
|
|
180
193
|
if (existsSync(join(cwd, ".agent-os/rules")))
|
|
181
|
-
lines.push(
|
|
194
|
+
lines.push(`Rules: \`${CURSOR ? ".cursor/rules/*" : ".claude/rules/*"}\` and other tool copies are generated. Edit \`.agent-os/rules/\`, then run \`npx @sayansr26/agent-os sync\`.`);
|
|
182
195
|
return lines;
|
|
183
196
|
}
|
|
184
197
|
|
|
@@ -186,7 +199,8 @@ try {
|
|
|
186
199
|
const blocks = [];
|
|
187
200
|
try { const s = snapshot(); if (s.length) blocks.push(s.join("\n")); } catch {}
|
|
188
201
|
try { blocks.push(contract().join("\n")); } catch {}
|
|
189
|
-
if (
|
|
202
|
+
if (CURSOR) process.stdout.write(JSON.stringify(blocks.length ? { additional_context: blocks.join("\n\n") } : {}));
|
|
203
|
+
else if (blocks.length) process.stdout.write(blocks.join("\n\n") + "\n");
|
|
190
204
|
} catch {
|
|
191
205
|
// Never let a resume hook break a session.
|
|
192
206
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { setupState, describeState } from "../plugins/agent-os/skills/init/scrip
|
|
|
10
10
|
import { applyScope, describe } from "./claude-setup.mjs";
|
|
11
11
|
import { createInterface } from "node:readline/promises";
|
|
12
12
|
import { compile, TARGETS } from "./targets.mjs";
|
|
13
|
+
import { enabled as cursorEnabled } from "./cursor.mjs";
|
|
13
14
|
|
|
14
15
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
15
16
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
@@ -42,6 +43,16 @@ Options
|
|
|
42
43
|
--no-global init: leave ~/.claude alone
|
|
43
44
|
`;
|
|
44
45
|
|
|
46
|
+
/** Will this init/sync compile for Claude Code? Mirrors how scaffold picks targets. */
|
|
47
|
+
function claudeTargeted(root, found) {
|
|
48
|
+
try {
|
|
49
|
+
const cfg = JSON.parse(readFileSync(join(root, DIR, "config.json"), "utf8"));
|
|
50
|
+
return (cfg.targets || []).includes("claude-code");
|
|
51
|
+
} catch {}
|
|
52
|
+
const present = found.filter((t) => t.present).map((t) => t.id);
|
|
53
|
+
return present.includes("claude-code") || !present.length;
|
|
54
|
+
}
|
|
55
|
+
|
|
45
56
|
function scaffold(root, found) {
|
|
46
57
|
const present = found.filter((t) => t.present).map((t) => t.id);
|
|
47
58
|
mkdirSync(join(root, DIR, "rules"), { recursive: true });
|
|
@@ -95,6 +106,17 @@ export async function main(argv) {
|
|
|
95
106
|
|
|
96
107
|
if (cmd === "help" || argv.includes("-h") || argv.includes("--help")) { console.log(USAGE); return; }
|
|
97
108
|
|
|
109
|
+
// A repository Rulegate owns: init would adopt Rulegate's generated CLAUDE.md and
|
|
110
|
+
// AGENTS.md as this tool's source, and every write after that fights `rulegate check`.
|
|
111
|
+
// That happened to Rulegate's own repository (its T112), so both writers stop here.
|
|
112
|
+
if ((cmd === "init" || cmd === "sync") && existsSync(join(root, ".rulegate"))) {
|
|
113
|
+
console.error(`\n This repository uses Rulegate (.rulegate/ exists), which replaces agent-os.`);
|
|
114
|
+
console.error(` agent-os ${cmd} would treat Rulegate's generated files as its source. Nothing was written.`);
|
|
115
|
+
console.error(`\n Use ${bold("npx rulegate sync")} here instead.\n`);
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
98
120
|
const found = detect(root);
|
|
99
121
|
|
|
100
122
|
if (cmd === "detect") {
|
|
@@ -141,18 +163,24 @@ export async function main(argv) {
|
|
|
141
163
|
let adopted = new Set();
|
|
142
164
|
|
|
143
165
|
let before = null;
|
|
166
|
+
// The SETUP state is Claude Code's: CLAUDE.md, .claude/settings.json, the
|
|
167
|
+
// plugin. In a project that does not target Claude Code every item reads
|
|
168
|
+
// MISSING and the verdict never leaves REPAIR, so it is not shown there.
|
|
169
|
+
const forClaude = claudeTargeted(root, found);
|
|
144
170
|
if (cmd === "init") {
|
|
145
171
|
console.log(`\n${bold("agent-os init")} ${root}\n`);
|
|
146
172
|
// Decide fresh vs repair before touching anything, and say which, so a
|
|
147
173
|
// second run on a set-up project reads as a repair rather than a re-init.
|
|
148
174
|
before = setupState(root);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
175
|
+
if (forClaude) {
|
|
176
|
+
for (const l of describeState(before)) console.log(` ${l}`);
|
|
177
|
+
console.log({
|
|
178
|
+
fresh: `\n ${dim("Fresh setup: creating everything.")}`,
|
|
179
|
+
repair: `\n ${dim("Existing setup: fixing the items marked MISSING, leaving the rest as it is.")}`,
|
|
180
|
+
healthy: `\n ${dim("Setup is complete: re-syncing and checking the plugin for updates.")}`,
|
|
181
|
+
}[before.status]);
|
|
182
|
+
console.log("");
|
|
183
|
+
}
|
|
156
184
|
console.log(summarise(found));
|
|
157
185
|
const hadSource = existsSync(join(root, DIR));
|
|
158
186
|
const { present, taken } = scaffold(root, found);
|
|
@@ -209,7 +237,7 @@ export async function main(argv) {
|
|
|
209
237
|
const cur = readIfExists(root, f.path);
|
|
210
238
|
if (cur === null) return true; // nothing there yet
|
|
211
239
|
if (cur.includes(BANNER)) return true; // we wrote it
|
|
212
|
-
if (
|
|
240
|
+
if (f.merged) return true; // merged in place, nothing lost
|
|
213
241
|
if (adopted.has(f.path)) return true; // init just took this as the source
|
|
214
242
|
if (matches(root, f.path, f.content) === true) return true; // already identical
|
|
215
243
|
return false;
|
|
@@ -290,7 +318,22 @@ Pick one:
|
|
|
290
318
|
console.log(`\n ${dim("--no-plugin skips this; --no-auto-route / --no-auto-handoff skip one mod")}`);
|
|
291
319
|
}
|
|
292
320
|
|
|
293
|
-
if (cmd
|
|
321
|
+
if (cmd !== "check" && targets.includes("cursor") && cursorEnabled(src.config)) {
|
|
322
|
+
const n = (re) => files.filter((f) => re.test(f.path)).length;
|
|
323
|
+
console.log(`\n${bold("Cursor")} ${dim("agent-os layer, compiled from this package")}\n`);
|
|
324
|
+
console.log(` ${n(/^\.cursor\/agents\/agent-os-/)} subagents agent-os-feature-cartographer, -architect, -builder, -tester, -reviewer, …`);
|
|
325
|
+
console.log(` ${n(/^\.cursor\/skills\/agent-os-[^/]+\/SKILL\.md$/)} skills /agent-os-init, /agent-os-map, /agent-os-memory`);
|
|
326
|
+
console.log(` hooks session start, generated-file guard${src.config.cursor?.gitProtection === false ? "" : ", git write protection"} ${dim("(.cursor/hooks.json)")}`);
|
|
327
|
+
console.log(` agent memory .claude/agent-memory/agent-os-<agent>/ ${dim("— shared with Claude Code")}`);
|
|
328
|
+
if (cmd === "init") console.log(`\n ${dim("Reload the Cursor window, then run /agent-os-init in Agent chat.")}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (cmd === "init" && !dry && !forClaude) {
|
|
332
|
+
const labels = targets.map((t) => TARGETS[t]?.label || t);
|
|
333
|
+
console.log(`\n${bold("Result")} ${labels.length ? `compiled for ${labels.join(", ")}` : "AGENTS.md only"}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (cmd === "init" && !dry && forClaude) {
|
|
294
337
|
const after = setupState(root);
|
|
295
338
|
console.log(`\n${bold("Result")} ${before.status} → ${after.status}`);
|
|
296
339
|
if (after.missing.length) {
|
package/src/cursor.mjs
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent-os layer for Cursor: the same agents, skills and hooks the Claude
|
|
3
|
+
* Code plugin ships, compiled into the places Cursor reads them.
|
|
4
|
+
*
|
|
5
|
+
* .cursor/agents/agent-os-<name>.md the seven agents, as subagents
|
|
6
|
+
* .cursor/skills/agent-os-{init,map,memory}/ the three skills
|
|
7
|
+
* .cursor/hooks/agent-os/ the session and pre-edit hooks, plus
|
|
8
|
+
* git write protection
|
|
9
|
+
* .cursor/hooks.json merged: user hooks are kept
|
|
10
|
+
* .cursor/rules/agent-os-agents.mdc when to use which agent, always on
|
|
11
|
+
*
|
|
12
|
+
* Everything is generated from this package, never from `.agent-os/`, so `sync`
|
|
13
|
+
* keeps it current the way `claude plugin update` does for Claude Code.
|
|
14
|
+
*
|
|
15
|
+
* Agent memory stays in `.claude/agent-memory/agent-os-<name>/`: the directory
|
|
16
|
+
* Claude Code's plugin agents use, so a project on both tools has one map.
|
|
17
|
+
*/
|
|
18
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { BANNER, parseFrontmatter } from "./source.mjs";
|
|
22
|
+
import { GIT_DENY } from "../plugins/agent-os/skills/init/scripts/settings.mjs";
|
|
23
|
+
|
|
24
|
+
const PKG = join(fileURLToPath(new URL(".", import.meta.url)), "..");
|
|
25
|
+
const PLUGIN = join(PKG, "plugins/agent-os");
|
|
26
|
+
const OWN = join(PKG, "cursor");
|
|
27
|
+
const HOOKS = ".cursor/hooks/agent-os";
|
|
28
|
+
|
|
29
|
+
const read = (p) => readFileSync(p, "utf8");
|
|
30
|
+
const md = (fields, body) => `---\n${fields}---\n\n<!-- ${BANNER} -->\n\n${body.trim()}\n`;
|
|
31
|
+
const js = (text) => text.startsWith("#!")
|
|
32
|
+
? text.replace(/^(#!.*\n)/, `$1// ${BANNER}\n`)
|
|
33
|
+
: `// ${BANNER}\n${text}`;
|
|
34
|
+
|
|
35
|
+
/** `{ "cursor": { "agentOs": false } }` turns the whole layer off. */
|
|
36
|
+
export const enabled = (config) => config?.cursor?.agentOs !== false;
|
|
37
|
+
const gitGuard = (config) => config?.cursor?.gitProtection !== false;
|
|
38
|
+
|
|
39
|
+
/** Claude Code names and commands, rewritten to what this layer installs. */
|
|
40
|
+
export function translate(text) {
|
|
41
|
+
return text
|
|
42
|
+
.replace(/agent-os:([a-z][a-z-]*)/g, "agent-os-$1")
|
|
43
|
+
.replace(/node "\$\{CLAUDE_PLUGIN_ROOT\}\/skills\/\w+\/scripts\/(\w+)\.mjs"/g, "npx -y @sayansr26/agent-os $1");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A description is what the parent reads to pick a subagent, so it names Cursor's files. */
|
|
47
|
+
const describe = (d) => JSON.stringify(translate(String(d || ""))
|
|
48
|
+
.replace(/CLAUDE\.md/g, "AGENTS.md").replace(/\.claude\/rules\//g, ".cursor/rules/"));
|
|
49
|
+
|
|
50
|
+
const READ_AS = `> **Running in Cursor.** This text was written for Claude Code. Read it as:
|
|
51
|
+
> \`CLAUDE.md\` → \`AGENTS.md\` (edit \`.agent-os/AGENTS.md\`, then \`npx @sayansr26/agent-os sync\`);
|
|
52
|
+
> \`.claude/rules/\` → \`.cursor/rules/\` (edit \`.agent-os/rules/\`, then sync);
|
|
53
|
+
> an agent \`<name>\` → the \`agent-os-<name>\` subagent; \`/agent-os:<skill>\` → \`/agent-os-<skill>\`;
|
|
54
|
+
> "the Agent tool" or "dispatch" → delegate to that subagent.`;
|
|
55
|
+
|
|
56
|
+
function agents() {
|
|
57
|
+
const dir = join(PLUGIN, "agents");
|
|
58
|
+
return readdirSync(dir).filter((f) => f.endsWith(".md")).sort().map((f) => {
|
|
59
|
+
const { fields, body } = parseFrontmatter(read(join(dir, f)));
|
|
60
|
+
const name = `agent-os-${fields.name}`;
|
|
61
|
+
// Claude Code creates the memory directory and preloads MEMORY.md for an
|
|
62
|
+
// agent with `memory: project`. Cursor does neither, so the agent is told.
|
|
63
|
+
const memory = `> **Your memory directory** is \`.claude/agent-memory/${name}/\` at the project root
|
|
64
|
+
> (shared with Claude Code's agent-os agents). Create it if it does not exist. Cursor does
|
|
65
|
+
> not load \`MEMORY.md\` for you — read it yourself before anything else.
|
|
66
|
+
>
|
|
67
|
+
${READ_AS}`;
|
|
68
|
+
return {
|
|
69
|
+
path: `.cursor/agents/${name}.md`,
|
|
70
|
+
content: md(
|
|
71
|
+
`name: ${name}\ndescription: ${describe(fields.description)}\nmodel: inherit\n`,
|
|
72
|
+
`${memory}\n\n${translate(body)}`),
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function skills() {
|
|
78
|
+
const out = [];
|
|
79
|
+
const refs = join(PLUGIN, "skills/init/references");
|
|
80
|
+
// init is rewritten for Cursor; its references are shared, read through the table.
|
|
81
|
+
{
|
|
82
|
+
const { fields, body } = parseFrontmatter(read(join(OWN, "skills/agent-os-init/SKILL.md")));
|
|
83
|
+
out.push({ path: ".cursor/skills/agent-os-init/SKILL.md",
|
|
84
|
+
content: md(`name: ${fields.name}\ndescription: ${JSON.stringify(fields.description)}\n`, body) });
|
|
85
|
+
for (const f of readdirSync(refs).filter((f) => f.endsWith(".md")).sort())
|
|
86
|
+
out.push({ path: `.cursor/skills/agent-os-init/references/${f}`,
|
|
87
|
+
content: `<!-- ${BANNER} -->\n\n${READ_AS}\n\n${translate(read(join(refs, f))).trim()}\n` });
|
|
88
|
+
}
|
|
89
|
+
// map and memory carry over as they are, renamed. Their scripts run through
|
|
90
|
+
// the CLI, which bundles the same files, so no script is copied.
|
|
91
|
+
for (const s of ["map", "memory"]) {
|
|
92
|
+
const { fields, body } = parseFrontmatter(read(join(PLUGIN, "skills", s, "SKILL.md")));
|
|
93
|
+
out.push({ path: `.cursor/skills/agent-os-${s}/SKILL.md`,
|
|
94
|
+
content: md(`name: agent-os-${s}\ndescription: ${describe(fields.description)}\n`, `${READ_AS}\n\n${translate(body)}`) });
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function hookScripts(config) {
|
|
100
|
+
const out = [
|
|
101
|
+
{ path: `${HOOKS}/hooks/session-resume.mjs`, content: js(read(join(PLUGIN, "hooks/session-resume.mjs"))) },
|
|
102
|
+
{ path: `${HOOKS}/hooks/pre-edit.mjs`, content: js(read(join(PLUGIN, "hooks/pre-edit.mjs"))) },
|
|
103
|
+
{ path: `${HOOKS}/lib/features.mjs`, content: js(read(join(PLUGIN, "lib/features.mjs"))) },
|
|
104
|
+
];
|
|
105
|
+
if (gitGuard(config)) out.push(
|
|
106
|
+
{ path: `${HOOKS}/hooks/git-guard.mjs`, content: js(read(join(OWN, "hooks/git-guard.mjs"))) },
|
|
107
|
+
{ path: `${HOOKS}/lib/git-deny.mjs`,
|
|
108
|
+
content: js(`/** The same rules Claude Code gets as permissions.deny (settings.mjs). */\nexport const GIT_DENY = ${JSON.stringify(GIT_DENY, null, 2)};\n`) },
|
|
109
|
+
);
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The agent contract, always on — the Cursor half of CLAUDE.md's Step 4b section. */
|
|
114
|
+
const contract = () => ({
|
|
115
|
+
path: ".cursor/rules/agent-os-agents.mdc",
|
|
116
|
+
content: md("description: When to use each agent-os subagent\nalwaysApply: true\n", `## Agents in this project (agent-os)
|
|
117
|
+
|
|
118
|
+
Delegate to these subagents without being asked:
|
|
119
|
+
|
|
120
|
+
- Before changing an existing feature: ask \`agent-os-feature-cartographer\` how
|
|
121
|
+
it is built. In plan mode ask it read-only, and have it file the map as the
|
|
122
|
+
first step after plan mode ends.
|
|
123
|
+
- New subsystem, cross-module change, shared data model: \`agent-os-architect\` first.
|
|
124
|
+
- Writing the change: \`agent-os-builder\`, given the map and the files to change.
|
|
125
|
+
- Done means: \`agent-os-tester\` verified it and \`agent-os-reviewer\` found no
|
|
126
|
+
rule violations.
|
|
127
|
+
- Once verified: \`agent-os-documenter\` records it.
|
|
128
|
+
- Work spanning several of these, or that cannot be stated in one sentence:
|
|
129
|
+
\`agent-os-orchestrator\`.
|
|
130
|
+
|
|
131
|
+
\`/agent-os-map\` builds the architecture map, \`/agent-os-memory\` inspects agent
|
|
132
|
+
memory, \`/agent-os-init\` sets up or repairs all of this.
|
|
133
|
+
|
|
134
|
+
Generated files carry a banner. Edit \`.agent-os/\`, then run
|
|
135
|
+
\`npx @sayansr26/agent-os sync\`; never edit the output.`),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
/** Every file of the layer except hooks.json. */
|
|
139
|
+
export function cursorFiles(src) {
|
|
140
|
+
if (!enabled(src.config)) return [];
|
|
141
|
+
return [...agents(), ...skills(), ...hookScripts(src.config), contract()];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const OURS = (h) => String(h?.command || "").includes(`${HOOKS}/`);
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Merge the agent-os hooks into `.cursor/hooks.json`. Hooks the user wrote are
|
|
148
|
+
* kept; earlier agent-os entries are replaced, so a removed hook goes away.
|
|
149
|
+
*/
|
|
150
|
+
export function mergeHooks(src, existing) {
|
|
151
|
+
if (!existing && !enabled(src.config)) return [];
|
|
152
|
+
const cfg = existing ? JSON.parse(existing) : { version: 1, hooks: {} };
|
|
153
|
+
cfg.version ||= 1;
|
|
154
|
+
cfg.hooks ||= {};
|
|
155
|
+
for (const ev of Object.keys(cfg.hooks)) {
|
|
156
|
+
cfg.hooks[ev] = (cfg.hooks[ev] || []).filter((h) => !OURS(h));
|
|
157
|
+
if (!cfg.hooks[ev].length) delete cfg.hooks[ev];
|
|
158
|
+
}
|
|
159
|
+
if (enabled(src.config)) {
|
|
160
|
+
const add = (ev, h) => (cfg.hooks[ev] ||= []).push(h);
|
|
161
|
+
add("sessionStart", { command: `node ${HOOKS}/hooks/session-resume.mjs --cursor` });
|
|
162
|
+
add("preToolUse", { command: `node ${HOOKS}/hooks/pre-edit.mjs --cursor`, matcher: "Write" });
|
|
163
|
+
if (gitGuard(src.config))
|
|
164
|
+
add("beforeShellExecution", { command: `node ${HOOKS}/hooks/git-guard.mjs`, matcher: "\\bgit\\b" });
|
|
165
|
+
}
|
|
166
|
+
return [{ path: ".cursor/hooks.json", content: JSON.stringify(cfg, null, 2) + "\n" }];
|
|
167
|
+
}
|
package/src/selftest.mjs
CHANGED
|
@@ -57,6 +57,39 @@ try {
|
|
|
57
57
|
ok(read(".claude/skills/demo/SKILL.md") !== null, "skills -> .claude/skills");
|
|
58
58
|
ok(read(".cline/skills/demo/SKILL.md") !== null, "skills -> .cline/skills");
|
|
59
59
|
|
|
60
|
+
console.log("\n Cursor gets the agent-os layer");
|
|
61
|
+
{
|
|
62
|
+
const agents = ["architect", "builder", "documenter", "feature-cartographer", "orchestrator", "reviewer", "tester"];
|
|
63
|
+
ok(agents.every((a) => read(`.cursor/agents/agent-os-${a}.md`)?.startsWith(`---\nname: agent-os-${a}\n`)), "7 subagents in .cursor/agents/, Cursor-legal names");
|
|
64
|
+
const carto = read(".cursor/agents/agent-os-feature-cartographer.md");
|
|
65
|
+
ok(carto.includes(".claude/agent-memory/agent-os-feature-cartographer/"), "subagent memory shared with Claude Code's directory");
|
|
66
|
+
ok(!/agent-os:[a-z]/.test(carto) && !/^memory:|^tools:/m.test(carto), "no Claude-only names or frontmatter left");
|
|
67
|
+
ok(["init", "map", "memory"].every((s) => read(`.cursor/skills/agent-os-${s}/SKILL.md`)?.includes(`name: agent-os-${s}\n`)), "skills /agent-os-init, -map, -memory");
|
|
68
|
+
ok(!read(".cursor/skills/agent-os-memory/SKILL.md").includes("CLAUDE_PLUGIN_ROOT"), "skill scripts run through the CLI");
|
|
69
|
+
ok(read(".cursor/rules/agent-os-agents.mdc")?.includes("alwaysApply: true"), "agent contract is an always-on rule");
|
|
70
|
+
const hooks = JSON.parse(read(".cursor/hooks.json"));
|
|
71
|
+
ok(hooks.version === 1 && hooks.hooks.sessionStart && hooks.hooks.preToolUse && hooks.hooks.beforeShellExecution, "hooks.json: sessionStart, preToolUse, beforeShellExecution");
|
|
72
|
+
|
|
73
|
+
const hook = (script, args, input) => spawnSync(process.execPath, [join(root, ".cursor/hooks/agent-os/hooks", script), ...args],
|
|
74
|
+
{ cwd: root, encoding: "utf8", input, env: { ...process.env, CURSOR_PROJECT_DIR: root } }).stdout;
|
|
75
|
+
const start = JSON.parse(hook("session-resume.mjs", ["--cursor"], ""));
|
|
76
|
+
ok(start.additional_context?.includes("`agent-os-feature-cartographer`"), "session start returns additional_context with Cursor names");
|
|
77
|
+
const pre = (file) => JSON.parse(hook("pre-edit.mjs", ["--cursor"], JSON.stringify({ tool_input: { file_path: join(root, file) } })));
|
|
78
|
+
ok(pre(".cursor/rules/api.mdc").permission === "deny", "pre-edit denies an edit to a generated rule");
|
|
79
|
+
ok(pre(".cursor/agents/agent-os-builder.md").permission === "deny", "pre-edit denies an edit to a generated subagent");
|
|
80
|
+
ok(pre("src/new.ts").permission === "allow", "pre-edit answers allow, never empty, for everything else");
|
|
81
|
+
const git = (command) => JSON.parse(hook("git-guard.mjs", [], JSON.stringify({ command }))).permission;
|
|
82
|
+
ok(git("git status") === "allow" && git("git log --oneline") === "allow", "read-only git allowed");
|
|
83
|
+
ok(["git push", "git commit -m x", "cd a && git reset --hard", "git -C x add .", "A=1 git rebase main"].every((c) => git(c) === "deny"), "git writes denied, chained and flag forms too");
|
|
84
|
+
|
|
85
|
+
writeFileSync(join(root, ".cursor/hooks.json"), JSON.stringify({ version: 1, hooks: { afterFileEdit: [{ command: "./fmt.sh" }] } }));
|
|
86
|
+
cli(["sync"], root);
|
|
87
|
+
const merged = JSON.parse(read(".cursor/hooks.json")).hooks;
|
|
88
|
+
ok(merged.afterFileEdit?.[0]?.command === "./fmt.sh" && merged.sessionStart?.length === 1, "hooks.json merged: user hook kept, ours added once");
|
|
89
|
+
cli(["sync"], root);
|
|
90
|
+
ok(JSON.parse(read(".cursor/hooks.json")).hooks.preToolUse.length === 1, "re-sync does not duplicate hooks");
|
|
91
|
+
}
|
|
92
|
+
|
|
60
93
|
console.log("\n drift");
|
|
61
94
|
ok(cli(["check"], root).status === 0, "check passes when in sync");
|
|
62
95
|
// Realistic drift: someone edits the body of a generated file and leaves the
|
|
@@ -229,7 +262,34 @@ try {
|
|
|
229
262
|
const oc = JSON.parse(read("opencode.json"));
|
|
230
263
|
ok(oc.model === "anthropic/x", "existing keys preserved");
|
|
231
264
|
ok(oc.instructions.includes("KEEP.md"), "existing instructions preserved");
|
|
232
|
-
} finally { rmSync(root, { recursive: true, force: true });
|
|
265
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
266
|
+
|
|
267
|
+
console.log("\n a Cursor-only init does not report Claude Code items");
|
|
268
|
+
const cu = mkdtempSync(join(tmpdir(), "agent-os-cursor-"));
|
|
269
|
+
try {
|
|
270
|
+
mkdirSync(join(cu, ".agent-os"), { recursive: true });
|
|
271
|
+
writeFileSync(join(cu, ".agent-os/config.json"), JSON.stringify({ targets: ["cursor"] }));
|
|
272
|
+
const r = cli(["init", "--no-global"], cu);
|
|
273
|
+
ok(r.status === 0, "init exits 0");
|
|
274
|
+
ok(!r.stdout.includes("SETUP") && !r.stdout.includes("still to do") && !r.stdout.includes("CLAUDE.md"), "no Claude Code SETUP or to-do list");
|
|
275
|
+
ok(/Result.*compiled for Cursor/.test(r.stdout) && r.stdout.includes("/agent-os-init"), "result names Cursor and the next step");
|
|
276
|
+
ok(!existsSync(join(cu, ".claude/settings.json")), "nothing written for Claude Code");
|
|
277
|
+
writeFileSync(join(cu, ".agent-os/config.json"), JSON.stringify({ targets: ["cursor"], cursor: { gitProtection: false } }));
|
|
278
|
+
cli(["sync"], cu);
|
|
279
|
+
ok(!JSON.parse(readFileSync(join(cu, ".cursor/hooks.json"), "utf8")).hooks.beforeShellExecution, "gitProtection: false removes the git hook");
|
|
280
|
+
} finally { rmSync(cu, { recursive: true, force: true }); }
|
|
281
|
+
|
|
282
|
+
console.log("\n a Rulegate repository is left alone");
|
|
283
|
+
const rg = mkdtempSync(join(tmpdir(), "agent-os-rulegate-"));
|
|
284
|
+
try {
|
|
285
|
+
mkdirSync(join(rg, ".rulegate/rules"), { recursive: true });
|
|
286
|
+
writeFileSync(join(rg, "CLAUDE.md"), "<!-- Generated by Rulegate -->\n# Project\n");
|
|
287
|
+
for (const cmd of ["init", "sync"]) {
|
|
288
|
+
const r = cli([cmd, "--no-plugin", "--no-global"], rg);
|
|
289
|
+
ok(r.status === 1 && /uses Rulegate/.test(r.stderr), `${cmd} refuses with .rulegate/ present`);
|
|
290
|
+
}
|
|
291
|
+
ok(!existsSync(join(rg, ".agent-os")) && !existsSync(join(rg, "AGENTS.md")), "nothing written");
|
|
292
|
+
} finally { rmSync(rg, { recursive: true, force: true }); rmSync(HOME, { recursive: true, force: true }); }
|
|
233
293
|
|
|
234
294
|
console.log(`\n${fail ? `FAILED (${fail})` : "PASSED"}\n`);
|
|
235
295
|
process.exit(fail ? 1 : 0);
|
package/src/targets.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* existing JSON config receive the current contents and return the merged file.
|
|
8
8
|
*/
|
|
9
9
|
import { BANNER } from "./source.mjs";
|
|
10
|
+
import { cursorFiles, mergeHooks } from "./cursor.mjs";
|
|
10
11
|
|
|
11
12
|
const hdr = (comment) => `${comment} ${BANNER}\n`;
|
|
12
13
|
const mdHeader = `<!-- ${BANNER} -->\n\n`;
|
|
@@ -54,13 +55,19 @@ export const TARGETS = {
|
|
|
54
55
|
// ---- translate ----------------------------------------------------------
|
|
55
56
|
cursor: {
|
|
56
57
|
label: "Cursor",
|
|
57
|
-
// .mdc, and a rule without frontmatter is ignored entirely.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
`---\
|
|
63
|
-
|
|
58
|
+
// .mdc, and a rule without frontmatter is ignored entirely. Then the
|
|
59
|
+
// agent-os layer — subagents, skills, hooks — which src/cursor.mjs builds.
|
|
60
|
+
files: (src) => [
|
|
61
|
+
...src.rules.map((r) => ({
|
|
62
|
+
path: `.cursor/rules/${r.name}.mdc`,
|
|
63
|
+
content: `---\ndescription: ${r.description || r.name}\n` +
|
|
64
|
+
(r.always ? "alwaysApply: true\n" : `globs: ${r.paths.join(",")}\nalwaysApply: false\n`) +
|
|
65
|
+
`---\n\n${mdHeader}${r.body}\n`,
|
|
66
|
+
})),
|
|
67
|
+
...cursorFiles(src),
|
|
68
|
+
],
|
|
69
|
+
merge: mergeHooks,
|
|
70
|
+
mergeFrom: ".cursor/hooks.json",
|
|
64
71
|
},
|
|
65
72
|
|
|
66
73
|
windsurf: {
|
|
@@ -179,10 +186,10 @@ export function compile(src, targetIds, readExisting) {
|
|
|
179
186
|
for (const id of targetIds) {
|
|
180
187
|
const t = TARGETS[id];
|
|
181
188
|
if (!t) continue;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
out.push(...
|
|
189
|
+
// A target writes files, merges into one existing config, or both. Merged
|
|
190
|
+
// files are marked: they are rewritten in place with nothing of the user's lost.
|
|
191
|
+
if (t.files) out.push(...t.files(src).map((f) => ({ ...f, target: id })));
|
|
192
|
+
if (t.merge) out.push(...t.merge(src, readExisting(t.mergeFrom)).map((f) => ({ ...f, target: id, merged: true })));
|
|
186
193
|
}
|
|
187
194
|
return out;
|
|
188
195
|
}
|