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
7 changes: 4 additions & 3 deletions .dotagents/INSTRUCTIONS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Instructions for {{ var.agent_name }} from {{ env.app_name }}

## Project

Cyrus is a cross-platform app for controlling AI coding agents (Claude Code, Codex, and others) that run across a user's own devices — desktop, mobile, web, laptops, and servers.

It is single-user and personal: every device a user owns joins that user's one "room." Thread metadata is shared across all devices through a minimal server, while agent execution stays local to the device that owns it. The guiding principle is **shared metadata, owned execution** — letting the user start, observe, and continue the same agent conversation from any of their devices in real time.
## Coding Criterias

- This Project is still in development, so no backward compatibility is required.
- This project highly values "error as values" than exceptions. So always prefer "`better-result` library to get error as values.
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ describe("draft session lifecycle", () => {
"mock-agent",
"thread-1",
"project-1",
"hello"
[{ type: "text", text: "hello" }]
);
expect(prompt.isOk()).toBe(true);
if (prompt.isErr()) throw new Error("expected prompt to succeed");
Expand Down
6 changes: 4 additions & 2 deletions apps/cli/__tests__/integration/wiring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { Result } from "better-result";
import { runTurn } from "../../src/utils/run-turn";
import { createMockPromptStream } from "../helpers/acp-runtime";

const textMessage = (text: string) => [{ type: "text" as const, text }];

describe("acp mock runtime", () => {
test("streams token and completion events", () => {
const events: AgentEvent[] = [];
Expand All @@ -27,7 +29,7 @@ describe("acp mock runtime", () => {
agentName: "mock-agent",
threadId: "thread-1",
projectId: "project-1",
message: "ping",
message: textMessage("ping"),
emit: (event) => {
emitted.push({ type: event.type });
return Promise.resolve();
Expand Down Expand Up @@ -57,7 +59,7 @@ describe("acp mock runtime", () => {
agentName: "mock-agent",
threadId: "thread-1",
projectId: "project-1",
message: "ping",
message: textMessage("ping"),
emit: () => Promise.resolve(),
emitTerminal: (event) => {
terminal.push({ type: event.type });
Expand Down
1 change: 1 addition & 0 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@cyrus/errors": "workspace:*",
"@cyrus/schemas": "workspace:*",
"@cyrus/utils": "workspace:*",
"@ff-labs/fff-node": "^0.9.6",
"@orpc/server": "catalog:",
"@t3-oss/env-core": "catalog:",
"@tursodatabase/database": "^0.6.1",
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/commands/service/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { closeSync, openSync, readFileSync, unlinkSync } from "node:fs";
import { Result } from "better-result";
import { LOG_PATH, PID_PATH } from "@/constants/paths";
import { env } from "@/lib/env";
import { ensureDir } from "@/utils/dir";
import { ensureDir } from "@/utils/fs";
import { clearPid, isAlive, runningPid, writePid } from "@/utils/process";
import { print } from "@/utils/style";

Expand Down
30 changes: 30 additions & 0 deletions apps/cli/src/core/agents/prompt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, test } from "bun:test";
import { mapPromptBlocksToAcp } from "@/core/agents/prompt";

describe("prompt-blocks", () => {
test("maps text and resource blocks to ACP content", () => {
const blocks = [
{ type: "text" as const, text: "review this" },
{ type: "resource" as const, uri: "src/index.ts", name: "index.ts" },
{
type: "resource" as const,
uri: "https://example.com/docs",
name: "docs",
},
];

expect(mapPromptBlocksToAcp(blocks, "/tmp/project")).toEqual([
{ type: "text", text: "review this" },
{
type: "resource_link",
uri: "file:///tmp/project/src/index.ts",
name: "index.ts",
},
{
type: "resource_link",
uri: "https://example.com/docs",
name: "docs",
},
]);
});
});
40 changes: 40 additions & 0 deletions apps/cli/src/core/agents/prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import type { ContentBlock } from "@agentclientprotocol/sdk";
import type { PromptInputBlock } from "@cyrus/schemas/rtc/chat";

const HTTP_URI_PATTERN = /^https?:\/\//i;

export function mapPromptBlocksToAcp(
blocks: PromptInputBlock[],
threadCwd: string
): ContentBlock[] {
return blocks.map((block) => mapPromptBlockToAcp(block, threadCwd));
}

function mapPromptBlockToAcp(
block: PromptInputBlock,
threadCwd: string
): ContentBlock {
if (block.type === "text") {
return { type: "text", text: block.text };
}

const uri = resolveResourceUri(block.uri, threadCwd);
const name = block.name ?? uri.split("/").pop() ?? uri;

return {
type: "resource_link",
uri,
name,
};
}

function resolveResourceUri(uri: string, threadCwd: string): string {
if (HTTP_URI_PATTERN.test(uri) || uri.startsWith("file://")) {
return uri;
}

const absolute = resolve(threadCwd, uri);
return pathToFileURL(absolute).href;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
4 changes: 3 additions & 1 deletion apps/cli/src/core/agents/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ function createMockSession(sessionId: string): RuntimeSession {
},
modes: { availableModes: [{ id: "mode-1", name: "Mode 1" }] },
configOptions: [],
commands: [],
usage: {},
},
},
close: mock(async () => undefined),
Expand Down Expand Up @@ -90,7 +92,7 @@ describe("AgentRuntime", () => {
"project-1",
"/tmp/project",
"session-1",
"hello"
[{ type: "text", text: "hello" }]
)) {
events.push(event.type);
}
Expand Down
Loading