@orlan-maker/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/http.js CHANGED
@@ -25,13 +25,13 @@ const explained = {
25
25
  org_suspended: "This organisation is suspended.",
26
26
  topic_missing: "The agent has several topics and used none yet. Add --topic <topic id>: `orlan topics list` shows them.",
27
27
  file_too_large: "The file is larger than Orlan takes.",
28
- plan_limit_agents: "The organisation uses all the agents of the Free plan. Disconnect an agent with `orlan mcp disconnect`, or ask an admin to move the organisation to Team (organisation menu, Billing).",
29
- guest_account_agent_limit: "A guest account connects one agent. Disconnect it with `orlan mcp disconnect`, or add your email in Orlan (the bar at the top) to connect more.",
28
+ plan_limit_agents: "The organisation uses all the agents of the Free plan. Disconnect an agent with `orlan disconnect`, or ask an admin to move the organisation to Team (organisation menu, Billing).",
29
+ guest_account_agent_limit: "A guest account connects one agent. Disconnect it with `orlan disconnect`, or add your email in Orlan (the bar at the top) to connect more.",
30
30
  guest_account_file_limit: "A guest account holds 3 files, 25 MB in total. Add your email in Orlan (the bar at the top) to upload more.",
31
31
  };
32
32
  /** What to do after a named API error (F038): every error ends with a next_step line. */
33
33
  const nextSteps = {
34
- not_signed_in: "orlan auth login, then orlan mcp connect --agent <id>",
34
+ not_signed_in: "orlan auth login, then orlan connect --agent <id>",
35
35
  forbidden: "ask an admin of the organisation, or tell your person what you wanted to do",
36
36
  not_found: "orlan brief (the topics, files and requests your token can reach)",
37
37
  ref_ambiguous: "give the id from the list above, or add --topic <topic>",
@@ -56,8 +56,11 @@ const nextSteps = {
56
56
  upload_invalid: "run the command again: it sends the file again",
57
57
  upload_expired: "run the command again: it sends the file again",
58
58
  mention_invalid: "write the comment without that mention",
59
- plan_limit_agents: "orlan mcp disconnect --agent <id>, or ask an admin to move the organisation to Team",
60
- guest_account_agent_limit: "orlan mcp disconnect --agent <id>, or add your email in Orlan",
59
+ compare_not_ready: "run the command again in a few seconds",
60
+ task_invalid: "check the text, the topic, --thread and --assignee: `orlan tasks add --help`",
61
+ decision_invalid: "check the title, the topic and --thread: `orlan decisions add --help`",
62
+ plan_limit_agents: "orlan disconnect --agent <id>, or ask an admin to move the organisation to Team",
63
+ guest_account_agent_limit: "orlan disconnect --agent <id>, or add your email in Orlan",
61
64
  guest_account_file_limit: "add your email in Orlan (the bar at the top)",
62
65
  internal_error: "run the command again; when it fails again, tell your person",
63
66
  };
@@ -90,10 +93,21 @@ export function apiError(status, code) {
90
93
  return new OrlanError((code && explained[code]) ?? `Orlan answered ${status}${code ? ` (${code})` : ""}.`, code);
91
94
  }
92
95
  /** The error of an agent token that Orlan did not accept: revoked or replaced. */
93
- export const agentTokenRefused = () => new OrlanError("Orlan did not accept the agent token. It was revoked or replaced.", "not_signed_in", "orlan mcp connect --agent <id>");
96
+ export const agentTokenRefused = () => new OrlanError("Orlan did not accept the agent token. It was revoked or replaced.", "not_signed_in", "orlan connect --agent <id>");
97
+ /**
98
+ * The id of the harness session the CLI runs in (F085), as the harness's hooks report it: ORLAN_SESSION
99
+ * (the Orlan OpenCode plugin sets it), else the id that Claude Code, Codex or OpenCode puts in the
100
+ * shell. Cursor puts none there: Orlan then takes the newest open hook session of the token.
101
+ */
102
+ export function harnessSession(env = process.env) {
103
+ return [env.ORLAN_SESSION, env.CLAUDE_CODE_SESSION_ID, env.CODEX_THREAD_ID, env.OPENCODE_SESSION_ID]
104
+ .map((value) => value?.trim())
105
+ .find((value) => value)
106
+ ?.slice(0, 200);
107
+ }
94
108
  /**
95
109
  * The stateless calls of the daily commands (F038): each is one plain HTTP request with the agent
96
- * token. No MCP session: no initialize, no DELETE, no session row. Orlan writes each call in the
110
+ * token. No session to open or close, and no session row. Orlan writes each call in the
97
111
  * audit log like any agent call. Human ids (#12, v5, a file or topic name) work as input.
98
112
  */
99
113
  export class AgentApi {
@@ -127,9 +141,17 @@ export class AgentApi {
127
141
  }
128
142
  return json;
129
143
  }
130
- /** Calls an agent tool with its input and returns its JSON answer. A refusal throws with its code. */
144
+ /**
145
+ * Calls an agent tool with its input and returns its JSON answer. A refusal throws with its code. The
146
+ * call names the harness session it runs in, so the board shows it on that agent (F085).
147
+ */
131
148
  call(tool, input = {}) {
132
- return this.send(new URL(`/api/agent/tools/${encodeURIComponent(tool)}`, this.server), { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(input) }, tool);
149
+ const session = harnessSession();
150
+ return this.send(new URL(`/api/agent/tools/${encodeURIComponent(tool)}`, this.server), {
151
+ method: "POST",
152
+ headers: { "content-type": "application/json", ...(session ? { "x-orlan-session": session } : {}) },
153
+ body: JSON.stringify(input),
154
+ }, tool);
133
155
  }
134
156
  /** GET an agent route, with its query. */
135
157
  get(pathname, query, what) {
package/dist/main.js CHANGED
@@ -8,17 +8,16 @@ import { fileURLToPath } from "node:url";
8
8
  import { parseArgs } from "node:util";
9
9
  import { commands } from "./commands.js";
10
10
  import { OrlanError } from "./http.js";
11
- const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
11
+ const { name, version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
12
12
  function overview() {
13
13
  const width = Math.max(...Object.keys(commands).map((name) => name.length));
14
14
  return [
15
15
  "orlan - connect an agent to Orlan, and work with its topics, files and comments.",
16
16
  "",
17
17
  "Set up:",
18
- " npm i -g @orlan-maker/cli",
18
+ ` npm i -g ${name}`,
19
19
  " orlan auth login",
20
- " orlan skills add --agent <id>",
21
- " orlan mcp connect --agent <id>",
20
+ " orlan connect --agent <id>",
22
21
  "",
23
22
  "Commands:",
24
23
  ...Object.entries(commands).map(([name, command]) => ` ${name.padEnd(width)} ${command.summary}`),
package/dist/skills.js CHANGED
@@ -5,7 +5,7 @@
5
5
  import { mkdir, readFile, writeFile } from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import { skillsDir } from "./agents.js";
8
- export const skillNames = ["orlan-review", "orlan-versions"];
8
+ export const skillNames = ["orlan", "orlan-review", "orlan-versions"];
9
9
  /** Writes every Orlan skill for the agent. Returns the folders it wrote. */
10
10
  export async function writeSkills(agent, cwd = process.cwd()) {
11
11
  const root = skillsDir(agent, cwd);
@@ -0,0 +1 @@
1
+ This session is connected to Orlan, the shared board of the team. "The canvas", "the board" and "the topic" mean the Orlan topic that orlan brief names. When the person asks for a diagram, a page, a deck, a mock or another file for people to see, or names the canvas or the board, load the orlan skill first and do what it says.
@@ -5,7 +5,10 @@
5
5
  orlan="$CLAUDE_PLUGIN_ROOT/bin/orlan"
6
6
  "$orlan" hook working --agent claude-code
7
7
  if "$orlan" brief --agent claude-code 2>&1; then
8
- echo "The Orlan plugin's monitor runs orlan wait --follow for this session: each request comes as a notification. Do not run orlan wait yourself."
8
+ # This line tells the session what Orlan is and when to load the orlan skill (D088, F086). Codex,
9
+ # Cursor and OpenCode read the same file (F090).
10
+ cat "$CLAUDE_PLUGIN_ROOT/hooks/orlan-intro.txt"
11
+ echo "orlan inbox lists the open requests. orlan wait waits for the next request and blocks the turn: run it only when the person asks for that."
9
12
  echo "Answer a request with the orlan command in few steps: orlan files pull <file id> --out <path> && cat <path>; edit the file; orlan respond <request id> --file <path> [--to <file id>] --message \"<what changed>\"."
10
13
  fi
11
14
  exit 0
package/opencode/orlan.js CHANGED
@@ -1,5 +1,5 @@
1
- // The Orlan plugin for OpenCode (F045). `orlan mcp connect --agent opencode` writes it to OpenCode's
2
- // plugins folder, and `orlan mcp disconnect --agent opencode` takes it out.
1
+ // The Orlan plugin for OpenCode (F045). `orlan connect --agent opencode` writes it to OpenCode's
2
+ // plugins folder, and `orlan disconnect --agent opencode` takes it out.
3
3
  //
4
4
  // OpenCode has no stop hook and no monitor: a turn that ends raises `session.idle`, and a plugin may
5
5
  // answer it with a new prompt. So when a session goes idle, this plugin runs `orlan hook wait`. When a
@@ -7,12 +7,16 @@
7
7
  // session with prompt_async: a new turn starts. When the session works again before a request
8
8
  // comes, the plugin ends the wait, and an untaken request stays in the inbox.
9
9
  // The board shows the session's state from OpenCode's events through `orlan hook <state>`.
10
+ // OpenCode has no session start hook that adds context: the plugin adds the Orlan intro line (what
11
+ // Orlan is, F090) to the system prompt of each session's model call, with experimental.chat.system.transform.
10
12
  import { spawn } from "node:child_process";
11
13
 
12
- // The orlan command. `orlan mcp connect` writes this Node and this CLI by full path here, because
14
+ // The orlan command. `orlan connect` writes this Node and this CLI by full path here, because
13
15
  // OpenCode started from the desktop often has no `orlan` on its PATH.
14
16
  const ORLAN = ["orlan"];
15
17
  const AGENT = ["--agent", "opencode"];
18
+ // The Orlan intro line, hooks/orlan-intro.txt of the orlan package. `orlan connect` writes it here.
19
+ const INTRO = "";
16
20
 
17
21
  /** Runs `orlan <args>` with the JSON the hook reads on standard input. */
18
22
  function orlan(args, input) {
@@ -74,6 +78,14 @@ export const OrlanPlugin = async ({ client }) => {
74
78
  };
75
79
 
76
80
  return {
81
+ // What Orlan is, in the system prompt of a session (a title or other call with no session gets nothing).
82
+ "experimental.chat.system.transform": async (input, output) => {
83
+ if (input.sessionID && INTRO) output.system.push(INTRO);
84
+ },
85
+ // The orlan commands of the agent's shell name its session (F085): the parent of a subagent's session.
86
+ "shell.env": async (input, output) => {
87
+ if (input.sessionID) output.env.ORLAN_SESSION = root(input.sessionID);
88
+ },
77
89
  event: async ({ event }) => {
78
90
  const properties = event.properties ?? {};
79
91
  switch (event.type) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orlan-maker/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The orlan command: connect Claude Code, Codex, Cursor or OpenCode to Orlan, and work with its topics, files and comments from the terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,10 +12,8 @@
12
12
  "skills",
13
13
  "bin",
14
14
  "hooks",
15
- "monitors",
16
15
  "opencode",
17
16
  ".claude-plugin",
18
- ".mcp.json",
19
17
  "README.md",
20
18
  "LICENSE"
21
19
  ],
@@ -28,19 +26,12 @@
28
26
  },
29
27
  "homepage": "https://orlan.app",
30
28
  "publishConfig": {
31
- "access": "public",
32
- "provenance": true
33
- },
34
- "devDependencies": {
35
- "@orlan/api": "0.1.0",
36
- "@orlan/db": "0.1.0",
37
- "@orlan/files": "0.1.0",
38
- "@orlan/shared": "0.1.0",
39
- "@orlan/sync": "0.1.0",
40
- "@orlan/worker": "0.1.0"
29
+ "access": "public"
41
30
  },
42
31
  "scripts": {
43
32
  "build": "node scripts/build.mjs",
33
+ "release": "node --env-file-if-exists=../../.env scripts/publish.mjs",
34
+ "prepack": "node scripts/build.mjs",
44
35
  "typecheck": "tsc -p ."
45
36
  }
46
- }
37
+ }
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: orlan
3
+ description: Orlan is the shared board of a team and its AI agents. Each Orlan topic has one board (people call it the canvas) with files (HTML, PDF, PowerPoint, images), notes, comments, tasks and votes. Use when Orlan is connected and the person says the canvas, the board, the topic, "in Orlan" or "on Orlan", asks what is on the board, or asks for a diagram, page, deck, mock or other file for people to see or review. Also use for a comment, a version or a request on Orlan. Do not use for work that has no link to the board, such as a sum, a question on code, or a fix in a local file that the person names.
4
+ ---
5
+
6
+ # Orlan
7
+
8
+ Orlan is one board for a team and its AI agents. A person opens a topic. The topic's board holds
9
+ files (HTML, PDF, PowerPoint, images) as page tiles, and notes, text, frames and arrows. People
10
+ comment on regions of pages, vote and set tasks. You work on the same board: you read it, post
11
+ files and versions, reply to comments and resolve them.
12
+
13
+ ## The words people use
14
+
15
+ When Orlan is connected, these words mean the Orlan topic. Do not ask which one the person means.
16
+
17
+ - "the canvas", "the board", "the topic", "the whiteboard" - the board of your Orlan topic.
18
+ - "in Orlan", "on Orlan", "on the board", "put it up", "share it" - do it on the topic.
19
+ - "the file", "the deck", "the page", "the doc" with no path - a file on the topic, when one has that
20
+ name or type. A path such as `./notes.txt` is a local file.
21
+ - "the comments", "the feedback", "the review" - the open threads on the topic.
22
+
23
+ `orlan brief` names your topic. With one topic, it is the one. With more, it is the one you used last,
24
+ or the one the person names.
25
+
26
+ ## When to use Orlan
27
+
28
+ - The person asks what is on the canvas or the board: run `orlan board` (every item with its id and
29
+ place, the timer, the vote and the open tasks), read the files you need
30
+ (`orlan files pull <file> --out <path>`), and answer from them.
31
+ - The person asks for a diagram: a diagram on Orlan is board shapes with words and labeled arrows. Do
32
+ not post an HTML file or notes as a diagram, and do not draw it with notes, texts and arrows. This
33
+ version of the orlan command cannot draw shapes yet: tell the person so, and ask what they want
34
+ instead.
35
+ - The person asks for a note, a text, a frame or an arrow on the board: `orlan board add`. Change one
36
+ with `orlan board change <item>`, delete one with `orlan board delete <item>`.
37
+ - The person asks you to make something else that people look at: a page, a mock, a deck, a report, a
38
+ chart. Make the file, check it, then post it to the topic:
39
+ `orlan files add <path>`. When a local rule says to keep visuals as local files, keep the local file
40
+ and post it too. Tell the person that it is on the board, and give its name and version.
41
+ - The person asks you to change a file that is on the topic: pull it with `--edit`, change it, and post
42
+ a new version: `orlan files push <file> <path> --changelog "<what changed>"`.
43
+ - A comment or a request names you: follow the `orlan-review` skill.
44
+
45
+ ## When not to use Orlan
46
+
47
+ Do not call Orlan for work that has no link to the board: a sum, a general question, a question on
48
+ code, a fix in a local file the person names, a test run. Do the work and answer. Do not post a file
49
+ the person did not ask for.
50
+
51
+ ## The loop
52
+
53
+ 1. Brief: `orlan brief` - the topic, its files and versions, your open requests, the next step.
54
+ 2. Pull: `orlan files pull <file> --edit --out <path>` for a file you change.
55
+ 3. Work: make or change the file, and check it.
56
+ 4. Post: `orlan files add <path>` for a new file, or `orlan files push <file> <path> --changelog "..."`
57
+ for a new version. A version waits for a person to make it current.
58
+ 5. Reply: say what you did in the thread (`orlan comment <thread> "<text>"`), or answer a request with
59
+ `orlan respond`. Say in your answer what is now on the board.
60
+
61
+ ## More commands
62
+
63
+ - `orlan board` - the board: each item with its id, type, place and text, the timer, the votes and
64
+ the open tasks.
65
+ - `orlan board add note|text|frame "<text>" --at <x,y>`, `orlan board add arrow --from <item> --to <item>`,
66
+ `orlan board change <item>`, `orlan board delete <item>` - change the board. A person can undo each
67
+ change. `orlan board edit '<JSON>'` makes several changes in one step.
68
+ - `orlan tasks add "<text>"` and `orlan tasks change <task id> --done` - the topic's tasks.
69
+ - `orlan decisions add "<title>" --body "<text>"` - records a decision the people made.
70
+ - `orlan search "<words>"` - finds topic and file names, page text, comments and decisions.
71
+
72
+ Run `orlan <command> --help` for the options of a command.
73
+
74
+ ## Examples
75
+
76
+ - "what is in the canvas?" - run `orlan brief`, pull the files, and say what the board holds: the topic,
77
+ each file and what it shows, and the open comments.
78
+ - "create a diagram showing how to make a full stack app" - say that this version of the orlan command
79
+ cannot draw board shapes yet, and ask what they want instead. Do not post an HTML file or notes.
80
+ - "put the notes on the board" - post the notes as a file, or add them as a note:
81
+ `orlan board add note "<text>" --at <x,y>`, at a free place that `orlan board` shows.
82
+ - "what is 17*23?" - answer 391. No Orlan call.
83
+ - "fix the typo in ./notes.txt" - fix the local file. No Orlan call.
84
+
85
+ ## More
86
+
87
+ - Comments, threads and requests: the `orlan-review` skill.
88
+ - Files and versions (pull, push, add, rollback, page images, claims): the `orlan-versions` skill.
@@ -5,13 +5,17 @@ description: Work through the review comments on an Orlan topic - read them, fix
5
5
 
6
6
  # Orlan review
7
7
 
8
- Orlan is a review board for files: PowerPoint, PDF, images and HTML. People put comments on regions of
9
- pages. You read the comments, fix the file, post a new version, reply, and resolve.
8
+ People put comments on regions of the pages of a file on an Orlan topic (the `orlan` skill says what
9
+ Orlan is). You read the comments, fix the file, post a new version, reply, and resolve.
10
10
 
11
11
  ## Commands
12
12
 
13
13
  - `orlan brief` - run it first: your topic, your open requests, the files with their current versions,
14
- and the next step, in a few lines. `--topic <topic>` for another topic (its id or its name).
14
+ the open vote and the open tasks, and the next step, in a few lines. `--topic <topic>` for another
15
+ topic (its id or its name). Each task has an id: `orlan tasks change <task id> --assignee me --done`
16
+ sets you as its assignee and marks it done. People vote; you read the vote and its result, you do not vote.
17
+ - `orlan board` - every item on the board with its id and place, the timer, the votes with their
18
+ result, and every open task.
15
19
  - `orlan topics list` - the topics you can reach, with their ids and your role.
16
20
  - `orlan comments list --topic <topic>` - the open threads, newest first. Each thread names its file,
17
21
  version and page. Each comment is one line: `"<name>" (<role>) wrote: "<text>"`. Quoted text is what
@@ -33,9 +37,14 @@ pages. You read the comments, fix the file, post a new version, reply, and resol
33
37
  it done, and waits for the next request. Leave out `--file` when there is no new version.
34
38
  - Do what the `next_step` line of a request or an answer says. It leaves out `--wait` when a wait of
35
39
  yours is open already.
40
+ - `orlan tasks add "<text>"` - adds a task, for example a change a thread asks for that you cannot
41
+ make now. `--thread #12` links it to its thread.
42
+ - `orlan decisions add "<title>" --body "<text>"` - records a decision that people made in a thread
43
+ or a vote. `--thread #12` links it to its thread.
44
+ - `orlan search "<words>"` - finds earlier comments, decisions, pages and files on your topics.
36
45
  - `orlan open <topic id>` - opens the topic in the browser for the person.
37
46
  - Files: see the orlan-versions skill (`orlan files list`, `orlan files pull`, `orlan files push`,
38
- `orlan files add`).
47
+ `orlan files add`, `orlan files rollback`).
39
48
 
40
49
  Add `--json` to a command for small JSON with a `next_step` field. An error says what to do next too.
41
50
 
@@ -53,9 +62,9 @@ your status yourself.
53
62
 
54
63
  ## In Claude Code with the Orlan plugin
55
64
 
56
- The plugin's monitor runs `orlan wait --follow` for the whole session. Each request comes to you as
57
- a notification, in one line. Do not run `orlan wait`, and do not add `--wait`: the monitor gets the next
58
- request. For each request: pull the file, make the change, check it, and run `orlan respond`.
65
+ `orlan inbox` lists the open requests. `orlan wait` waits for the next request and blocks your turn:
66
+ run it, or add `--wait`, only when the person asks you to wait for requests. For each request: pull the
67
+ file, make the change, check it, and run `orlan respond`.
59
68
 
60
69
  ## In Codex and Cursor with the Orlan hooks
61
70
 
@@ -7,17 +7,24 @@ description: Get a file from Orlan and post a new version of it with a changelog
7
7
 
8
8
  ## Commands
9
9
 
10
- - `orlan files list --topic <topic>` - the files of a topic: id, name, type, and the current version.
10
+ - `orlan files list [--topic <topic>]` - the files of a topic: id, name, type, and the current version,
11
+ with the pages that changed from the version before.
11
12
  - `orlan files pull <file>` - downloads the current version to the folder you are in, with its own
12
13
  name. `--version 5` (or `v5`) gets an older version, `--out <path>` names the file. Add `--edit` when
13
14
  you pull it to change it: other agents and people then see your claim on the file for 15 minutes
14
15
  (`--ttl` changes it). When another agent has a claim, the answer says who and what to do next.
16
+ - `orlan files pull <file> --pages 2,5` - saves the image of each page (`<name>-page-<n>.png`, in the
17
+ folder `--out` names) and prints its text, not the file. `--since 4` gets the pages that changed
18
+ since v4. Look at the images to see a page as people see it.
15
19
  - `orlan files push <file> <path> --changelog "<what changed>"` - posts the file as a new version.
16
20
  It must be the same type of file. The CLI sends it in one request, so a large file works too.
17
21
  It ends your claim. When a version landed after the one you pulled, Orlan refuses it with
18
22
  `stale_base` and that version's changelog: pull the newest version, make your change on it, push again.
19
23
  - `orlan files add <path>` - posts a local file as a new file on your topic and puts it on the board.
20
- `--topic <topic>` names the topic; the default is the topic you used last.
24
+ `--topic <topic>` names the topic; the default is the topic you used last. `--at <x,y>` puts it at
25
+ that point of the board (`orlan board` shows where the items are).
26
+ - `orlan files rollback <file> --version 3 --changelog "<why>"` - posts an older version again, as the
27
+ newest version. It waits for a person to make it current, as a push does.
21
28
  - `<file>` is the file id or the file name, and `<topic>` the topic id or its name. When a name is on two
22
29
  topics, the error lists both: give the id, or add `--topic`.
23
30
 
package/.mcp.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "mcpServers": {
3
- "orlan": {
4
- "type": "http",
5
- "url": "${ORLAN_SERVER:-https://orlan.app}/mcp",
6
- "headersHelper": "\"${CLAUDE_PLUGIN_ROOT}/bin/orlan\" mcp headers --agent claude-code"
7
- }
8
- }
9
- }
package/dist/mcp.js DELETED
@@ -1,113 +0,0 @@
1
- /**
2
- * A small MCP client over Streamable HTTP, for the MCP test of `orlan status` and `orlan mcp connect`:
3
- * it proves that the agent's MCP server works. The daily commands use plain HTTP (AgentApi, F038). A
4
- * test is one MCP session of the agent token: it starts the session, calls its tool, and closes it. The
5
- * client names itself "orlan-cli", so Orlan keeps these sessions off the Agents tab and sends no
6
- * "session ended" notification for them. Every call is in the audit log like any agent call.
7
- */
8
- import { OrlanError } from "./http.js";
9
- const PROTOCOL_VERSION = "2025-06-18";
10
- /** The JSON-RPC answer with this id, from a JSON body or from a server-sent event stream. */
11
- async function answerOf(response, id) {
12
- const text = await response.text();
13
- const type = response.headers.get("content-type") ?? "";
14
- const messages = type.includes("text/event-stream")
15
- ? text
16
- .split(/\r?\n\r?\n/)
17
- .map((event) => event
18
- .split(/\r?\n/)
19
- .filter((line) => line.startsWith("data:"))
20
- .map((line) => line.slice(5).trimStart())
21
- .join("\n"))
22
- .filter((data) => data.length > 0)
23
- .map((data) => JSON.parse(data))
24
- : [JSON.parse(text)];
25
- const answer = messages.find((message) => message.id === id);
26
- if (!answer)
27
- throw new OrlanError("Orlan's MCP server gave no answer.");
28
- if (answer.error)
29
- throw new OrlanError(`Orlan's MCP server refused the call: ${answer.error.message}`);
30
- return answer;
31
- }
32
- export class McpClient {
33
- sessionId;
34
- nextId = 1;
35
- url;
36
- /** The agent token: the file downloads send it as their Bearer header too. */
37
- token;
38
- version;
39
- constructor(server, token, version) {
40
- this.url = new URL("/mcp", server);
41
- this.token = token;
42
- this.version = version;
43
- }
44
- async post(body) {
45
- let response;
46
- try {
47
- response = await fetch(this.url, {
48
- method: "POST",
49
- headers: {
50
- authorization: `Bearer ${this.token}`,
51
- "content-type": "application/json",
52
- accept: "application/json, text/event-stream",
53
- ...(this.sessionId ? { "mcp-session-id": this.sessionId, "mcp-protocol-version": PROTOCOL_VERSION } : {}),
54
- },
55
- body: JSON.stringify(body),
56
- });
57
- }
58
- catch (error) {
59
- throw new OrlanError(`Orlan's MCP server at ${this.url} did not answer: ${error.message}.`);
60
- }
61
- if (response.status === 401) {
62
- throw new OrlanError("Orlan did not accept the agent token. It was revoked or replaced: run `orlan mcp connect` again.", "not_signed_in");
63
- }
64
- if (!response.ok && response.status !== 202) {
65
- throw new OrlanError(`Orlan's MCP server answered ${response.status}.`);
66
- }
67
- return response;
68
- }
69
- async request(method, params) {
70
- const id = this.nextId++;
71
- const response = await this.post({ jsonrpc: "2.0", id, method, params });
72
- if (method === "initialize")
73
- this.sessionId = response.headers.get("mcp-session-id") ?? undefined;
74
- return (await answerOf(response, id)).result;
75
- }
76
- async open() {
77
- await this.request("initialize", {
78
- protocolVersion: PROTOCOL_VERSION,
79
- capabilities: {},
80
- clientInfo: { name: "orlan-cli", version: this.version },
81
- });
82
- if (!this.sessionId)
83
- throw new OrlanError("Orlan's MCP server started no session.");
84
- await (await this.post({ jsonrpc: "2.0", method: "notifications/initialized" })).text();
85
- }
86
- /** Calls a tool and returns its JSON answer. A refusal ("Refused: <code>") throws with that code. */
87
- async call(tool, args = {}) {
88
- if (!this.sessionId)
89
- await this.open();
90
- const result = (await this.request("tools/call", { name: tool, arguments: args }));
91
- const text = result.content?.find((part) => part.type === "text")?.text ?? "";
92
- if (result.isError) {
93
- const code = /^Refused: ([a-z_]+)/.exec(text)?.[1];
94
- throw new OrlanError(`Orlan refused ${tool}: ${text.replace(/^Refused: /, "")}`, code);
95
- }
96
- return JSON.parse(text);
97
- }
98
- /** Ends the session (HTTP DELETE). */
99
- async close() {
100
- if (!this.sessionId)
101
- return;
102
- const sessionId = this.sessionId;
103
- this.sessionId = undefined;
104
- await fetch(this.url, {
105
- method: "DELETE",
106
- headers: {
107
- authorization: `Bearer ${this.token}`,
108
- "mcp-session-id": sessionId,
109
- "mcp-protocol-version": PROTOCOL_VERSION,
110
- },
111
- }).catch(() => undefined);
112
- }
113
- }
@@ -1,7 +0,0 @@
1
- [
2
- {
3
- "name": "orlan-requests",
4
- "command": "\"${CLAUDE_PLUGIN_ROOT}/bin/orlan\" wait --follow --agent claude-code",
5
- "description": "Orlan requests: a mention of this agent on the board"
6
- }
7
- ]