mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 22:03:45 +00:00
Add session export to HTML, improve tool error handling, and enhance RPC mode documentation
This commit is contained in:
parent
68092ccf01
commit
9e3e319f1a
9 changed files with 638 additions and 63 deletions
|
|
@ -2,7 +2,7 @@ import { Agent, ProviderTransport, type ThinkingLevel } from "@mariozechner/pi-a
|
|||
import { getModel, type KnownProvider } from "@mariozechner/pi-ai";
|
||||
import { ProcessTerminal, TUI } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import { readFileSync } from "fs";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { SessionManager } from "./session-manager.js";
|
||||
|
|
@ -38,6 +38,8 @@ interface Args {
|
|||
resume?: boolean;
|
||||
help?: boolean;
|
||||
mode?: Mode;
|
||||
noSession?: boolean;
|
||||
session?: string;
|
||||
messages: string[];
|
||||
}
|
||||
|
||||
|
|
@ -68,6 +70,10 @@ function parseArgs(args: string[]): Args {
|
|||
result.apiKey = args[++i];
|
||||
} else if (arg === "--system-prompt" && i + 1 < args.length) {
|
||||
result.systemPrompt = args[++i];
|
||||
} else if (arg === "--no-session") {
|
||||
result.noSession = true;
|
||||
} else if (arg === "--session" && i + 1 < args.length) {
|
||||
result.session = args[++i];
|
||||
} else if (!arg.startsWith("-")) {
|
||||
result.messages.push(arg);
|
||||
}
|
||||
|
|
@ -90,6 +96,8 @@ ${chalk.bold("Options:")}
|
|||
--mode <mode> Output mode: text (default), json, or rpc
|
||||
--continue, -c Continue previous session
|
||||
--resume, -r Select a session to resume
|
||||
--session <path> Use specific session file
|
||||
--no-session Don't save session (ephemeral)
|
||||
--help, -h Show this help
|
||||
|
||||
${chalk.bold("Examples:")}
|
||||
|
|
@ -140,6 +148,23 @@ Guidelines:
|
|||
|
||||
Current directory: ${process.cwd()}`;
|
||||
|
||||
/**
|
||||
* Look for AGENT.md or CLAUDE.md in the current directory and return its contents
|
||||
*/
|
||||
function loadProjectContext(): string | null {
|
||||
const candidates = ["AGENT.md", "CLAUDE.md"];
|
||||
for (const filename of candidates) {
|
||||
if (existsSync(filename)) {
|
||||
try {
|
||||
return readFileSync(filename, "utf-8");
|
||||
} catch (error) {
|
||||
console.error(chalk.yellow(`Warning: Could not read ${filename}: ${error}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function selectSession(sessionManager: SessionManager): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
const ui = new TUI(new ProcessTerminal());
|
||||
|
|
@ -241,7 +266,7 @@ async function runRpcMode(agent: Agent, _sessionManager: SessionManager): Promis
|
|||
});
|
||||
|
||||
// Listen for JSON input on stdin
|
||||
const readline = require("readline");
|
||||
const readline = await import("readline");
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
|
|
@ -277,7 +302,12 @@ export async function main(args: string[]) {
|
|||
}
|
||||
|
||||
// Setup session manager
|
||||
const sessionManager = new SessionManager(parsed.continue && !parsed.resume);
|
||||
const sessionManager = new SessionManager(parsed.continue && !parsed.resume, parsed.session);
|
||||
|
||||
// Disable session saving if --no-session flag is set
|
||||
if (parsed.noSession) {
|
||||
sessionManager.disable();
|
||||
}
|
||||
|
||||
// Handle --resume flag: show session selector
|
||||
if (parsed.resume) {
|
||||
|
|
@ -398,6 +428,27 @@ export async function main(args: string[]) {
|
|||
// Start session
|
||||
sessionManager.startSession(agent.state);
|
||||
|
||||
// Inject project context (AGENT.md/CLAUDE.md) if not continuing/resuming
|
||||
if (!parsed.continue && !parsed.resume) {
|
||||
const projectContext = loadProjectContext();
|
||||
if (projectContext) {
|
||||
// Queue the context as a message that will be injected at the start
|
||||
await agent.queueMessage({
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `[Project Context from ${existsSync("AGENT.md") ? "AGENT.md" : "CLAUDE.md"}]\n\n${projectContext}`,
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
if (shouldPrintMessages) {
|
||||
console.log(chalk.dim(`Loaded project context from ${existsSync("AGENT.md") ? "AGENT.md" : "CLAUDE.md"}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to agent events to save messages and log events
|
||||
agent.subscribe((event) => {
|
||||
// Save messages on completion
|
||||
|
|
@ -412,15 +463,14 @@ export async function main(args: string[]) {
|
|||
});
|
||||
|
||||
// Route to appropriate mode
|
||||
if (isInteractive) {
|
||||
// No mode flag in interactive - always use TUI
|
||||
if (mode === "rpc") {
|
||||
// RPC mode - headless operation
|
||||
await runRpcMode(agent, sessionManager);
|
||||
} else if (isInteractive) {
|
||||
// No messages and not RPC - use TUI
|
||||
await runInteractiveMode(agent, sessionManager, VERSION);
|
||||
} else {
|
||||
// CLI mode with messages
|
||||
if (mode === "rpc") {
|
||||
await runRpcMode(agent, sessionManager);
|
||||
} else {
|
||||
await runSingleShotMode(agent, sessionManager, parsed.messages, mode);
|
||||
}
|
||||
await runSingleShotMode(agent, sessionManager, parsed.messages, mode);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ export interface SessionHeader {
|
|||
id: string;
|
||||
timestamp: string;
|
||||
cwd: string;
|
||||
systemPrompt: string;
|
||||
model: string;
|
||||
thinkingLevel: string;
|
||||
}
|
||||
|
|
@ -50,11 +49,16 @@ export class SessionManager {
|
|||
private sessionId!: string;
|
||||
private sessionFile!: string;
|
||||
private sessionDir: string;
|
||||
private enabled: boolean = true;
|
||||
|
||||
constructor(continueSession: boolean = false) {
|
||||
constructor(continueSession: boolean = false, customSessionPath?: string) {
|
||||
this.sessionDir = this.getSessionDirectory();
|
||||
|
||||
if (continueSession) {
|
||||
if (customSessionPath) {
|
||||
// Use custom session file path
|
||||
this.sessionFile = resolve(customSessionPath);
|
||||
this.loadSessionId();
|
||||
} else if (continueSession) {
|
||||
const mostRecent = this.findMostRecentlyModifiedSession();
|
||||
if (mostRecent) {
|
||||
this.sessionFile = mostRecent;
|
||||
|
|
@ -67,6 +71,11 @@ export class SessionManager {
|
|||
}
|
||||
}
|
||||
|
||||
/** Disable session saving (for --no-session mode) */
|
||||
disable() {
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
private getSessionDirectory(): string {
|
||||
const cwd = process.cwd();
|
||||
const safePath = "--" + cwd.replace(/^\//, "").replace(/\//g, "-") + "--";
|
||||
|
|
@ -121,12 +130,12 @@ export class SessionManager {
|
|||
}
|
||||
|
||||
startSession(state: AgentState): void {
|
||||
if (!this.enabled) return;
|
||||
const entry: SessionHeader = {
|
||||
type: "session",
|
||||
id: this.sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
cwd: process.cwd(),
|
||||
systemPrompt: state.systemPrompt,
|
||||
model: `${state.model.provider}/${state.model.id}`,
|
||||
thinkingLevel: state.thinkingLevel,
|
||||
};
|
||||
|
|
@ -134,6 +143,7 @@ export class SessionManager {
|
|||
}
|
||||
|
||||
saveMessage(message: any): void {
|
||||
if (!this.enabled) return;
|
||||
const entry: SessionMessageEntry = {
|
||||
type: "message",
|
||||
timestamp: new Date().toISOString(),
|
||||
|
|
@ -143,6 +153,7 @@ export class SessionManager {
|
|||
}
|
||||
|
||||
saveEvent(event: AgentEvent): void {
|
||||
if (!this.enabled) return;
|
||||
const entry: SessionEventEntry = {
|
||||
type: "event",
|
||||
timestamp: new Date().toISOString(),
|
||||
|
|
@ -152,6 +163,7 @@ export class SessionManager {
|
|||
}
|
||||
|
||||
saveThinkingLevelChange(thinkingLevel: string): void {
|
||||
if (!this.enabled) return;
|
||||
const entry: ThinkingLevelChangeEntry = {
|
||||
type: "thinking_level_change",
|
||||
timestamp: new Date().toISOString(),
|
||||
|
|
@ -161,6 +173,7 @@ export class SessionManager {
|
|||
}
|
||||
|
||||
saveModelChange(model: string): void {
|
||||
if (!this.enabled) return;
|
||||
const entry: ModelChangeEntry = {
|
||||
type: "model_change",
|
||||
timestamp: new Date().toISOString(),
|
||||
|
|
|
|||
|
|
@ -163,10 +163,7 @@ export const editTool: AgentTool<typeof editSchema> = {
|
|||
if (signal) {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
}
|
||||
resolve({
|
||||
content: [{ type: "text", text: `Error: File not found: ${path}` }],
|
||||
details: undefined,
|
||||
});
|
||||
reject(new Error(`File not found: ${path}`));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -188,15 +185,11 @@ export const editTool: AgentTool<typeof editSchema> = {
|
|||
if (signal) {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
}
|
||||
resolve({
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`,
|
||||
},
|
||||
],
|
||||
details: undefined,
|
||||
});
|
||||
reject(
|
||||
new Error(
|
||||
`Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -207,15 +200,11 @@ export const editTool: AgentTool<typeof editSchema> = {
|
|||
if (signal) {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
}
|
||||
resolve({
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,
|
||||
},
|
||||
],
|
||||
details: undefined,
|
||||
});
|
||||
reject(
|
||||
new Error(
|
||||
`Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,8 +27,6 @@ const IMAGE_MIME_TYPES: Record<string, string> = {
|
|||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".bmp": "image/bmp",
|
||||
".svg": "image/svg+xml",
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -52,7 +50,7 @@ export const readTool: AgentTool<typeof readSchema> = {
|
|||
name: "read",
|
||||
label: "read",
|
||||
description:
|
||||
"Read the contents of a file. Supports text files and images (jpg, png, gif, webp, bmp, svg). Images are sent as attachments. For text files, defaults to first 2000 lines. Use offset/limit for large files.",
|
||||
"Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, defaults to first 2000 lines. Use offset/limit for large files.",
|
||||
parameters: readSchema,
|
||||
execute: async (
|
||||
_toolCallId: string,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue