Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<repo>/.orbcode/settings.json` stay put); the
legacy `.orbcode/AGENTS.md` location is still read for backward compatibility.

## [0.3.2] - 2026-06-24

### Fixed
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
10 changes: 5 additions & 5 deletions src/branding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ export const COLORS = {
} as const;

export const LOGO = `
___ _ ____ _
/ _ \\ _ __ | |__ / ___| ___ __| | ___
| | | || '__|| '_ \\ | | / _ \\ / _\` | / _ \\
| |_| || | | |_) || |___ | (_) || (_| || __/
\\___/ |_| |_.__/ \\____| \\___/ \\__,_| \\___|
___ _ _
/ _ \\ _ __| |__ ___ ___ __| | ___
| | | | '__| '_ \\ / __/ _ \\ / _\` |/ _ \\
| |_| | | | |_) | (_| (_) | (_| | __/
\\___/|_| |_.__/ \\___\\___/ \\__,_|\\___|
`;
186 changes: 186 additions & 0 deletions src/config/links.ts
Original file line number Diff line number Diff line change
@@ -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 `<repo>/.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<Partial<LinkedRepo>>
}
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")
}
4 changes: 3 additions & 1 deletion src/core/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -272,14 +273,15 @@ 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
${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}`
Expand Down
8 changes: 5 additions & 3 deletions src/memory/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
}

Expand Down
Loading
Loading