Skip to content
Merged
59 changes: 59 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Changelog

All notable changes to **Codeep CLI** are documented here.
Expand All @@ -11,6 +11,65 @@
> as the social-share summary (IFTTT → X/Bluesky), capped at 220 chars.
> If omitted, the feed falls back to the first paragraph.

## [Unreleased]

> TL;DR — `codeep --yolo -- "<prompt>"` starts Codeep for a launcher with nothing left to answer, Omarchy's skills in `~/.agents/skills` load, and Codeep's colours follow the Omarchy theme.

### Added

- **`codeep -- <prompt>`** starts a new session with `<prompt>` as its first
message, as if it had been typed (also `-p <prompt>` and `--prompt
<prompt>`). Everything after `--` is the prompt, so `codeep -- review` asks
the model and does not run `codeep review`. A missing or doubled prompt is
an error rather than a guess.
- **`codeep --yolo`** starts without stopping to ask, for this launch only:
agent actions run without confirmation, a vague task gets no clarifying
questions, the folder is used as the project with read & write access, and
a new session starts. Nothing is saved, so the next plain `codeep` asks as
before; changing Agent Confirmation or Agent Interactive Mode in
`/settings` ends `--yolo`'s hold on that setting. Writes to files that
decide what runs later are still confirmed, and a workspace's own MCP
servers still ask to be trusted. The status bar shows **YOLO** while
confirmations are off.
- **Skills in `~/.agents/skills` load**, the directory agent tools share and
where Omarchy links its skills. When two skills have the same name, the
project's `.codeep/skills` wins, then `~/.codeep/skills`, then
`~/.agents/skills`. `/skills bundles` names the directory each came from.
- **A skill can send the agent to the files beside it.** `invoke_skill` now
gives the skill's directory and reads a file from it (a guide such as
`reporting.md`), which `read_file` cannot reach for a skill outside the
project. Only files inside that directory are read.
- **Codeep follows the Omarchy theme.** On Omarchy, the colours Codeep
paints itself — the brand red, the greys of the chat, the code colours,
the warning orange and the **YOLO** badge — come from the current theme's
`colors.toml` and change when the theme does, in a session that is already
open, the way Claude Code, OpenCode and Pi do. A theme colour too faint to
read as text is darkened or lightened until it reads, so light themes stay
legible. **Follow Omarchy theme** in `/settings` (On by default, shown
only on Omarchy) turns it off. Codeep only reads Omarchy's state; it
writes nothing to your Omarchy config. Elsewhere every colour is exactly
what it was.

### Fixed

- **Startup asks its questions one at a time.** In a folder that is not a
repository but has its own MCP servers, the question whether to trust them
could replace "Set as Project?", leaving that one unanswered and the
startup stopped. It now comes after the startup questions, and a launch
prompt is sent after it.
- **Folded and literal descriptions.** A `SKILL.md` whose `description:` was
written as a `>` or `|` block, as Omarchy's are, loaded with ">" as its
description.
- **The agent sees a skill's whole description.** The catalog cut every entry
at 200 characters, mid-word, which dropped the part of a long description
that says when to use the skill.

### Security

- **Writing to `.agents/skills/` is confirmed in every mode**, as
`.codeep/skills/` already was: every Codeep session loads the skills in
`~/.agents/skills`.

## [3.8.1] — 2026-09-30

> TL;DR — While a thinking model works between steps, the agent screen now says it is waiting for the model and for how long, not "Listing" a tool that already finished. Auto-verify's checks show as checks.
Expand Down
10 changes: 5 additions & 5 deletions Codeep.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Codeep

<p align="center">
Expand Down Expand Up @@ -680,10 +680,16 @@
**structured skill bundles** — directory-based capability packs the agent
discovers and invokes on its own.

A bundle is a directory `.codeep/skills/<name>/SKILL.md` (project-scoped)
or `~/.codeep/skills/<name>/SKILL.md` (global). The file is Markdown with
a YAML frontmatter header — the format is a **superset of Claude Code
skills**, so existing skills drop in unchanged:
A bundle is a directory with a `SKILL.md` in one of three places. When two
have the same name, the first one here wins:

1. `.codeep/skills/<name>/SKILL.md` — project-scoped
2. `~/.codeep/skills/<name>/SKILL.md` — global, Codeep's own
3. `~/.agents/skills/<name>/SKILL.md` — global, shared with other agent
tools (Omarchy links its skills in here)

The file is Markdown with a YAML frontmatter header — the format is a
**superset of Claude Code skills**, so existing skills drop in unchanged:

```markdown
---
Expand All @@ -703,6 +709,11 @@
fails, surface the error and stop — don't deploy a broken build.
```

A long `description` can also be a folded (`>`) or literal (`|`) block.
Files a skill refers to, such as a `reporting.md` guide, sit next to its
`SKILL.md`: `invoke_skill` tells the agent the skill's directory and reads
them from there.

The agent gets the bundle catalog injected into its system prompt on
every iteration and can invoke any bundle via the `invoke_skill` tool:

Expand Down Expand Up @@ -1212,6 +1223,8 @@
| `codeep account sync` | Pull API keys + personalities + commands from codeep.dev → local |
| `codeep account push` | Push local API keys + personalities + commands → codeep.dev |
| `codeep review [files…]` | Offline, deterministic code review for CI — markdown or `--json`, `--fail-on <error\|warning\|info\|none>` sets the exit code. No API key needed. |
| `codeep -- <prompt>` | Start a new session and send `<prompt>` as its first message (also `-p <prompt>` / `--prompt <prompt>`). Everything after `--` is the prompt, so `codeep -- review` asks the model rather than running `codeep review`. Startup's own questions come first: the project and access questions without `--yolo`, and with or without it, trusting a workspace's own MCP servers the first time. |
| `codeep --yolo` | Start without stopping to ask, for this launch only — for launchers that open Codeep unattended (`codeep --yolo -- "<prompt>"`). Agent actions run without confirmation (Agent Confirmation: Never) and without clarifying questions first (Agent Interactive Mode: Off), the folder is used as the project with read & write access, and a new session starts. Nothing is saved: the next plain `codeep` asks as before, and changing either setting in `/settings` ends `--yolo`'s hold on it. Writes to files that decide what runs later (`.git/config`, git hooks, MCP server lists) are still confirmed, and a workspace's own MCP servers (`.mcp.json`) still ask to be trusted the first time, before the prompt is sent. The status bar shows **YOLO** while confirmations are off. |

### Authentication

Expand Down Expand Up @@ -1343,6 +1356,7 @@
| Agent Branch | Off | Create new branch for agent commits |
| Agent Auto-Verify | Off | `Off`, `Build only`, `Typecheck only`, `Test only`, or `Build + Typecheck + Test` |
| Agent Max Fix Attempts | 1 | Max attempts to auto-fix errors when Auto-Verify is enabled. More than 1 usually means the agent is stuck — bail and let the user decide. |
| Follow Omarchy theme | On | On [Omarchy](https://omarchy.org) only (the row is not shown elsewhere): paint Codeep's own colours — the brand red, the greys, the code colours — from the current Omarchy theme, and switch with it while Codeep runs. `Off` keeps Codeep's colours. Codeep only reads `~/.local/state/omarchy/current/theme/colors.toml`, and watches it for the next switch. |

## Usage Examples

Expand Down Expand Up @@ -1467,7 +1481,7 @@
export DEEPSEEK_API_KEY=your_key # or whichever provider
```

4. Open Zed's AI panel and select **Codeep** as the agent.

Check warning on line 1484 in README.md

View workflow job for this annotation

GitHub Actions / review

codeep: performance

SELECT * can be inefficient, select only needed columns — Specify required columns explicitly

### Configurable permissions (Zed & VS Code extension)

Expand Down
7 changes: 7 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@
agentMaxDuration: number; // Max agent duration in minutes (default: 20)
agentApiTimeout: number; // Base API timeout for agent in ms (default: 90000, dynamically adjusted)
agentInteractive: boolean; // Show interactive mode (default: true)
/** On an Omarchy desktop, paint the TUI's truecolour roles (brand colour,
* greys, code colours) from the current Omarchy theme and follow it when
* it changes — renderer/omarchyTheme.ts. Default true: Omarchy users
* expect every app to switch with the desktop. Off keeps Codeep's own
* colours. Does nothing anywhere else. */
followOmarchyTheme: boolean;
projectPermissions: ProjectPermission[];
/** @deprecated Legacy PLAINTEXT key store. Kept only so the one-time
* migration into secure storage can read it; emptied afterwards. New keys
Expand Down Expand Up @@ -213,7 +219,7 @@
/**
* Get sessions directory - local .codeep/sessions/ if in project, otherwise global
*/
export function getSessionsDir(projectPath?: string): string {

Check notice on line 222 in src/config/index.ts

View workflow job for this annotation

GitHub Actions / review

codeep: documentation

Exported function/class without JSDoc — Add JSDoc documentation for public APIs
if (projectPath && isProjectDirectory(projectPath)) {
const localDir = join(projectPath, '.codeep', 'sessions');
// .codeep/ can come with a cloned repo; a symlinked sessions directory
Expand All @@ -233,7 +239,7 @@
* Looks for common project indicators: package.json, pyproject.toml, Cargo.toml, go.mod, composer.json, etc.
* Also checks if user has manually initialized this folder as a project (.codeep/project.json)
*/
export function isProjectDirectory(path: string): boolean {

Check notice on line 242 in src/config/index.ts

View workflow job for this annotation

GitHub Actions / review

codeep: documentation

Exported function/class without JSDoc — Add JSDoc documentation for public APIs
// Check if user has manually initialized this as a project
const manualProjectMarker = join(path, '.codeep', 'project.json');
if (existsSync(manualProjectMarker)) {
Expand All @@ -259,7 +265,7 @@
/**
* Check if directory has standard project markers (not manually initialized)
*/
export function hasStandardProjectMarkers(path: string): boolean {

Check notice on line 268 in src/config/index.ts

View workflow job for this annotation

GitHub Actions / review

codeep: documentation

Exported function/class without JSDoc — Add JSDoc documentation for public APIs
const projectFiles = [
'package.json',
'pyproject.toml',
Expand All @@ -280,7 +286,7 @@
* Initialize a folder as a Codeep project
* Creates .codeep/project.json marker file
*/
export function initializeAsProject(path: string): boolean {

Check notice on line 289 in src/config/index.ts

View workflow job for this annotation

GitHub Actions / review

codeep: documentation

Exported function/class without JSDoc — Add JSDoc documentation for public APIs
try {
const projectFile = join(path, '.codeep', 'project.json');
const projectData = {
Expand All @@ -299,7 +305,7 @@
/**
* Check if folder was manually initialized as project
*/
export function isManuallyInitializedProject(path: string): boolean {

Check notice on line 308 in src/config/index.ts

View workflow job for this annotation

GitHub Actions / review

codeep: documentation

Exported function/class without JSDoc — Add JSDoc documentation for public APIs
return existsSync(join(path, '.codeep', 'project.json'));
}

Expand Down Expand Up @@ -344,7 +350,7 @@
* `glm-5.3`, so new users started a flagship behind what the website promised,
* and nothing failed because 5.2 still works.
*/
export const DEFAULT_PROVIDER = 'z.ai';

Check notice on line 353 in src/config/index.ts

View workflow job for this annotation

GitHub Actions / review

codeep: documentation

Exported function/class without JSDoc — Add JSDoc documentation for public APIs
export const DEFAULT_MODEL = 'glm-5.3';

function createConfig(): Conf<ConfigSchema> {
Expand Down Expand Up @@ -381,6 +387,7 @@
agentMaxDuration: 480,
agentApiTimeout: 600000,
agentInteractive: true,
followOmarchyTheme: true,
protocol: 'openai',
plan: 'lite',
language: 'en',
Expand Down Expand Up @@ -955,7 +962,7 @@
.map(m => {
const inPrice = m.pricing?.prompt ? Number(m.pricing.prompt) * 1_000_000 : null;
const outPrice = m.pricing?.completion ? Number(m.pricing.completion) * 1_000_000 : null;
const priceStr = inPrice !== null && outPrice !== null

Check notice on line 965 in src/config/index.ts

View workflow job for this annotation

GitHub Actions / review

codeep: bug

Using == for null check also matches undefined — Use === null or == null intentionally

Check notice on line 965 in src/config/index.ts

View workflow job for this annotation

GitHub Actions / review

codeep: bug

Using == for null check also matches undefined — Use === null or == null intentionally
? ` · $${inPrice.toFixed(2)} in / $${outPrice.toFixed(2)} out per 1M`
: '';
const ctxStr = m.context_length ? ` · ${Math.round(m.context_length / 1000)}K ctx` : '';
Expand Down
129 changes: 129 additions & 0 deletions src/renderer/App.palette.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { describe, expect, it, vi, afterEach } from 'vitest';
import { App } from './App';
import { resetPalette, setPalette } from './palette';

/**
* What a running App does when the palette changes under it — an Omarchy
* theme switch, or "Follow Omarchy theme" switched in /settings. The colours
* themselves are read at render time everywhere (palette.test.ts); the App's
* part is the transcript, which it formats once and caches with the ANSI
* baked in.
*/

const TEAL = '\x1b[38;2;0;128;128m';
const RED = '\x1b[38;2;240;42;48m';

function makeApp(options: { yolo?: boolean } = {}) {
const app = new App({
onSubmit: async () => {},
onCommand: () => {},
onExit: () => {},
getStatus: () => ({
version: '3.8.1', provider: 'OpenAI', model: 'gpt-5.6-terra', agentMode: 'on',
projectPath: '/tmp/project', hasWriteAccess: true, sessionId: '', messageCount: 0,
}),
yolo: () => options.yolo ?? false,
});
const internals = app as unknown as {
screen: Record<string, (...args: unknown[]) => unknown>;
input: Record<string, (...args: unknown[]) => unknown>;
getVisibleMessages: (height: number, width: number) => Array<{ text: string }>;
};
// Detach the Screen's resize listener so these Apps don't pile up on stdout.
process.stdout.removeListener('resize', internals.screen.resizeHandler as () => void);
// addMessage() and a repaint schedule a real render; keep its frame off
// the test runner's stdout.
for (const method of ['init', 'cleanup', 'render', 'fullRender']) {
vi.spyOn(internals.screen, method).mockImplementation(() => {});
}
const transcript = () => internals.getVisibleMessages(40, 80).map(l => l.text).join('\n');
return { app, internals, transcript };
}

afterEach(() => {
resetPalette();
vi.restoreAllMocks();
});

describe('App and the palette', () => {
it('re-formats the cached transcript in the new colours', async () => {
const { app, transcript } = makeApp();
app.addMessage({ role: 'user', content: 'fix the **build**' });
expect(transcript()).toContain(RED);

setPalette({ primary: [0, 128, 128] });
// The cache still holds the message as it was first formatted…
expect(transcript()).toContain(RED);
app.repaintInNewPalette();
// …until the repaint drops it.
expect(transcript()).toContain(TEAL);
expect(transcript()).not.toContain(RED);
// Let the renders this queued run now, against the stubbed Screen.
await new Promise(resolve => setImmediate(resolve));
});

it('repaints on every palette change while started, and stops listening when stopped', async () => {
const { app, internals } = makeApp();
for (const method of ['start', 'stop', 'onKey']) vi.spyOn(internals.input, method).mockImplementation(() => {});
const repaint = vi.spyOn(app, 'repaintInNewPalette');

setPalette({ primary: [1, 1, 1] });
expect(repaint).not.toHaveBeenCalled();

app.start();
setPalette({ primary: [0, 128, 128] });
expect(repaint).toHaveBeenCalledTimes(1);

// Let the render start() and the repaint scheduled run while the Screen
// is still stubbed, rather than onto the test runner's stdout.
await new Promise(resolve => setImmediate(resolve));
app.stop();
resetPalette();
expect(repaint).toHaveBeenCalledTimes(1);
});

it('paints the status bar in the current brand colour', () => {
const { app, internals } = makeApp();
const styles: string[] = [];
vi.spyOn(internals.screen, 'write').mockImplementation((...args: unknown[]) => {
styles.push(String(args[2]) + String(args[3] ?? ''));
});
setPalette({ primary: [0, 128, 128] });
(app as unknown as { renderStatusBar: (y: number, w: number, s: boolean) => void }).renderStatusBar(0, 80, true);
expect(styles.join('')).toContain(TEAL + 'gpt-5.6-terra');
});

it('paints the YOLO badge and a warning toast in the current palette, and in 208 orange without a theme', () => {
const statusBar = () => {
const { app, internals } = makeApp({ yolo: true });
const writes: string[] = [];
vi.spyOn(internals.screen, 'write').mockImplementation((...args: unknown[]) => {
writes.push(String(args[3] ?? '') + String(args[2]));
});
Object.assign(app, { notification: 'Rate limited, retrying in 4s', notificationIsWarn: true });
(app as unknown as { renderStatusBar: (y: number, w: number, s: boolean) => void }).renderStatusBar(0, 80, true);
return writes.join('');
};
expect(statusBar()).toContain('\x1b[48;5;208m\x1b[30m\x1b[1m YOLO ');
expect(statusBar()).toContain('\x1b[38;5;208m Rate limited');

setPalette({ yoloBadge: [223, 142, 29], yoloBadgeText: [0, 0, 0], warningToast: [135, 104, 74] });
expect(statusBar()).toContain('\x1b[48;2;223;142;29m\x1b[38;2;0;0;0m\x1b[1m YOLO ');
expect(statusBar()).toContain('\x1b[38;2;135;104;74m Rate limited');
});

it('paints the prompt that points at an open picker in the attention colour', () => {
const { app, internals } = makeApp();
const writes: string[] = [];
vi.spyOn(internals.screen, 'write').mockImplementation((...args: unknown[]) => {
writes.push(String(args[3] ?? '') + String(args[2]));
});
const renderInput = () => (app as unknown as { renderInput: (y: number, w: number) => void }).renderInput(0, 80);
Object.assign(app, { menuOpen: true, menuItemsAll: [] });
renderInput();
expect(writes.join('')).toContain('\x1b[33mSelect an option below…');
setPalette({ attention: [135, 104, 74] });
renderInput();
expect(writes.join('')).toContain('\x1b[38;2;135;104;74mSelect an option below…');
});
});
Loading
Loading