@matterailab/orbcode 0.3.1 → 0.3.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/README.md CHANGED
@@ -297,7 +297,8 @@ MatterAI gateway untouched.
297
297
  | `/tasks` | print the current task list |
298
298
  | `/status` | version, model, account, gateway, context usage, cost, approval modes |
299
299
  | `/usage` | fetch plan usage |
300
- | `/init` | analyze the codebase and create/improve `AGENTS.md` |
300
+ | `/init` | analyze the codebase and create/improve `AGENTS.md` in the repo's `.orb/` directory |
301
+ | `/link` | link other repos on your machine so changes here are checked against them (enter a folder path) |
301
302
  | `/mcp` | manage MCP servers — enable, disable, reconnect, view status & tool counts |
302
303
  | `/login` | start the browser sign-in flow |
303
304
  | `/logout` | remove the saved token |
@@ -1,6 +1,6 @@
1
1
  import OpenAI from "openai";
2
- import { API_GATEWAY_PATH, getUrlFromToken } from "../auth/auth.js";
3
- import { DEFAULT_HEADERS, X_AXONCODE_TASKID, X_AXON_REPO, X_ORGANIZATIONID } from "./headers.js";
2
+ import { API_GATEWAY_PATH } from "../auth/auth.js";
3
+ import { DEFAULT_HEADERS, X_AXONCODE_TASKID, X_AXON_REPO, X_ORGANIZATIONID, } from "./headers.js";
4
4
  import { stripReasoningDetails } from "./llmClient.js";
5
5
  import { getModel } from "./models.js";
6
6
  export class AxonClient {
@@ -9,13 +9,20 @@ export class AxonClient {
9
9
  constructor(options) {
10
10
  this.options = options;
11
11
  this.client = new OpenAI({
12
- baseURL: options.baseUrl || getUrlFromToken(API_GATEWAY_PATH, options.token),
12
+ // Use the gateway URL directly. Do not rehost it through
13
+ // getUrlFromToken — that helper rewrites any api.matterai.so host
14
+ // onto the control plane, which would send inference to
15
+ // api.matterai.so instead of the gateway at api2.matterai.so.
16
+ // Per-model `baseUrl` overrides (e.g. local dev) still win.
17
+ baseURL: options.baseUrl || API_GATEWAY_PATH,
13
18
  apiKey: options.token,
14
19
  defaultHeaders: DEFAULT_HEADERS,
15
20
  });
16
21
  }
17
22
  requestHeaders() {
18
- const headers = { [X_AXONCODE_TASKID]: this.options.taskId };
23
+ const headers = {
24
+ [X_AXONCODE_TASKID]: this.options.taskId,
25
+ };
19
26
  if (this.options.organizationId)
20
27
  headers[X_ORGANIZATIONID] = this.options.organizationId;
21
28
  if (this.options.repo)
@@ -27,7 +34,10 @@ export class AxonClient {
27
34
  const requestOptions = {
28
35
  model: model.id,
29
36
  temperature: 0.2,
30
- messages: [{ role: "system", content: systemPrompt }, ...stripReasoningDetails(messages)],
37
+ messages: [
38
+ { role: "system", content: systemPrompt },
39
+ ...stripReasoningDetails(messages),
40
+ ],
31
41
  stream: true,
32
42
  stream_options: { include_usage: true },
33
43
  max_tokens: model.maxOutputTokens,
@@ -48,12 +58,14 @@ export class AxonClient {
48
58
  for await (const chunk of stream) {
49
59
  // The gateway may return an error object instead of throwing.
50
60
  if ("error" in chunk) {
51
- const error = chunk.error;
61
+ const error = chunk
62
+ .error;
52
63
  const err = new Error(`Axon API Error ${error?.code ?? ""}: ${error?.message ?? "unknown error"}`);
53
64
  err.status = error?.code;
54
65
  throw err;
55
66
  }
56
- if ("provider" in chunk && typeof chunk.provider === "string") {
67
+ if ("provider" in chunk &&
68
+ typeof chunk.provider === "string") {
57
69
  inferenceProvider = chunk.provider;
58
70
  }
59
71
  if (chunk.usage) {
@@ -74,7 +86,9 @@ export class AxonClient {
74
86
  if (newText.includes("<think>")) {
75
87
  isThinking = true;
76
88
  }
77
- if (newText.includes("<think>") || newText.includes("</think>") || isThinking) {
89
+ if (newText.includes("<think>") ||
90
+ newText.includes("</think>") ||
91
+ isThinking) {
78
92
  if (newText.includes("</think>")) {
79
93
  isThinking = false;
80
94
  }
@@ -90,7 +104,8 @@ export class AxonClient {
90
104
  if (typeof deltaRecord.reasoning === "string" && deltaRecord.reasoning) {
91
105
  yield { type: "reasoning", text: deltaRecord.reasoning };
92
106
  }
93
- if (typeof deltaRecord.reasoning_content === "string" && deltaRecord.reasoning_content) {
107
+ if (typeof deltaRecord.reasoning_content === "string" &&
108
+ deltaRecord.reasoning_content) {
94
109
  yield { type: "reasoning", text: deltaRecord.reasoning_content };
95
110
  }
96
111
  if (delta.tool_calls && delta.tool_calls.length > 0) {
@@ -101,7 +116,8 @@ export class AxonClient {
101
116
  // index + argument fragments. Drop pure placeholders.
102
117
  const hasValidId = tc.id !== null && tc.id !== undefined;
103
118
  const hasValidName = !!tc.function.name;
104
- const hasArguments = typeof tc.function.arguments === "string" && tc.function.arguments.length > 0;
119
+ const hasArguments = typeof tc.function.arguments === "string" &&
120
+ tc.function.arguments.length > 0;
105
121
  return hasValidId || hasValidName || hasArguments;
106
122
  })
107
123
  .map((tc) => ({
package/dist/branding.js CHANGED
@@ -23,9 +23,9 @@ export const COLORS = {
23
23
  user: "#569CD6",
24
24
  };
25
25
  export const LOGO = `
26
- ___ _ ____ _
27
- / _ \\ _ __ | |__ / ___| ___ __| | ___
28
- | | | || '__|| '_ \\ | | / _ \\ / _\` | / _ \\
29
- | |_| || | | |_) || |___ | (_) || (_| || __/
30
- \\___/ |_| |_.__/ \\____| \\___/ \\__,_| \\___|
26
+ ___ _ _
27
+ / _ \\ _ __| |__ ___ ___ __| | ___
28
+ | | | | '__| '_ \\ / __/ _ \\ / _\` |/ _ \\
29
+ | |_| | | | |_) | (_| (_) | (_| | __/
30
+ \\___/|_| |_.__/ \\___\\___/ \\__,_|\\___|
31
31
  `;
@@ -0,0 +1,163 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ const MAX_AGENTS_CHARS = 4000;
5
+ const MAX_LINKED_REPOS = 8;
6
+ /** Where a linked repo's AGENTS.md might live, in precedence order. `.orbital`
7
+ * (the IDE extension's dir) and `.orbcode` are legacy locations, still read for
8
+ * backward compatibility and cross-tool linking. */
9
+ const AGENTS_LOCATIONS = [
10
+ path.join(".orb", "AGENTS.md"),
11
+ path.join(".orbital", "AGENTS.md"),
12
+ path.join(".orbcode", "AGENTS.md"),
13
+ "AGENTS.md",
14
+ ];
15
+ function isDir(p) {
16
+ try {
17
+ return fs.statSync(p).isDirectory();
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ /**
24
+ * The repo-level OrbCode directory for shared, tool-neutral data like AGENTS.md
25
+ * and links.json — always `.orb`. This is the folder the IDE and the CLI both
26
+ * read/write, so they stay in sync.
27
+ *
28
+ * Note: this is deliberately NOT where machine settings live. Those stay put
29
+ * (`~/.orbcode` and `<repo>/.orbcode/settings.json`); only the repo-level
30
+ * AGENTS.md/links folder moved to `.orb`. Legacy `.orbcode/AGENTS.md` files are
31
+ * still *read* (see the memory loader and AGENTS_LOCATIONS), but new files are
32
+ * written here.
33
+ */
34
+ export function resolveProjectDir(cwd) {
35
+ return path.join(cwd, ".orb");
36
+ }
37
+ function linksFilePath(cwd) {
38
+ return path.join(resolveProjectDir(cwd), "links.json");
39
+ }
40
+ /**
41
+ * Read the linked repos for a project (empty array if none / unreadable).
42
+ *
43
+ * The schema is tolerant so links written by either tool work: each entry needs
44
+ * only an `input` (the IDE extension may omit the resolved `path`); we fill the
45
+ * `path` by resolving the input when it's missing.
46
+ */
47
+ export function loadLinks(cwd = process.cwd()) {
48
+ try {
49
+ const parsed = JSON.parse(fs.readFileSync(linksFilePath(cwd), "utf8"));
50
+ if (!Array.isArray(parsed.links))
51
+ return [];
52
+ const out = [];
53
+ for (const raw of parsed.links) {
54
+ if (!raw)
55
+ continue;
56
+ const input = typeof raw.input === "string" ? raw.input : typeof raw.path === "string" ? raw.path : undefined;
57
+ if (!input)
58
+ continue;
59
+ const resolved = typeof raw.path === "string" ? raw.path : resolveLinkTarget(input);
60
+ if (!resolved)
61
+ continue;
62
+ out.push({ input, path: resolved });
63
+ }
64
+ return out;
65
+ }
66
+ catch {
67
+ return [];
68
+ }
69
+ }
70
+ function saveLinks(cwd, links) {
71
+ const file = linksFilePath(cwd);
72
+ fs.mkdirSync(path.dirname(file), { recursive: true });
73
+ fs.writeFileSync(file, JSON.stringify({ links }, null, "\t") + "\n");
74
+ }
75
+ /**
76
+ * Turn the folder path the user entered — absolute, `~/path`, or relative to
77
+ * cwd — into an absolute filesystem path. Returns undefined for empty input.
78
+ */
79
+ export function resolveLinkTarget(input) {
80
+ let value = input.trim();
81
+ if (!value)
82
+ return undefined;
83
+ if (value.startsWith("~/"))
84
+ value = path.join(os.homedir(), value.slice(2));
85
+ return path.resolve(value);
86
+ }
87
+ /** Resolve, validate and persist a new link. Idempotent on the resolved path. */
88
+ export function addLink(cwd, input) {
89
+ const resolved = resolveLinkTarget(input);
90
+ if (!resolved)
91
+ return { ok: false, message: "Enter a folder path." };
92
+ if (path.resolve(cwd) === resolved)
93
+ return { ok: false, message: "Can't link a repo to itself." };
94
+ if (!isDir(resolved))
95
+ return { ok: false, message: `Not a directory: ${resolved}` };
96
+ const links = loadLinks(cwd);
97
+ if (links.some((l) => l.path === resolved))
98
+ return { ok: false, message: "Already linked." };
99
+ if (links.length >= MAX_LINKED_REPOS) {
100
+ return { ok: false, message: `At most ${MAX_LINKED_REPOS} linked repos.` };
101
+ }
102
+ links.push({ input: input.trim(), path: resolved });
103
+ saveLinks(cwd, links);
104
+ return { ok: true, message: `Linked ${resolved}` };
105
+ }
106
+ /** Remove a link by its resolved path. */
107
+ export function removeLink(cwd, targetPath) {
108
+ const links = loadLinks(cwd).filter((l) => l.path !== targetPath);
109
+ saveLinks(cwd, links);
110
+ }
111
+ /** First AGENTS.md found inside a linked repo, or undefined. */
112
+ function readLinkedAgents(repo) {
113
+ for (const rel of AGENTS_LOCATIONS) {
114
+ try {
115
+ const text = fs.readFileSync(path.join(repo, rel), "utf8");
116
+ if (text.trim())
117
+ return text;
118
+ }
119
+ catch {
120
+ // try the next location
121
+ }
122
+ }
123
+ return undefined;
124
+ }
125
+ function truncate(text, max) {
126
+ return text.length <= max ? text : text.slice(0, max) + "\n… (truncated)";
127
+ }
128
+ /**
129
+ * Render the linked-repos block for the agent's environment details. Returns ""
130
+ * when nothing is linked. Each repo's AGENTS.md is pulled in (when present) so
131
+ * the model knows the linked codebase without exploring it first.
132
+ */
133
+ export function renderLinkedReposSection(cwd) {
134
+ const links = loadLinks(cwd).slice(0, MAX_LINKED_REPOS);
135
+ if (links.length === 0)
136
+ return "";
137
+ const parts = [
138
+ "## Linked Repositories",
139
+ "",
140
+ "This repository is linked to the repositories below — separate codebases on disk that are coupled to this one. When you change this repo, consider whether the change ripples into a linked repo: inspect the linked code for impact and, when relevant, propose (or make, if the user asks) the matching changes there. You can read and edit files in these repos directly by their absolute paths.",
141
+ "",
142
+ ];
143
+ for (const link of links) {
144
+ const exists = isDir(link.path);
145
+ parts.push(`### ${path.basename(link.path)} — \`${link.path}\`${exists ? "" : " (path not found)"}`);
146
+ if (!exists) {
147
+ parts.push("");
148
+ continue;
149
+ }
150
+ const agents = readLinkedAgents(link.path);
151
+ parts.push("");
152
+ if (agents) {
153
+ parts.push("Its AGENTS.md:");
154
+ parts.push("");
155
+ parts.push(truncate(agents.trim(), MAX_AGENTS_CHARS));
156
+ }
157
+ else {
158
+ parts.push("(no AGENTS.md found — explore the repo directly if you need its structure)");
159
+ }
160
+ parts.push("");
161
+ }
162
+ return parts.join("\n");
163
+ }
@@ -11,6 +11,7 @@ import { getSessionFilePath, saveSession } from "./sessions.js";
11
11
  import { HookRunner } from "./hooks.js";
12
12
  import { loadMemoryFiles } from "../memory/loader.js";
13
13
  import { loadSkills } from "../skills/loader.js";
14
+ import { renderLinkedReposSection } from "../config/links.js";
14
15
  const MAX_STEPS_PER_TURN = 50;
15
16
  const RESULT_PREVIEW_LINES = 6;
16
17
  function detectRepo(cwd) {
@@ -219,6 +220,7 @@ export class Agent {
219
220
  const timeZoneOffsetHours = Math.floor(Math.abs(timeZoneOffset));
220
221
  const timeZoneOffsetMinutes = Math.abs(Math.round((Math.abs(timeZoneOffset) - timeZoneOffsetHours) * 60));
221
222
  const timeZoneOffsetStr = `${timeZoneOffset >= 0 ? "+" : "-"}${timeZoneOffsetHours}:${timeZoneOffsetMinutes.toString().padStart(2, "0")}`;
223
+ const linkedRepos = renderLinkedReposSection(this.options.cwd);
222
224
  return `# Environment Details
223
225
 
224
226
  ## Current Workspace Directory (${this.options.cwd}) Files
@@ -226,7 +228,7 @@ ${files.join("\n") || "(empty directory)"}
226
228
  ${files.length >= 200 ? "\n(File list truncated.)" : ""}
227
229
 
228
230
  ${git}
229
-
231
+ ${linkedRepos ? `\n${linkedRepos}` : ""}
230
232
  ## Current Time
231
233
  Current time in ISO 8601 UTC format: ${now.toISOString()}
232
234
  User time zone: ${timeZone}, UTC${timeZoneOffsetStr}`;
@@ -11,8 +11,8 @@ import { getConfigDir } from "../config/settings.js";
11
11
  * first), with closer-to-cwd and higher-precedence types winning:
12
12
  *
13
13
  * 1. User memory: ~/.orbcode/AGENTS.md
14
- * 2. Project memory: AGENTS.md and .orbcode/AGENTS.md in cwd and every
15
- * parent directory (closer-to-cwd wins)
14
+ * 2. Project memory: AGENTS.md and .orb/AGENTS.md (and the legacy
15
+ * .orbcode/AGENTS.md) in cwd and every parent directory (closer-to-cwd wins)
16
16
  * 3. Local memory: AGENTS.local.md in cwd and parents (highest precedence)
17
17
  *
18
18
  * Memory files support `@path` include directives (relative, `~/`, or
@@ -118,9 +118,11 @@ export function loadMemoryFiles(cwd = process.cwd()) {
118
118
  const files = [];
119
119
  // 1. User memory (~/.orbcode/AGENTS.md)
120
120
  files.push(...loadWithIncludes(path.join(getConfigDir(), "AGENTS.md"), "user", processed, 0));
121
- // 2. Project memory (AGENTS.md + .orbcode/AGENTS.md), root -> cwd
121
+ // 2. Project memory (AGENTS.md + .orb/AGENTS.md), root -> cwd. The legacy
122
+ // .orbcode/AGENTS.md location is still read for backward compatibility.
122
123
  for (const dir of ancestorDirs(cwd).reverse()) {
123
124
  files.push(...loadWithIncludes(path.join(dir, "AGENTS.md"), "project", processed, 0));
125
+ files.push(...loadWithIncludes(path.join(dir, ".orb", "AGENTS.md"), "project", processed, 0));
124
126
  files.push(...loadWithIncludes(path.join(dir, ".orbcode", "AGENTS.md"), "project", processed, 0));
125
127
  }
126
128
  // 3. Local memory (AGENTS.local.md), root -> cwd — highest precedence
package/dist/ui/App.js CHANGED
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
3
3
  import { Box, Static, Text, useApp, useInput } from "ink";
4
4
  import open from "open";
5
+ import * as path from "node:path";
5
6
  import { COLORS, VERSION } from "../branding.js";
6
7
  import { BUILTIN_AXON_MODELS, getModel, isValidAxonModel, } from "../api/models.js";
7
8
  import { LoginView } from "./LoginView.js";
@@ -23,6 +24,8 @@ import { ModelPicker } from "./components/ModelPicker.js";
23
24
  import { SessionPicker } from "./components/SessionPicker.js";
24
25
  import { listSessions } from "../core/sessions.js";
25
26
  import { RowView } from "./components/rows.js";
27
+ import { LinkManager } from "./components/LinkManager.js";
28
+ import { addLink, loadLinks, removeLink, resolveProjectDir, } from "../config/links.js";
26
29
  const SLASH_COMMANDS = [
27
30
  { name: "/help", description: "show available commands" },
28
31
  { name: "/model", description: "select the Axon model to use" },
@@ -46,6 +49,10 @@ const SLASH_COMMANDS = [
46
49
  name: "/init",
47
50
  description: "analyze this codebase and create an AGENTS.md",
48
51
  },
52
+ {
53
+ name: "/link",
54
+ description: "link other repos so changes here are checked against them",
55
+ },
49
56
  {
50
57
  name: "/mcp",
51
58
  description: "manage MCP servers — enable, disable, reconnect, view status",
@@ -117,13 +124,21 @@ function usageLines(profile) {
117
124
  }
118
125
  return lines;
119
126
  }
120
- const INIT_PROMPT = `Analyze this codebase and create an AGENTS.md file containing:
121
- 1. A short overview of what the project does
122
- 2. Build, run, lint and test commands
123
- 3. Architecture and code structure (key directories and what lives in them)
124
- 4. Code style conventions used in this repo (imports, formatting, naming, error handling)
127
+ function buildInitPrompt(agentsPath) {
128
+ return `Analyze this codebase and write a concise AGENTS.md that reduces cold-start for future coding sessions. Create or update the file at exactly this path:
129
+
130
+ ${agentsPath}
131
+
132
+ Investigate first — read the directory layout, key config files, and a few representative source files — then write. Keep it under ~150 lines so it stays cheap to include in every future prompt. Cover, briefly:
133
+ 1. What the project does (1-2 lines) and its main tech stack.
134
+ 2. Project structure — the key directories/files and what each is responsible for.
135
+ 3. Architecture — how the main pieces fit together (entry points, data/control flow).
136
+ 4. Business-logic / domain mapping — where the core domain concepts live in the code.
137
+ 5. Notable code patterns and conventions to follow (imports, naming, error handling, tests).
138
+ 6. The common build, run, lint and test commands.
125
139
 
126
- If an AGENTS.md already exists, improve it. Keep it under ~60 lines so it is cheap to include in future prompts.`;
140
+ Favor durable facts over volatile detail. If an AGENTS.md already exists at that path, refine it rather than rewriting from scratch.`;
141
+ }
127
142
  // Ported from the Orbital extension's commit slash command (commitCommandResponse).
128
143
  const buildCommitPrompt = (userInput) => `The user has explicitly asked you to check pending changes and generate detailed commit messages. You MUST now help them with this.
129
144
 
@@ -195,6 +210,9 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
195
210
  const [queuedMessages, setQueuedMessages] = useState([]);
196
211
  const [modelPickerOpen, setModelPickerOpen] = useState(false);
197
212
  const [resumableSessions, setResumableSessions] = useState(null);
213
+ const [linkManagerOpen, setLinkManagerOpen] = useState(false);
214
+ const [links, setLinks] = useState([]);
215
+ const [linkStatus, setLinkStatus] = useState("");
198
216
  // MCP manager (created once, shared across agents in this process). Null until
199
217
  // the first agent is created so we don't spawn servers before login.
200
218
  const mcpManagerRef = useRef(null);
@@ -633,7 +651,7 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
633
651
  }
634
652
  break;
635
653
  }
636
- case "/init":
654
+ case "/init": {
637
655
  if (!getAuthToken(settings)) {
638
656
  setView("login");
639
657
  break;
@@ -641,7 +659,14 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
641
659
  pushRow({ kind: "user", text: "/init" });
642
660
  setBusy(true);
643
661
  setBusyLabel("Thinking");
644
- void getAgent().runTurn(INIT_PROMPT);
662
+ const agentsPath = path.join(resolveProjectDir(process.cwd()), "AGENTS.md");
663
+ void getAgent().runTurn(buildInitPrompt(agentsPath));
664
+ break;
665
+ }
666
+ case "/link":
667
+ setLinks(loadLinks(process.cwd()));
668
+ setLinkStatus("");
669
+ setLinkManagerOpen(true);
645
670
  break;
646
671
  case "/mcp": {
647
672
  const manager = mcpManagerRef.current;
@@ -995,14 +1020,24 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
995
1020
  !modelPickerOpen &&
996
1021
  !mcpPickerOpen &&
997
1022
  !mcpMigrationEntries &&
998
- !resumableSessions;
1023
+ !resumableSessions &&
1024
+ !linkManagerOpen;
999
1025
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: rows, children: (row) => _jsx(RowView, { row: row }, row.id) }, staticKey), view === "login" ? (_jsx(LoginSection, { onLogin: handleLogin })) : (_jsxs(Box, { flexDirection: "column", children: [updateInfo?.updateAvailable && updateInfo.latest && (_jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: COLORS.warning, paddingX: 2, alignSelf: "flex-start", children: [_jsxs(Text, { color: COLORS.warning, bold: true, children: ["\u2191 Update available: v", updateInfo.current, " \u2192 v", updateInfo.latest] }), _jsxs(Text, { children: ["Run ", _jsx(Text, { color: COLORS.accent, children: "orbcode update" }), " to install the latest version, then relaunch."] })] })), streamingReasoning && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: COLORS.thinking, italic: true, children: "\u2726 Thinking\u2026" }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, italic: true, children: tail(streamingReasoning, expandReasoningRef.current ? 30 : 3) }) })] })), streamingText && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: COLORS.primary, children: "\u25CF " }), streamingText] }) })), taskLines.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: "Tasks" }), taskLines.slice(0, 10).map((line, i) => (_jsx(Text, { dimColor: /^[-*]\s*\[x\]/i.test(line), children: line
1000
1026
  .replace(/^[-*]\s*\[x\]/i, " ■")
1001
1027
  .replace(/^[-*]\s*\[-\]/, " ◧")
1002
1028
  .replace(/^[-*]\s*\[ \]/, " □") }, i))), taskLines.length > 10 && (_jsxs(Text, { dimColor: true, children: [" \u2026 ", taskLines.length - 10, " more"] }))] })), modelPickerOpen && (_jsx(Box, { marginTop: 1, children: _jsx(ModelPicker, { currentId: settings.model, onSelect: (modelId) => {
1003
1029
  setModelPickerOpen(false);
1004
1030
  switchModel(modelId);
1005
- }, onCancel: () => setModelPickerOpen(false) }) })), resumableSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: resumableSessions, onSelect: handleResume, onCancel: () => setResumableSessions(null) }) })), pendingHookTrust && (_jsx(Box, { marginTop: 1, children: _jsx(HookTrustPrompt, { cwd: process.cwd(), commands: pendingHookTrust.commands, onDecision: resolveHookTrust }) })), pendingMcpApproval && (_jsx(Box, { marginTop: 1, children: _jsx(McpApprovalPrompt, { serverNames: pendingMcpApproval, onApprove: resolveMcpApproval }) })), mcpPickerOpen && mcpManagerRef.current && (_jsx(Box, { marginTop: 1, children: _jsx(McpPicker, { manager: mcpManagerRef.current, onChanged: () => {
1031
+ }, onCancel: () => setModelPickerOpen(false) }) })), resumableSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: resumableSessions, onSelect: handleResume, onCancel: () => setResumableSessions(null) }) })), linkManagerOpen && (_jsx(Box, { marginTop: 1, children: _jsx(LinkManager, { links: links, status: linkStatus, onAdd: (input) => {
1032
+ const result = addLink(process.cwd(), input);
1033
+ setLinkStatus(result.message);
1034
+ if (result.ok)
1035
+ setLinks(loadLinks(process.cwd()));
1036
+ }, onRemove: (link) => {
1037
+ removeLink(process.cwd(), link.path);
1038
+ setLinks(loadLinks(process.cwd()));
1039
+ setLinkStatus(`Unlinked ${path.basename(link.path)}`);
1040
+ }, onClose: () => setLinkManagerOpen(false) }) })), pendingHookTrust && (_jsx(Box, { marginTop: 1, children: _jsx(HookTrustPrompt, { cwd: process.cwd(), commands: pendingHookTrust.commands, onDecision: resolveHookTrust }) })), pendingMcpApproval && (_jsx(Box, { marginTop: 1, children: _jsx(McpApprovalPrompt, { serverNames: pendingMcpApproval, onApprove: resolveMcpApproval }) })), mcpPickerOpen && mcpManagerRef.current && (_jsx(Box, { marginTop: 1, children: _jsx(McpPicker, { manager: mcpManagerRef.current, onChanged: () => {
1006
1041
  const m = mcpManagerRef.current;
1007
1042
  if (m)
1008
1043
  saveMcpApproval(process.cwd(), m.getEnabled(), m.getDisabled());
@@ -0,0 +1,65 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { COLORS } from "../../branding.js";
5
+ /**
6
+ * Interactive manager for the `/link` command. The last row is a free-text
7
+ * input where the user types a folder path to add; existing links above it can
8
+ * be selected and removed. Mirrors FollowupPrompt's "input is the final virtual
9
+ * row" pattern.
10
+ */
11
+ export function LinkManager({ links, status, onAdd, onRemove, onClose }) {
12
+ const [selected, setSelected] = useState(links.length);
13
+ const [draft, setDraft] = useState("");
14
+ const inputRow = links.length; // the free-text row sits after every link
15
+ const rowCount = links.length + 1;
16
+ const sel = Math.min(selected, inputRow); // clamp: list shrinks on removal
17
+ const isInput = sel === inputRow;
18
+ useInput((input, key) => {
19
+ if (key.escape) {
20
+ onClose();
21
+ return;
22
+ }
23
+ if (key.upArrow) {
24
+ setSelected((s) => (Math.min(s, inputRow) - 1 + rowCount) % rowCount);
25
+ return;
26
+ }
27
+ if (key.downArrow) {
28
+ setSelected((s) => (Math.min(s, inputRow) + 1) % rowCount);
29
+ return;
30
+ }
31
+ if (key.return) {
32
+ if (isInput) {
33
+ if (draft.trim()) {
34
+ onAdd(draft.trim());
35
+ setDraft("");
36
+ // Stay on the input row so several repos can be added in a row.
37
+ // rowCount == the new input-row index once a link is appended.
38
+ setSelected(rowCount);
39
+ }
40
+ }
41
+ else {
42
+ onRemove(links[sel]);
43
+ setSelected(inputRow); // jump back to the input row after removing
44
+ }
45
+ return;
46
+ }
47
+ if (isInput) {
48
+ if (key.backspace || key.delete)
49
+ setDraft((d) => d.slice(0, -1));
50
+ else if (input && !key.ctrl && !key.meta)
51
+ setDraft((d) => d + input);
52
+ }
53
+ else if (input === "d" || key.backspace || key.delete) {
54
+ onRemove(links[sel]);
55
+ setSelected(inputRow);
56
+ }
57
+ });
58
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: COLORS.primary, paddingX: 1, children: [_jsx(Text, { bold: true, color: COLORS.primary, children: "Linked repositories" }), _jsx(Text, { dimColor: true, children: "Repos linked here are shared with the agent so changes can be checked across them." }), links.length === 0 && _jsx(Text, { dimColor: true, children: " (none yet)" }), links.map((link, index) => (
59
+ // truncate-start keeps the meaningful tail of long paths visible
60
+ // instead of wrapping them onto the next line.
61
+ _jsxs(Text, { color: sel === index ? COLORS.accent : undefined, wrap: "truncate-start", children: [sel === index ? "❯ " : " ", index + 1, ". ", link.path] }, link.path))), _jsxs(Text, { color: isInput ? COLORS.accent : undefined, children: [isInput ? "❯ " : " ", "add a repo (folder path):"] }), isInput && (
62
+ // The typed/pasted path lives on its own line and truncates from
63
+ // the start, so a long path shows its tail + cursor without wrapping.
64
+ _jsxs(Text, { wrap: "truncate-start", children: [" ", draft, _jsx(Text, { inverse: true, children: " " })] })), status && _jsx(Text, { dimColor: true, children: status }), _jsx(Text, { dimColor: true, children: "\u2191/\u2193 select \u00B7 enter add/remove \u00B7 d remove \u00B7 esc done" })] }));
65
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matterailab/orbcode",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "OrbCode CLI — agentic coding in your terminal, powered by Axon models by MatterAI",
5
5
  "type": "module",
6
6
  "bin": {