@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.
@@ -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, a monitor, hooks, skills and the MCP server."
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.0",
5
- "description": "Work with the Orlan review board: the orlan command, a monitor that wakes the session when a person mentions the agent, hooks that show the session's state on the board, the skills, and the MCP server.",
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, from the terminal or over MCP.
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
- In Orlan, open the organisation menu, choose **Connect an agent**, and copy the prompt for your agent.
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 skills add --agent claude-code
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 skills add` writes the skills `orlan-review` and `orlan-versions` where the agent reads skills.
27
- - `orlan mcp connect` makes an agent token and adds the Orlan MCP server to the agent. Only an admin of
28
- the organisation connects agents. For Claude Code it also installs the Orlan plugin (below);
29
- `--no-plugin` adds only the MCP server.
30
- - `orlan status` shows who you are, the server, and each connected agent, and tests each agent's MCP
31
- connection. It ends with exit code 1 when a check fails.
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 | MCP entry |
39
+ | Agent | Skills | Also installs |
36
40
  | --- | --- | --- |
37
- | claude-code | `~/.claude/skills` | `claude mcp add --transport http --scope user orlan ...` |
38
- | codex | `~/.agents/skills` | `[mcp_servers.orlan]` in `~/.codex/config.toml` |
39
- | cursor | `~/.cursor/skills` | `mcpServers.orlan` in `~/.cursor/mcp.json` |
40
- | opencode | `~/.config/opencode/skills` | `mcp.orlan` in `~/.config/opencode/opencode.json`, and the plugin in `~/.config/opencode/plugins/orlan.js` |
41
- | other | `.agents/skills` in the folder you are in | `orlan mcp print` prints the address and the header |
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
- Without `--agent`, the CLI finds the agent that runs it from its environment.
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 mcp connect --agent claude-code` installs it too; from a checkout,
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
- - The skills `orlan-review` and `orlan-versions`.
65
- - The Orlan MCP server. Its header comes from `orlan mcp headers`, with the token from the keychain. It
66
- goes to `$ORLAN_SERVER`, else https://orlan.app: on another server, set `ORLAN_SERVER` where you start
67
- Claude Code.
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 mcp connect --agent opencode` also writes the Orlan plugin to `~/.config/opencode/plugins/orlan.js`.
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. A request wakes a session only while OpenCode runs
75
- (the TUI or `opencode serve`). Restart a running OpenCode to load the plugin. `orlan mcp disconnect
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, with no MCP session: `orlan files push` makes 2 requests (the file,
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 and `orlan mcp headers`
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, with
118
- no MCP session.
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 mcp connect --agent codex|cursor` installs them. Codex and Cursor sessions do not wake when idle:
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
- `orlan mcp headers` prints the MCP authorization header as JSON, for a `headersHelper`. The plugin's MCP
163
- server runs it.
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 (the plugin's monitor), the `next_step` lines do not say `--wait`:
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 mcp disconnect --agent <id>` revokes the agent's token and removes its MCP entry.
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, and how its MCP settings get the Orlan
3
- * server. The ids match the web app's connect dialog (agentClients in @orlan/shared).
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 name in each agent's settings. */
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 that has one the CLI edits. Checked against each tool's
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, and with the new
105
- * one when `entry` is given. Every other line stays as it was, comments too.
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, entry) {
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 lines) {
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
- let result = kept.join("\n").replace(/\n+$/, "");
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
- /** Sets or removes one key in a JSON settings file, under `section`. */
132
- async function editJson(file, section, value) {
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 = text.trim() ? JSON.parse(text) : {};
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
- "Run `orlan mcp print` and add the entry by hand.");
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] = servers;
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 mcp connect` writes it: opencode/orlan.js of this package, with this Node and
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
- if (!source.startsWith(OPENCODE_PLUGIN_MARK) || !source.includes(line)) {
329
- throw new OrlanError("The OpenCode plugin of this orlan package is damaged. Install @orlan-maker/cli again.");
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.replace(line, `const ORLAN = ${JSON.stringify([process.execPath, cliMain()])};`);
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
- * Writes the Orlan MCP server into the agent's settings, in place of an older entry. Returns what it
346
- * changed, for the person.
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 addMcp(agent, url, secret) {
349
- const authorization = `Bearer ${secret}`;
371
+ export async function removeMcp(agent) {
350
372
  switch (agent) {
351
373
  case "claude-code": {
352
- await runClaude(["mcp", "remove", MCP_NAME, "--scope", "user"]);
353
- const added = await runClaude([
354
- "mcp",
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
- if (text)
396
- await writeText(file, editCodexToml(text));
397
- return;
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
- await editJson(mcpFile("cursor"), "mcpServers", undefined);
401
- return;
388
+ return removeJsonKey(mcpFile("cursor"), "mcpServers");
402
389
  case "opencode":
403
- await editJson(mcpFile("opencode"), "mcp", undefined);
404
- return;
390
+ return removeJsonKey(mcpFile("opencode"), "mcp");
405
391
  }
406
392
  }