@orlan-maker/cli 0.1.0 → 0.2.1
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/.claude-plugin/plugin.json +2 -2
- package/README.md +89 -43
- package/bin/orlan +0 -0
- package/dist/agents.js +74 -88
- package/dist/commands.js +778 -213
- package/dist/http.js +35 -11
- package/dist/main.js +3 -4
- package/dist/skills.js +1 -1
- package/hooks/orlan-intro.txt +1 -0
- package/hooks/session-start +4 -1
- package/opencode/orlan.js +15 -3
- package/package.json +5 -14
- package/skills/orlan/SKILL.md +88 -0
- package/skills/orlan-review/SKILL.md +16 -7
- package/skills/orlan-versions/SKILL.md +9 -2
- package/.mcp.json +0 -9
- package/dist/mcp.js +0 -113
- package/monitors/monitors.json +0 -7
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{
|
|
10
10
|
"name": "orlan",
|
|
11
11
|
"source": "./",
|
|
12
|
-
"description": "The Orlan review board in Claude Code: the orlan command,
|
|
12
|
+
"description": "The Orlan review board in Claude Code: the orlan command, hooks and skills."
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orlan",
|
|
3
3
|
"displayName": "Orlan",
|
|
4
|
-
"version": "0.1
|
|
5
|
-
"description": "Work with the Orlan review board: the orlan command,
|
|
4
|
+
"version": "0.2.1",
|
|
5
|
+
"description": "Work with the Orlan review board: the orlan command, hooks that show the session's state on the board, and the skills.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Orlan",
|
|
8
8
|
"url": "https://orlan.app"
|
package/README.md
CHANGED
|
@@ -2,20 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
The `orlan` command connects an AI agent to [Orlan](https://orlan.app), the review board for decks,
|
|
4
4
|
PDFs, images and HTML. The agent then reads the comments on a file, posts a new version, replies and
|
|
5
|
-
resolves,
|
|
5
|
+
resolves, all with the `orlan` command in the terminal.
|
|
6
6
|
|
|
7
7
|
Needs Node 22.18 or later.
|
|
8
8
|
|
|
9
9
|
## Set up
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
The prompt runs these four commands:
|
|
11
|
+
Three commands connect an agent:
|
|
13
12
|
|
|
14
13
|
```
|
|
15
14
|
npm i -g @orlan-maker/cli
|
|
16
15
|
orlan auth login
|
|
17
|
-
orlan
|
|
18
|
-
orlan mcp connect --agent claude-code
|
|
16
|
+
orlan connect --agent claude-code
|
|
19
17
|
```
|
|
20
18
|
|
|
21
19
|
- `orlan auth login` shows a code and opens the approval page in your browser. Sign in, check the
|
|
@@ -23,24 +21,31 @@ orlan mcp connect --agent claude-code
|
|
|
23
21
|
approval (codes work for 10 minutes). The CLI token goes to the OS keychain (macOS keychain, or the
|
|
24
22
|
Secret Service on Linux), else to `credentials.json` in the config folder, readable by you only.
|
|
25
23
|
`--server <url>` signs in to another Orlan server; `--no-browser` only prints the address.
|
|
26
|
-
- `orlan
|
|
27
|
-
|
|
28
|
-
the
|
|
29
|
-
`--
|
|
30
|
-
- `orlan
|
|
31
|
-
|
|
24
|
+
- `orlan connect` makes an agent token and keeps it in the keychain. The agent then works on Orlan
|
|
25
|
+
with the `orlan` command. Only an admin of the organisation connects agents. The command installs
|
|
26
|
+
the skills, and the plugin or the hooks of the agent (table below). Then it tests the token with one
|
|
27
|
+
plain HTTP call. `--topic` and `--role` change the topics and the role of the agent.
|
|
28
|
+
- The skills are `orlan`, `orlan-review` and `orlan-versions`. `orlan` tells the agent what Orlan is:
|
|
29
|
+
"the canvas", "the board" and "the topic" mean the Orlan topic, and a file the person asks for goes
|
|
30
|
+
on the topic. A diagram is board shapes, never an HTML file or notes. `orlan skills add` writes the
|
|
31
|
+
skills again without a new token.
|
|
32
|
+
- An older CLI wrote an Orlan server entry in the agent's settings. `orlan connect` removes that
|
|
33
|
+
entry, and keeps every other entry.
|
|
34
|
+
- `orlan status` shows who you are, the server, and each connected agent, and tests each agent's
|
|
35
|
+
connection with one plain HTTP call. It ends with exit code 1 when a check fails.
|
|
32
36
|
|
|
33
37
|
Agents: `claude-code`, `codex`, `cursor`, `opencode`, `other`.
|
|
34
38
|
|
|
35
|
-
| Agent | Skills |
|
|
39
|
+
| Agent | Skills | Also installs |
|
|
36
40
|
| --- | --- | --- |
|
|
37
|
-
| claude-code |
|
|
38
|
-
| codex | `~/.agents/skills` |
|
|
39
|
-
| cursor | `~/.cursor/skills` |
|
|
40
|
-
| opencode | `~/.config/opencode/skills` |
|
|
41
|
-
| other | `.agents/skills` in the folder you are in |
|
|
41
|
+
| claude-code | in the plugin | the Claude Code plugin (below) |
|
|
42
|
+
| codex | `~/.agents/skills` | the hooks in `$CODEX_HOME/hooks.json` |
|
|
43
|
+
| cursor | `~/.cursor/skills` | the hooks in `~/.cursor/hooks.json` |
|
|
44
|
+
| opencode | `~/.config/opencode/skills` | the plugin in `~/.config/opencode/plugins/orlan.js` |
|
|
45
|
+
| other | `.agents/skills` in the folder you are in | - |
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
With `--no-plugin`, Claude Code gets no plugin, and the skills go to `~/.claude/skills`. Without
|
|
48
|
+
`--agent`, the CLI finds the agent that runs it from its environment.
|
|
44
49
|
|
|
45
50
|
## The Claude Code plugin
|
|
46
51
|
|
|
@@ -52,45 +57,57 @@ claude plugin marketplace add AliProgrammin/orlan-plugin
|
|
|
52
57
|
claude plugin install orlan@orlan
|
|
53
58
|
```
|
|
54
59
|
|
|
55
|
-
`orlan
|
|
60
|
+
`orlan connect --agent claude-code` installs it too; from a checkout,
|
|
56
61
|
`claude plugin marketplace add ./packages/cli` and `claude plugin install orlan@orlan` do the same. One install gives the session:
|
|
57
62
|
|
|
58
63
|
- `orlan` on the PATH of Claude Code's shell.
|
|
59
|
-
- A monitor that runs `orlan wait --follow`: a mention of the agent wakes the session, also when it is
|
|
60
|
-
idle. Monitors run only in an interactive session.
|
|
61
64
|
- Hooks that show the session on the board: working (session start, a prompt, each tool), waiting for
|
|
62
65
|
input (a permission prompt or a question in the terminal), idle (the turn ended), done (the session
|
|
63
|
-
ended). At the session start, `orlan brief` goes into the session's context
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
ended). At the session start, `orlan brief` goes into the session's context, with one line that says
|
|
67
|
+
what Orlan is: "the canvas", "the board" and "the topic" mean the Orlan topic, and a diagram, a page,
|
|
68
|
+
a deck or a mock starts with the `orlan` skill. Codex, Cursor and OpenCode get the same line, from
|
|
69
|
+
`hooks/orlan-intro.txt`.
|
|
70
|
+
- The skills `orlan`, `orlan-review` and `orlan-versions`.
|
|
71
|
+
|
|
72
|
+
The session works on Orlan with the `orlan` command. A new request comes to the session when the agent
|
|
73
|
+
runs `orlan brief`, `orlan inbox` or `orlan wait`.
|
|
68
74
|
|
|
69
75
|
## The OpenCode plugin
|
|
70
76
|
|
|
71
|
-
`orlan
|
|
77
|
+
`orlan connect --agent opencode` also writes the Orlan plugin to `~/.config/opencode/plugins/orlan.js`.
|
|
72
78
|
When a session's turn ends, the plugin runs `orlan hook wait`. A mention of the agent on the board then goes
|
|
73
79
|
to the session as a new prompt, and a new turn starts. The plugin also shows the session on the board:
|
|
74
|
-
working, waiting for a permission prompt, idle, done.
|
|
75
|
-
|
|
80
|
+
working, waiting for a permission prompt, idle, done. It adds the line that says what Orlan is to the
|
|
81
|
+
system prompt of each session. A request wakes a session only while OpenCode runs
|
|
82
|
+
(the TUI or `opencode serve`). Restart a running OpenCode to load the plugin. `orlan disconnect
|
|
76
83
|
--agent opencode` removes it.
|
|
77
84
|
|
|
78
85
|
## Daily commands
|
|
79
86
|
|
|
80
87
|
Each works as the connected agent, with the agent's own token, and is in the topic's audit log. Each
|
|
81
|
-
is plain HTTP with the agent token
|
|
88
|
+
is plain HTTP with the agent token: `orlan files push` makes 2 requests (the file,
|
|
82
89
|
then the post), `orlan files pull` 2 (the file's answer, then the download), and most others 1.
|
|
83
90
|
|
|
84
91
|
```
|
|
85
92
|
orlan brief [--topic <topic>]
|
|
86
93
|
orlan topics list
|
|
87
|
-
orlan files list --topic <topic>
|
|
88
|
-
orlan files add <path> [--topic <topic>]
|
|
89
|
-
orlan files pull <file> [--version <n>] [--topic <topic>] [--out <path>] [--edit [--shared] [--ttl <time>]]
|
|
94
|
+
orlan files list [--topic <topic>]
|
|
95
|
+
orlan files add <path> [--at <x,y>] [--topic <topic>]
|
|
96
|
+
orlan files pull <file> [--version <n>] [--pages <n,n> | --since <n>] [--topic <topic>] [--out <path>] [--edit [--shared] [--ttl <time>]]
|
|
90
97
|
orlan files push <file> <path> --changelog "<what changed>" [--base <n>] [--topic <topic>]
|
|
98
|
+
orlan files rollback <file> --version <n> [--changelog "<why>"] [--topic <topic>]
|
|
91
99
|
orlan comments list [--topic <topic>] [--status open|resolved|all] [--file <file>]
|
|
92
100
|
orlan comment <thread> "<text>" | --file <file> --page <n> [--region <x,y,w,h>] "<text>" | --at <x,y> "<text>" [--topic <topic>]
|
|
93
101
|
orlan comments resolve <thread> [--topic <topic>]
|
|
102
|
+
orlan board [--topic <topic>]
|
|
103
|
+
orlan board add note|text|frame ["<text>"] --at <x,y> [--size <w,h>] | arrow --from <item> --to <item>
|
|
104
|
+
orlan board change <item> [--text "<text>"] [--at <x,y>] [--size <w,h>] [--from <item>|none] [--to <item>|none]
|
|
105
|
+
orlan board delete <item>...
|
|
106
|
+
orlan board edit '<edits as JSON>'
|
|
107
|
+
orlan tasks add "<text>" [--assignee me|user:<id>|agent:<id>] [--thread <thread>] [--done]
|
|
108
|
+
orlan tasks change <task id> [--text "<text>"] [--done | --open] [--assignee me|user:<id>|agent:<id>|none]
|
|
109
|
+
orlan decisions add "<title>" [--body "<text>"] [--thread <thread>]
|
|
110
|
+
orlan search "<words>" [--topic <topic>] [--limit <n>]
|
|
94
111
|
orlan open [<topic id>] [--file <file id>]
|
|
95
112
|
orlan wait [--follow]
|
|
96
113
|
orlan inbox [--new]
|
|
@@ -98,7 +115,6 @@ orlan inbox done <request id>
|
|
|
98
115
|
orlan respond <request id> --message "<text>" [--file <path>] [--to <file>] [--changelog "<text>"] [--base <n>] [--wait]
|
|
99
116
|
orlan hook <working|needs-input|idle|done>
|
|
100
117
|
orlan hook start|stop --agent codex|cursor
|
|
101
|
-
orlan mcp headers
|
|
102
118
|
```
|
|
103
119
|
|
|
104
120
|
The ids people see work as input: a topic by its name, a file by its name, a thread as `#12`, and a
|
|
@@ -108,14 +124,33 @@ its id and topic: give the id, or add `--topic`.
|
|
|
108
124
|
|
|
109
125
|
Every output ends with a `next_step` line, and every error says what to do next in a `next_step` line.
|
|
110
126
|
With `--json`, a command prints small JSON on one line with a `next_step` field; an error is JSON too:
|
|
111
|
-
`{"error": "<code>", "message": "...", "next_step": "..."}`. The hook commands
|
|
127
|
+
`{"error": "<code>", "message": "...", "next_step": "..."}`. The hook commands
|
|
112
128
|
print only what their harness reads, and `orlan inbox --new` prints nothing when there is no request.
|
|
113
129
|
|
|
114
130
|
`orlan brief` is for the start of a session. It prints the topic, the open requests, the files with
|
|
115
131
|
their current versions, and one `next_step` line, in about 500 tokens for 6 files and 3 requests. The
|
|
116
132
|
topic is the one the agent used last, else its only topic. `orlan files add` posts a new file to the
|
|
117
|
-
topic and puts it on the board, in one request. Both are plain HTTP calls with the agent token
|
|
118
|
-
|
|
133
|
+
topic and puts it on the board, in one request. Both are plain HTTP calls with the agent token.
|
|
134
|
+
|
|
135
|
+
`orlan board` prints the board of the topic. Each item shows its id, type, place and size in board
|
|
136
|
+
coordinates, and its text. A file shows its name and pages, and an arrow the items it links. Then come the
|
|
137
|
+
timer, the open vote and the last vote with its result, and the open tasks. People vote; an agent reads
|
|
138
|
+
the vote, it does not vote. `orlan board add` adds a note, a text, a frame or an arrow, `orlan board
|
|
139
|
+
change` changes an item's text, place, size or arrow ends, and `orlan board delete` deletes items. Each
|
|
140
|
+
command is one step that a person can undo on the board. `orlan board edit` takes a JSON list of edits
|
|
141
|
+
and makes them in one step, all or none; `"#0"` names the item that the first edit made. A file on the
|
|
142
|
+
board only moves. A negative coordinate goes with `=`: `--at=-50,120`. The CLI cannot draw shapes yet,
|
|
143
|
+
so a diagram is not made with these commands.
|
|
144
|
+
|
|
145
|
+
`orlan tasks add` adds a task to the topic, `orlan tasks change` changes its text, marks it done or open,
|
|
146
|
+
or sets its assignee (`--assignee me` is the agent). `orlan decisions add` records a decision in the
|
|
147
|
+
topic's decision log. `--thread #12` links a task or a decision to the thread it comes from.
|
|
148
|
+
|
|
149
|
+
`orlan search` searches the topics of the agent: topic and file names, the text of the pages, comments
|
|
150
|
+
and decisions. Each result names its topic and ids, and the matches are between « and ».
|
|
151
|
+
|
|
152
|
+
A command that needs a topic takes `--topic`. Without it, the command uses the topic the agent used
|
|
153
|
+
last, else its only topic.
|
|
119
154
|
|
|
120
155
|
`orlan comment` replies in a thread, or opens a new thread: with `--file` and `--page` on a page (the
|
|
121
156
|
whole page, or `--region` as fractions of the page), with `--at` on a point of the board. People see it
|
|
@@ -131,6 +166,13 @@ goes back to the inbox within 20 seconds. `--follow` stays open and prints one l
|
|
|
131
166
|
`--new` takes the new ones and prints nothing when there are none, for a stop hook. `orlan inbox done`
|
|
132
167
|
closes a request when its work is finished.
|
|
133
168
|
|
|
169
|
+
`orlan files pull --pages 2,5` gets pages, not the file: it saves the image of each page as
|
|
170
|
+
`<name>-page-<n>.png` in the `--out` folder, and prints the text of each page. `--since 4` gets the
|
|
171
|
+
pages that changed since version 4, from the page compare. `orlan files list` shows the pages that
|
|
172
|
+
changed from the version before. `orlan files add --at <x,y>` puts a new file at that point of the
|
|
173
|
+
board, or at the nearest free place. `orlan files rollback <file> --version 3` posts version 3 again as
|
|
174
|
+
the newest version; it waits for a person to make it current.
|
|
175
|
+
|
|
134
176
|
`orlan files pull --edit` puts the agent's claim on the file: other agents get it in their answer,
|
|
135
177
|
and the board shows "Claude (for Sara) is editing from v4 - 3 min" on the file. The claim warns and
|
|
136
178
|
blocks nothing. It lasts 15 minutes (`--ttl 90s`, `--ttl 1h`), and a push or respond of the agent ends
|
|
@@ -154,15 +196,18 @@ standard input, makes one request, prints nothing and makes no model call. Orlan
|
|
|
154
196
|
terminal prompt; the board tells people to go to the terminal. With no agent connected, it does nothing.
|
|
155
197
|
|
|
156
198
|
`orlan hook start` and `orlan hook stop` are the session start and stop hooks of Codex and Cursor, and
|
|
157
|
-
`orlan
|
|
158
|
-
the start hook puts the brief in the session, and when a turn ends the stop hook gives the session the
|
|
199
|
+
`orlan connect --agent codex|cursor` installs them. Codex and Cursor sessions do not wake when idle:
|
|
200
|
+
the start hook puts the brief and the line that says what Orlan is in the session, and when a turn ends the stop hook gives the session the
|
|
159
201
|
new requests, so it works on at once. After 5 requests in a row it lets the session stop. Codex runs a
|
|
160
202
|
hook only after you trust it once with `/hooks`.
|
|
161
203
|
|
|
162
|
-
|
|
163
|
-
|
|
204
|
+
Each command shows on the board as a step of the agent's hook session: a page it reads moves its cursor
|
|
205
|
+
there. The command finds its session from the harness: `CLAUDE_CODE_SESSION_ID` in Claude Code,
|
|
206
|
+
`CODEX_THREAD_ID` in Codex, and in OpenCode `ORLAN_SESSION` (the plugin sets it) or
|
|
207
|
+
`OPENCODE_SESSION_ID`. Cursor gives no session id to the shell: Orlan then takes the hook session of the
|
|
208
|
+
agent that reported last.
|
|
164
209
|
|
|
165
|
-
While a wait of the agent is open
|
|
210
|
+
While a wait of the agent is open, the `next_step` lines do not say `--wait`:
|
|
166
211
|
the open wait gets the next request.
|
|
167
212
|
|
|
168
213
|
A version the agent posts waits for a person to make it current. When several agents are connected,
|
|
@@ -170,7 +215,8 @@ add `--agent <id>`.
|
|
|
170
215
|
|
|
171
216
|
## Stop
|
|
172
217
|
|
|
173
|
-
- `orlan
|
|
218
|
+
- `orlan disconnect --agent <id>` revokes the agent's token, and removes its plugin or hooks and an old
|
|
219
|
+
Orlan server entry. The skills stay.
|
|
174
220
|
- `orlan auth logout` revokes the CLI token. You can also revoke it in Orlan: Profile, Orlan CLI sign-ins.
|
|
175
221
|
|
|
176
222
|
Run `orlan help` for every command, and `orlan <command> --help` for its options.
|
package/bin/orlan
CHANGED
|
File without changes
|
package/dist/agents.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The agents the CLI connects: where each one reads skills,
|
|
3
|
-
*
|
|
2
|
+
* The agents the CLI connects: where each one reads skills, its hooks or plugin, and the old Orlan MCP
|
|
3
|
+
* entry that a connect removes (D088). The ids match the web app's connect dialog (agentClients in
|
|
4
|
+
* @orlan/shared).
|
|
4
5
|
*/
|
|
5
6
|
import { execFile } from "node:child_process";
|
|
6
7
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
@@ -16,7 +17,7 @@ export const agentLabels = {
|
|
|
16
17
|
opencode: "OpenCode",
|
|
17
18
|
other: "your agent",
|
|
18
19
|
};
|
|
19
|
-
/** The MCP server
|
|
20
|
+
/** The name of the old Orlan MCP server in each agent's settings (before D088). */
|
|
20
21
|
export const MCP_NAME = "orlan";
|
|
21
22
|
export function parseAgent(value) {
|
|
22
23
|
if (agentIds.includes(value))
|
|
@@ -61,8 +62,8 @@ export function skillsDir(agent, cwd = process.cwd()) {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
/**
|
|
64
|
-
* The MCP settings file of an agent
|
|
65
|
-
* documentation on 2026-09-24 (B018):
|
|
65
|
+
* The MCP settings file of an agent, where an older CLI wrote the Orlan MCP entry. Checked against each
|
|
66
|
+
* tool's documentation on 2026-09-24 (B018):
|
|
66
67
|
* - Codex: `[mcp_servers.<name>]` with `url` and `http_headers` in ~/.codex/config.toml -
|
|
67
68
|
* https://learn.chatgpt.com/docs/extend/mcp?surface=cli (was https://developers.openai.com/codex/mcp)
|
|
68
69
|
* - Cursor: `mcpServers.<name>` with `url` and `headers` in ~/.cursor/mcp.json (the editor and the
|
|
@@ -101,14 +102,13 @@ async function writeText(file, text) {
|
|
|
101
102
|
/** A TOML table header line such as `[mcp_servers.orlan]` or `[[x]]`, with its dotted name. */
|
|
102
103
|
const tableHeader = (line) => /^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(#.*)?$/.exec(line)?.[1];
|
|
103
104
|
/**
|
|
104
|
-
* The Codex config.toml without the `[mcp_servers.orlan]` table and its sub-tables
|
|
105
|
-
*
|
|
105
|
+
* The Codex config.toml without the `[mcp_servers.orlan]` table and its sub-tables. Every other line
|
|
106
|
+
* stays as it was, comments too.
|
|
106
107
|
*/
|
|
107
|
-
export function editCodexToml(text
|
|
108
|
-
const lines = text.split("\n");
|
|
108
|
+
export function editCodexToml(text) {
|
|
109
109
|
const kept = [];
|
|
110
110
|
let inOrlan = false;
|
|
111
|
-
for (const line of
|
|
111
|
+
for (const line of text.split("\n")) {
|
|
112
112
|
const header = tableHeader(line);
|
|
113
113
|
if (header !== undefined) {
|
|
114
114
|
const name = header.replace(/\s/g, "").replace(/"/g, "");
|
|
@@ -117,39 +117,31 @@ export function editCodexToml(text, entry) {
|
|
|
117
117
|
if (!inOrlan)
|
|
118
118
|
kept.push(line);
|
|
119
119
|
}
|
|
120
|
-
|
|
121
|
-
if (entry) {
|
|
122
|
-
const table = [
|
|
123
|
-
`[mcp_servers.${MCP_NAME}]`,
|
|
124
|
-
`url = ${JSON.stringify(entry.url)}`,
|
|
125
|
-
`http_headers = { "Authorization" = ${JSON.stringify(entry.authorization)} }`,
|
|
126
|
-
].join("\n");
|
|
127
|
-
result = result ? `${result}\n\n${table}` : table;
|
|
128
|
-
}
|
|
120
|
+
const result = kept.join("\n").replace(/\n+$/, "");
|
|
129
121
|
return result ? `${result}\n` : "";
|
|
130
122
|
}
|
|
131
|
-
/**
|
|
132
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Removes the Orlan key under `section` of a JSON settings file. Every other key stays. Returns true
|
|
125
|
+
* when it removed it.
|
|
126
|
+
*/
|
|
127
|
+
async function removeJsonKey(file, section) {
|
|
133
128
|
const text = await readText(file);
|
|
129
|
+
if (!text.includes(`"${MCP_NAME}"`))
|
|
130
|
+
return false;
|
|
134
131
|
let settings;
|
|
135
132
|
try {
|
|
136
|
-
settings =
|
|
133
|
+
settings = JSON.parse(text);
|
|
137
134
|
}
|
|
138
135
|
catch {
|
|
139
136
|
throw new OrlanError(`${file} is not plain JSON (it has comments or an error), so Orlan does not change it. ` +
|
|
140
|
-
"
|
|
141
|
-
}
|
|
142
|
-
const servers = { ...(settings[section] ?? {}) };
|
|
143
|
-
if (value === undefined) {
|
|
144
|
-
if (!(MCP_NAME in servers))
|
|
145
|
-
return;
|
|
146
|
-
delete servers[MCP_NAME];
|
|
147
|
-
}
|
|
148
|
-
else {
|
|
149
|
-
servers[MCP_NAME] = value;
|
|
137
|
+
`Remove the "${MCP_NAME}" entry under "${section}" by hand, then run this again.`);
|
|
150
138
|
}
|
|
151
|
-
settings[section]
|
|
139
|
+
const servers = settings[section];
|
|
140
|
+
if (!servers || typeof servers !== "object" || !(MCP_NAME in servers))
|
|
141
|
+
return false;
|
|
142
|
+
delete servers[MCP_NAME];
|
|
152
143
|
await writeText(file, `${JSON.stringify(settings, null, 2)}\n`);
|
|
144
|
+
return true;
|
|
153
145
|
}
|
|
154
146
|
function runClaude(args) {
|
|
155
147
|
return new Promise((resolve, reject) => {
|
|
@@ -193,6 +185,13 @@ export async function installClaudePlugin() {
|
|
|
193
185
|
const installed = await runClaude(["plugin", "install", CLAUDE_PLUGIN, "--scope", "user"]);
|
|
194
186
|
return installed.ok && (await claudePluginEnabled());
|
|
195
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Takes the Orlan plugin out of Claude Code for this user. The marketplace stays: a person can have
|
|
190
|
+
* added it. With no claude command, or no plugin, it changes nothing.
|
|
191
|
+
*/
|
|
192
|
+
export async function uninstallClaudePlugin() {
|
|
193
|
+
await runClaude(["plugin", "uninstall", CLAUDE_PLUGIN, "--scope", "user"]).catch(() => undefined);
|
|
194
|
+
}
|
|
196
195
|
/**
|
|
197
196
|
* The hooks file of Codex and Cursor (F046). Checked against each tool's documentation on 2026-09-26:
|
|
198
197
|
* - Codex: $CODEX_HOME/hooks.json (default ~/.codex), events SessionStart, UserPromptSubmit,
|
|
@@ -259,6 +258,26 @@ function orlanHooks(agent) {
|
|
|
259
258
|
sessionEnd: entry("done", 5),
|
|
260
259
|
};
|
|
261
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* The line that tells a session what Orlan is and when to load the orlan skill (D088, F090):
|
|
263
|
+
* hooks/orlan-intro.txt of this package. The Claude Code plugin's session-start hook prints the same
|
|
264
|
+
* file, `orlan hook start` adds it for Codex and Cursor, and the OpenCode plugin carries it.
|
|
265
|
+
*/
|
|
266
|
+
export async function orlanIntro() {
|
|
267
|
+
return (await readFile(path.join(packageRoot(), "hooks", "orlan-intro.txt"), "utf8")).trim();
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* What `orlan hook start` prints: the context lines in the JSON each harness adds to the session.
|
|
271
|
+
* Codex reads hookSpecificOutput.additionalContext of SessionStart, Cursor additional_context of
|
|
272
|
+
* sessionStart (checked 2026-09-30 against https://learn.chatgpt.com/docs/hooks and
|
|
273
|
+
* https://cursor.com/docs/hooks).
|
|
274
|
+
*/
|
|
275
|
+
export function sessionStartOutput(agent, lines) {
|
|
276
|
+
const context = lines.join("\n");
|
|
277
|
+
return JSON.stringify(agent === "codex"
|
|
278
|
+
? { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: context } }
|
|
279
|
+
: { additional_context: context });
|
|
280
|
+
}
|
|
262
281
|
/** True for a hook entry Orlan wrote: a Cursor entry with an Orlan command, or a Codex group with one. */
|
|
263
282
|
function isOrlanHook(entry) {
|
|
264
283
|
if (!entry || typeof entry !== "object")
|
|
@@ -319,16 +338,20 @@ export function opencodePluginFile() {
|
|
|
319
338
|
/** The first line of the plugin, so a disconnect removes only a file Orlan wrote. */
|
|
320
339
|
const OPENCODE_PLUGIN_MARK = "// The Orlan plugin for OpenCode (F045).";
|
|
321
340
|
/**
|
|
322
|
-
* The plugin as `orlan
|
|
323
|
-
* this CLI by full path in place of `orlan` on the PATH.
|
|
341
|
+
* The plugin as `orlan connect` writes it: opencode/orlan.js of this package, with this Node and
|
|
342
|
+
* this CLI by full path in place of `orlan` on the PATH, and the Orlan intro line (F090).
|
|
324
343
|
*/
|
|
325
344
|
export async function opencodePlugin() {
|
|
326
345
|
const source = await readFile(path.join(packageRoot(), "opencode", "orlan.js"), "utf8");
|
|
327
346
|
const line = 'const ORLAN = ["orlan"];';
|
|
328
|
-
|
|
329
|
-
|
|
347
|
+
const introLine = 'const INTRO = "";';
|
|
348
|
+
if (!source.startsWith(OPENCODE_PLUGIN_MARK) || !source.includes(line) || !source.includes(introLine)) {
|
|
349
|
+
const { name } = JSON.parse(await readFile(path.join(packageRoot(), "package.json"), "utf8"));
|
|
350
|
+
throw new OrlanError(`The OpenCode plugin of this orlan package is damaged. Install ${name} again.`);
|
|
330
351
|
}
|
|
331
|
-
return source
|
|
352
|
+
return source
|
|
353
|
+
.replace(line, `const ORLAN = ${JSON.stringify([process.execPath, cliMain()])};`)
|
|
354
|
+
.replace(introLine, `const INTRO = ${JSON.stringify(await orlanIntro())};`);
|
|
332
355
|
}
|
|
333
356
|
/** Writes the Orlan plugin into OpenCode's plugins folder, or takes it out. Returns the file. */
|
|
334
357
|
export async function setOpencodePlugin(install) {
|
|
@@ -342,65 +365,28 @@ export async function setOpencodePlugin(install) {
|
|
|
342
365
|
return file;
|
|
343
366
|
}
|
|
344
367
|
/**
|
|
345
|
-
*
|
|
346
|
-
*
|
|
368
|
+
* Removes the Orlan MCP entry that an older `orlan mcp connect` wrote (D088: agents use the orlan
|
|
369
|
+
* command, not MCP). Every other entry stays. Returns true when it removed one.
|
|
347
370
|
*/
|
|
348
|
-
export async function
|
|
349
|
-
const authorization = `Bearer ${secret}`;
|
|
371
|
+
export async function removeMcp(agent) {
|
|
350
372
|
switch (agent) {
|
|
351
373
|
case "claude-code": {
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
"add",
|
|
356
|
-
"--transport",
|
|
357
|
-
"http",
|
|
358
|
-
"--scope",
|
|
359
|
-
"user",
|
|
360
|
-
MCP_NAME,
|
|
361
|
-
url,
|
|
362
|
-
"--header",
|
|
363
|
-
`Authorization: ${authorization}`,
|
|
364
|
-
]);
|
|
365
|
-
if (!added.ok)
|
|
366
|
-
throw new OrlanError(`claude mcp add failed: ${added.output.replaceAll(secret, "orl_...")}`);
|
|
367
|
-
return "Claude Code (claude mcp add --scope user)";
|
|
368
|
-
}
|
|
369
|
-
case "codex": {
|
|
370
|
-
const file = mcpFile("codex");
|
|
371
|
-
await writeText(file, editCodexToml(await readText(file), { url, authorization }));
|
|
372
|
-
return file;
|
|
374
|
+
// With no claude command there is no Claude Code to change.
|
|
375
|
+
const removed = await runClaude(["mcp", "remove", MCP_NAME, "--scope", "user"]).catch(() => undefined);
|
|
376
|
+
return removed?.ok ?? false;
|
|
373
377
|
}
|
|
374
|
-
case "cursor": {
|
|
375
|
-
const file = mcpFile("cursor");
|
|
376
|
-
await editJson(file, "mcpServers", { url, headers: { Authorization: authorization } });
|
|
377
|
-
return file;
|
|
378
|
-
}
|
|
379
|
-
case "opencode": {
|
|
380
|
-
const file = mcpFile("opencode");
|
|
381
|
-
await editJson(file, "mcp", { type: "remote", url, headers: { Authorization: authorization }, enabled: true });
|
|
382
|
-
return file;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
/** Takes the Orlan MCP server out of the agent's settings. */
|
|
387
|
-
export async function removeMcp(agent) {
|
|
388
|
-
switch (agent) {
|
|
389
|
-
case "claude-code":
|
|
390
|
-
await runClaude(["mcp", "remove", MCP_NAME, "--scope", "user"]);
|
|
391
|
-
return;
|
|
392
378
|
case "codex": {
|
|
393
379
|
const file = mcpFile("codex");
|
|
394
380
|
const text = await readText(file);
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
381
|
+
const edited = editCodexToml(text);
|
|
382
|
+
if (edited.trimEnd() === text.trimEnd())
|
|
383
|
+
return false;
|
|
384
|
+
await writeText(file, edited);
|
|
385
|
+
return true;
|
|
398
386
|
}
|
|
399
387
|
case "cursor":
|
|
400
|
-
|
|
401
|
-
return;
|
|
388
|
+
return removeJsonKey(mcpFile("cursor"), "mcpServers");
|
|
402
389
|
case "opencode":
|
|
403
|
-
|
|
404
|
-
return;
|
|
390
|
+
return removeJsonKey(mcpFile("opencode"), "mcp");
|
|
405
391
|
}
|
|
406
392
|
}
|