From e62526a760cdbbd6944ea2ccb2e86a05f1b6dd3a Mon Sep 17 00:00:00 2001 From: code-crusher Date: Tue, 30 Jun 2026 11:00:38 +0530 Subject: [PATCH 1/2] release: v0.3.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /link slash command — link other repos on the same machine so changes here are checked against them. Links live in .orb/links.json (shared with the IDE) and each linked repo's AGENTS.md is injected into the environment details. - /init now writes to .orb/AGENTS.md and the prompt is rewritten to target cold-start (project structure, architecture, business-logic mapping, conventions — what an agent needs to start coding without re-exploring). - Repo-level agent data moves to .orb/ (shared with the IDE); machine config stays at ~/.orbcode and /.orbcode/settings.json. Legacy .orbcode/AGENTS.md is still read for backward compatibility. - Tweak CLI logo spacing. --- .gitignore | 10 +- CHANGELOG.md | 28 +++++ README.md | 3 +- package.json | 2 +- src/branding.ts | 10 +- src/config/links.ts | 186 ++++++++++++++++++++++++++++++ src/core/agent.ts | 4 +- src/memory/loader.ts | 8 +- src/ui/App.tsx | 72 ++++++++++-- src/ui/components/LinkManager.tsx | 100 ++++++++++++++++ 10 files changed, 400 insertions(+), 23 deletions(-) create mode 100644 src/config/links.ts create mode 100644 src/ui/components/LinkManager.tsx diff --git a/.gitignore b/.gitignore index ac3eff6..1ea44a3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,11 @@ build/ coverage/ .cache/ -# Per-developer project memory loaded by the agent at runtime. -# The .orbcode/ directory itself is a valid project-config location -# (project-level settings.json), so only the personal AGENTS.md is excluded. +# Repo-level agent data in .orb/ (shared by the IDE and the CLI). AGENTS.md is +# per-developer project memory and links.json holds machine-specific absolute +# paths, so neither is committed. The .orbcode/ directory is unchanged — it's +# still a valid project-config location (project-level settings.json); only the +# legacy personal AGENTS.md there is excluded. +.orb/AGENTS.md +.orb/links.json .orbcode/AGENTS.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 1abcbee..11f8bb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- **Linked repositories (`/link`).** A new `/link` slash command opens an + interactive manager where you point this repo at other repos on your machine + (enter a folder path — absolute, `~/path`, or relative to the project). Links + are persisted per-project in + `.orb/links.json` and injected into the agent's environment details — + including each linked repo's `AGENTS.md`, pulled in ahead of time — so a + change here is checked for impact on, or propagated to, the linked repos. + `.orb/links.json` is shared with the Orbital IDE extension (links written + there are honored here, and vice versa), and a linked repo's `AGENTS.md` is + read from `.orb/`, `.orbital/`, or `.orbcode/`. + +### Changed + +- **`/init` now writes to `.orb/AGENTS.md` and targets cold-start.** The + generated `AGENTS.md` is written to the repo-level `.orb/` directory and now + captures project structure, architecture, business-logic mapping, and code + patterns/conventions — the context an agent needs to start coding without + re-exploring. +- **Repo-level agent data lives in `.orb/`.** The folder OrbCode creates in a + project for `AGENTS.md` (and now `links.json`) is `.orb/` — a single, + tool-neutral name shared by the IDE and the CLI. Machine settings are + unchanged (`~/.orbcode` and `/.orbcode/settings.json` stay put); the + legacy `.orbcode/AGENTS.md` location is still read for backward compatibility. + ## [0.3.2] - 2026-06-24 ### Fixed diff --git a/README.md b/README.md index 8f06574..2068328 100644 --- a/README.md +++ b/README.md @@ -297,7 +297,8 @@ MatterAI gateway untouched. | `/tasks` | print the current task list | | `/status` | version, model, account, gateway, context usage, cost, approval modes | | `/usage` | fetch plan usage | -| `/init` | analyze the codebase and create/improve `AGENTS.md` | +| `/init` | analyze the codebase and create/improve `AGENTS.md` in the repo's `.orb/` directory | +| `/link` | link other repos on your machine so changes here are checked against them (enter a folder path) | | `/mcp` | manage MCP servers — enable, disable, reconnect, view status & tool counts | | `/login` | start the browser sign-in flow | | `/logout` | remove the saved token | diff --git a/package.json b/package.json index 2b4b6a8..59335d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@matterailab/orbcode", - "version": "0.3.2", + "version": "0.3.3", "description": "OrbCode CLI — agentic coding in your terminal, powered by Axon models by MatterAI", "type": "module", "bin": { diff --git a/src/branding.ts b/src/branding.ts index fb38bcb..d569250 100644 --- a/src/branding.ts +++ b/src/branding.ts @@ -27,9 +27,9 @@ export const COLORS = { } as const; export const LOGO = ` - ___ _ ____ _ - / _ \\ _ __ | |__ / ___| ___ __| | ___ -| | | || '__|| '_ \\ | | / _ \\ / _\` | / _ \\ -| |_| || | | |_) || |___ | (_) || (_| || __/ - \\___/ |_| |_.__/ \\____| \\___/ \\__,_| \\___| + ___ _ _ + / _ \\ _ __| |__ ___ ___ __| | ___ +| | | | '__| '_ \\ / __/ _ \\ / _\` |/ _ \\ +| |_| | | | |_) | (_| (_) | (_| | __/ + \\___/|_| |_.__/ \\___\\___/ \\__,_|\\___| `; diff --git a/src/config/links.ts b/src/config/links.ts new file mode 100644 index 0000000..f984817 --- /dev/null +++ b/src/config/links.ts @@ -0,0 +1,186 @@ +import * as fs from "node:fs" +import * as os from "node:os" +import * as path from "node:path" + +/** + * Linked repositories. + * + * `/link` lets the user point this repo at other repos on their machine that + * are coupled to it (a shared API, a client/server pair, a monorepo sibling + * checked out separately, …). The links are persisted per-project and injected + * into the agent's environment details so a change here can be checked for + * impact on — or propagated to — the linked repos. + */ + +export interface LinkedRepo { + /** The folder path the user entered (absolute, `~/path`, or relative). */ + input: string + /** Absolute filesystem path the input resolved to. */ + path: string +} + +const MAX_AGENTS_CHARS = 4000 +const MAX_LINKED_REPOS = 8 +/** Where a linked repo's AGENTS.md might live, in precedence order. `.orbital` + * (the IDE extension's dir) and `.orbcode` are legacy locations, still read for + * backward compatibility and cross-tool linking. */ +const AGENTS_LOCATIONS = [ + path.join(".orb", "AGENTS.md"), + path.join(".orbital", "AGENTS.md"), + path.join(".orbcode", "AGENTS.md"), + "AGENTS.md", +] + +function isDir(p: string): boolean { + try { + return fs.statSync(p).isDirectory() + } catch { + return false + } +} + +/** + * The repo-level OrbCode directory for shared, tool-neutral data like AGENTS.md + * and links.json — always `.orb`. This is the folder the IDE and the CLI both + * read/write, so they stay in sync. + * + * Note: this is deliberately NOT where machine settings live. Those stay put + * (`~/.orbcode` and `/.orbcode/settings.json`); only the repo-level + * AGENTS.md/links folder moved to `.orb`. Legacy `.orbcode/AGENTS.md` files are + * still *read* (see the memory loader and AGENTS_LOCATIONS), but new files are + * written here. + */ +export function resolveProjectDir(cwd: string): string { + return path.join(cwd, ".orb") +} + +function linksFilePath(cwd: string): string { + return path.join(resolveProjectDir(cwd), "links.json") +} + +/** + * Read the linked repos for a project (empty array if none / unreadable). + * + * The schema is tolerant so links written by either tool work: each entry needs + * only an `input` (the IDE extension may omit the resolved `path`); we fill the + * `path` by resolving the input when it's missing. + */ +export function loadLinks(cwd = process.cwd()): LinkedRepo[] { + try { + const parsed = JSON.parse(fs.readFileSync(linksFilePath(cwd), "utf8")) as { + links?: Array> + } + if (!Array.isArray(parsed.links)) return [] + const out: LinkedRepo[] = [] + for (const raw of parsed.links) { + if (!raw) continue + const input = typeof raw.input === "string" ? raw.input : typeof raw.path === "string" ? raw.path : undefined + if (!input) continue + const resolved = typeof raw.path === "string" ? raw.path : resolveLinkTarget(input) + if (!resolved) continue + out.push({ input, path: resolved }) + } + return out + } catch { + return [] + } +} + +function saveLinks(cwd: string, links: LinkedRepo[]): void { + const file = linksFilePath(cwd) + fs.mkdirSync(path.dirname(file), { recursive: true }) + fs.writeFileSync(file, JSON.stringify({ links }, null, "\t") + "\n") +} + +/** + * Turn the folder path the user entered — absolute, `~/path`, or relative to + * cwd — into an absolute filesystem path. Returns undefined for empty input. + */ +export function resolveLinkTarget(input: string): string | undefined { + let value = input.trim() + if (!value) return undefined + if (value.startsWith("~/")) value = path.join(os.homedir(), value.slice(2)) + return path.resolve(value) +} + +export interface LinkResult { + ok: boolean + message: string +} + +/** Resolve, validate and persist a new link. Idempotent on the resolved path. */ +export function addLink(cwd: string, input: string): LinkResult { + const resolved = resolveLinkTarget(input) + if (!resolved) return { ok: false, message: "Enter a folder path." } + if (path.resolve(cwd) === resolved) return { ok: false, message: "Can't link a repo to itself." } + if (!isDir(resolved)) return { ok: false, message: `Not a directory: ${resolved}` } + + const links = loadLinks(cwd) + if (links.some((l) => l.path === resolved)) return { ok: false, message: "Already linked." } + if (links.length >= MAX_LINKED_REPOS) { + return { ok: false, message: `At most ${MAX_LINKED_REPOS} linked repos.` } + } + + links.push({ input: input.trim(), path: resolved }) + saveLinks(cwd, links) + return { ok: true, message: `Linked ${resolved}` } +} + +/** Remove a link by its resolved path. */ +export function removeLink(cwd: string, targetPath: string): void { + const links = loadLinks(cwd).filter((l) => l.path !== targetPath) + saveLinks(cwd, links) +} + +/** First AGENTS.md found inside a linked repo, or undefined. */ +function readLinkedAgents(repo: string): string | undefined { + for (const rel of AGENTS_LOCATIONS) { + try { + const text = fs.readFileSync(path.join(repo, rel), "utf8") + if (text.trim()) return text + } catch { + // try the next location + } + } + return undefined +} + +function truncate(text: string, max: number): string { + return text.length <= max ? text : text.slice(0, max) + "\n… (truncated)" +} + +/** + * Render the linked-repos block for the agent's environment details. Returns "" + * when nothing is linked. Each repo's AGENTS.md is pulled in (when present) so + * the model knows the linked codebase without exploring it first. + */ +export function renderLinkedReposSection(cwd: string): string { + const links = loadLinks(cwd).slice(0, MAX_LINKED_REPOS) + if (links.length === 0) return "" + + const parts: string[] = [ + "## Linked Repositories", + "", + "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.", + "", + ] + for (const link of links) { + const exists = isDir(link.path) + parts.push(`### ${path.basename(link.path)} — \`${link.path}\`${exists ? "" : " (path not found)"}`) + if (!exists) { + parts.push("") + continue + } + const agents = readLinkedAgents(link.path) + parts.push("") + if (agents) { + parts.push("Its AGENTS.md:") + parts.push("") + parts.push(truncate(agents.trim(), MAX_AGENTS_CHARS)) + } else { + parts.push("(no AGENTS.md found — explore the repo directly if you need its structure)") + } + parts.push("") + } + return parts.join("\n") +} diff --git a/src/core/agent.ts b/src/core/agent.ts index 07cc2c7..4953356 100644 --- a/src/core/agent.ts +++ b/src/core/agent.ts @@ -21,6 +21,7 @@ import { HookRunner, type HooksConfig } from "./hooks.js" import { McpManager } from "../mcp/manager.js" import { loadMemoryFiles } from "../memory/loader.js" import { loadSkills } from "../skills/loader.js" +import { renderLinkedReposSection } from "../config/links.js" const MAX_STEPS_PER_TURN = 50 const RESULT_PREVIEW_LINES = 6 @@ -272,6 +273,7 @@ export class Agent { const timeZoneOffsetHours = Math.floor(Math.abs(timeZoneOffset)) const timeZoneOffsetMinutes = Math.abs(Math.round((Math.abs(timeZoneOffset) - timeZoneOffsetHours) * 60)) const timeZoneOffsetStr = `${timeZoneOffset >= 0 ? "+" : "-"}${timeZoneOffsetHours}:${timeZoneOffsetMinutes.toString().padStart(2, "0")}` + const linkedRepos = renderLinkedReposSection(this.options.cwd) return `# Environment Details ## Current Workspace Directory (${this.options.cwd}) Files @@ -279,7 +281,7 @@ ${files.join("\n") || "(empty directory)"} ${files.length >= 200 ? "\n(File list truncated.)" : ""} ${git} - +${linkedRepos ? `\n${linkedRepos}` : ""} ## Current Time Current time in ISO 8601 UTC format: ${now.toISOString()} User time zone: ${timeZone}, UTC${timeZoneOffsetStr}` diff --git a/src/memory/loader.ts b/src/memory/loader.ts index 74e2c65..09d37ca 100644 --- a/src/memory/loader.ts +++ b/src/memory/loader.ts @@ -14,8 +14,8 @@ import type { MemoryFile } from "./types.js" * first), with closer-to-cwd and higher-precedence types winning: * * 1. User memory: ~/.orbcode/AGENTS.md - * 2. Project memory: AGENTS.md and .orbcode/AGENTS.md in cwd and every - * parent directory (closer-to-cwd wins) + * 2. Project memory: AGENTS.md and .orb/AGENTS.md (and the legacy + * .orbcode/AGENTS.md) in cwd and every parent directory (closer-to-cwd wins) * 3. Local memory: AGENTS.local.md in cwd and parents (highest precedence) * * Memory files support `@path` include directives (relative, `~/`, or @@ -130,9 +130,11 @@ export function loadMemoryFiles(cwd = process.cwd()): MemoryFile[] { // 1. User memory (~/.orbcode/AGENTS.md) files.push(...loadWithIncludes(path.join(getConfigDir(), "AGENTS.md"), "user", processed, 0)) - // 2. Project memory (AGENTS.md + .orbcode/AGENTS.md), root -> cwd + // 2. Project memory (AGENTS.md + .orb/AGENTS.md), root -> cwd. The legacy + // .orbcode/AGENTS.md location is still read for backward compatibility. for (const dir of ancestorDirs(cwd).reverse()) { files.push(...loadWithIncludes(path.join(dir, "AGENTS.md"), "project", processed, 0)) + files.push(...loadWithIncludes(path.join(dir, ".orb", "AGENTS.md"), "project", processed, 0)) files.push(...loadWithIncludes(path.join(dir, ".orbcode", "AGENTS.md"), "project", processed, 0)) } diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 9fde1d4..c494087 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -7,6 +7,7 @@ import React, { } from "react"; import { Box, Static, Text, useApp, useInput } from "ink"; import open from "open"; +import * as path from "node:path"; import { COLORS, VERSION } from "../branding.js"; import { @@ -53,6 +54,14 @@ import { ModelPicker } from "./components/ModelPicker.js"; import { SessionPicker } from "./components/SessionPicker.js"; import { listSessions, type SessionData } from "../core/sessions.js"; import { RowView, type Row } from "./components/rows.js"; +import { LinkManager } from "./components/LinkManager.js"; +import { + addLink, + loadLinks, + removeLink, + resolveProjectDir, + type LinkedRepo, +} from "../config/links.js"; const SLASH_COMMANDS: SlashCommand[] = [ { name: "/help", description: "show available commands" }, @@ -77,6 +86,10 @@ const SLASH_COMMANDS: SlashCommand[] = [ name: "/init", description: "analyze this codebase and create an AGENTS.md", }, + { + name: "/link", + description: "link other repos so changes here are checked against them", + }, { name: "/mcp", description: "manage MCP servers — enable, disable, reconnect, view status", @@ -149,13 +162,21 @@ function usageLines(profile: ProfileData): string[] { return lines; } -const INIT_PROMPT = `Analyze this codebase and create an AGENTS.md file containing: -1. A short overview of what the project does -2. Build, run, lint and test commands -3. Architecture and code structure (key directories and what lives in them) -4. Code style conventions used in this repo (imports, formatting, naming, error handling) +function buildInitPrompt(agentsPath: string): string { + 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: + + ${agentsPath} -If an AGENTS.md already exists, improve it. Keep it under ~60 lines so it is cheap to include in future prompts.`; +Investigate first — read the directory layout, key config files, and a few representative source files — then write. Keep it under ~60 lines so it is cheap to include in every future prompt. Cover, briefly: +1. What the project does (1-2 lines) and its main tech stack. +2. Project structure — the key directories/files and what each is responsible for. +3. Architecture — how the main pieces fit together (entry points, data/control flow). +4. Business-logic / domain mapping — where the core domain concepts live in the code. +5. Notable code patterns and conventions to follow (imports, naming, error handling, tests). +6. The common build, run, lint and test commands. + +Favor durable facts over volatile detail. If an AGENTS.md already exists at that path, refine it rather than rewriting from scratch.`; +} // Ported from the Orbital extension's commit slash command (commitCommandResponse). const buildCommitPrompt = ( @@ -275,6 +296,9 @@ export function App({ const [resumableSessions, setResumableSessions] = useState< SessionData[] | null >(null); + const [linkManagerOpen, setLinkManagerOpen] = useState(false); + const [links, setLinks] = useState([]); + const [linkStatus, setLinkStatus] = useState(""); // MCP manager (created once, shared across agents in this process). Null until // the first agent is created so we don't spawn servers before login. const mcpManagerRef = useRef(null); @@ -764,7 +788,7 @@ export function App({ } break; } - case "/init": + case "/init": { if (!getAuthToken(settings)) { setView("login"); break; @@ -772,7 +796,17 @@ export function App({ pushRow({ kind: "user", text: "/init" }); setBusy(true); setBusyLabel("Thinking"); - void getAgent().runTurn(INIT_PROMPT); + const agentsPath = path.join( + resolveProjectDir(process.cwd()), + "AGENTS.md", + ); + void getAgent().runTurn(buildInitPrompt(agentsPath)); + break; + } + case "/link": + setLinks(loadLinks(process.cwd())); + setLinkStatus(""); + setLinkManagerOpen(true); break; case "/mcp": { const manager = mcpManagerRef.current; @@ -1181,7 +1215,8 @@ export function App({ !modelPickerOpen && !mcpPickerOpen && !mcpMigrationEntries && - !resumableSessions; + !resumableSessions && + !linkManagerOpen; return ( @@ -1273,6 +1308,25 @@ export function App({ /> )} + {linkManagerOpen && ( + + { + const result = addLink(process.cwd(), input); + setLinkStatus(result.message); + if (result.ok) setLinks(loadLinks(process.cwd())); + }} + onRemove={(link) => { + removeLink(process.cwd(), link.path); + setLinks(loadLinks(process.cwd())); + setLinkStatus(`Unlinked ${path.basename(link.path)}`); + }} + onClose={() => setLinkManagerOpen(false)} + /> + + )} {pendingHookTrust && ( void + onRemove: (link: LinkedRepo) => void + onClose: () => void +} + +/** + * Interactive manager for the `/link` command. The last row is a free-text + * input where the user types a folder path to add; existing links above it can + * be selected and removed. Mirrors FollowupPrompt's "input is the final virtual + * row" pattern. + */ +export function LinkManager({ links, status, onAdd, onRemove, onClose }: LinkManagerProps) { + const [selected, setSelected] = useState(links.length) + const [draft, setDraft] = useState("") + + const inputRow = links.length // the free-text row sits after every link + const rowCount = links.length + 1 + const sel = Math.min(selected, inputRow) // clamp: list shrinks on removal + const isInput = sel === inputRow + + useInput((input, key) => { + if (key.escape) { + onClose() + return + } + if (key.upArrow) { + setSelected((s) => (Math.min(s, inputRow) - 1 + rowCount) % rowCount) + return + } + if (key.downArrow) { + setSelected((s) => (Math.min(s, inputRow) + 1) % rowCount) + return + } + if (key.return) { + if (isInput) { + if (draft.trim()) { + onAdd(draft.trim()) + setDraft("") + // Stay on the input row so several repos can be added in a row. + // rowCount == the new input-row index once a link is appended. + setSelected(rowCount) + } + } else { + onRemove(links[sel]) + setSelected(inputRow) // jump back to the input row after removing + } + return + } + if (isInput) { + if (key.backspace || key.delete) setDraft((d) => d.slice(0, -1)) + else if (input && !key.ctrl && !key.meta) setDraft((d) => d + input) + } else if (input === "d" || key.backspace || key.delete) { + onRemove(links[sel]) + setSelected(inputRow) + } + }) + + return ( + + + Linked repositories + + Repos linked here are shared with the agent so changes can be checked across them. + {links.length === 0 && (none yet)} + {links.map((link, index) => ( + // truncate-start keeps the meaningful tail of long paths visible + // instead of wrapping them onto the next line. + + {sel === index ? "❯ " : " "} + {index + 1}. {link.path} + + ))} + + {isInput ? "❯ " : " "} + add a repo (folder path): + + {isInput && ( + // The typed/pasted path lives on its own line and truncates from + // the start, so a long path shows its tail + cursor without wrapping. + + {" "} + {draft} + + + )} + {status && {status}} + ↑/↓ select · enter add/remove · d remove · esc done + + ) +} From dfe150ea7e7fabb5c4e9955795c8a0f66ee5d362 Mon Sep 17 00:00:00 2001 From: code-crusher Date: Tue, 30 Jun 2026 11:02:40 +0530 Subject: [PATCH 2/2] update agent file limit --- src/ui/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/App.tsx b/src/ui/App.tsx index c494087..3fbffd2 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -167,7 +167,7 @@ function buildInitPrompt(agentsPath: string): string { ${agentsPath} -Investigate first — read the directory layout, key config files, and a few representative source files — then write. Keep it under ~60 lines so it is cheap to include in every future prompt. Cover, briefly: +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: 1. What the project does (1-2 lines) and its main tech stack. 2. Project structure — the key directories/files and what each is responsible for. 3. Architecture — how the main pieces fit together (entry points, data/control flow).