Skip to content

Commit 8030c59

Browse files
committed
Add plot agent logs
1 parent 97b3195 commit 8030c59

4 files changed

Lines changed: 161 additions & 0 deletions

File tree

.changeset/wise-baths-smell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@plotday/sdk": minor
3+
---
4+
5+
Added: plot agent logs

sdk/cli/commands/agent-logs.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import * as fs from "fs";
2+
3+
import * as out from "../utils/output";
4+
import { getGlobalTokenPath } from "../utils/token";
5+
import { handleSSEStream } from "../utils/sse";
6+
7+
interface AgentLogsOptions {
8+
agentId: string;
9+
environment?: string;
10+
deployToken?: string;
11+
apiUrl: string;
12+
}
13+
14+
/**
15+
* Stream agent logs in real-time
16+
*/
17+
export async function agentLogsCommand(options: AgentLogsOptions) {
18+
const { agentId, environment = "personal", apiUrl } = options;
19+
20+
// Load deploy token
21+
let deployToken = options.deployToken;
22+
23+
if (!deployToken) {
24+
// Try to load from PLOT_DEPLOY_TOKEN environment variable
25+
deployToken = process.env.PLOT_DEPLOY_TOKEN;
26+
}
27+
28+
if (!deployToken) {
29+
// Try to load from global token file
30+
const globalTokenPath = getGlobalTokenPath();
31+
if (fs.existsSync(globalTokenPath)) {
32+
try {
33+
deployToken = fs.readFileSync(globalTokenPath, "utf-8").trim();
34+
} catch (error) {
35+
console.warn(
36+
`Warning: Failed to read global token file: ${globalTokenPath}`
37+
);
38+
}
39+
}
40+
}
41+
42+
if (!deployToken) {
43+
out.error(
44+
"Authentication required",
45+
"Run 'plot login' or provide token via --deploy-token or PLOT_DEPLOY_TOKEN env var"
46+
);
47+
process.exit(1);
48+
}
49+
50+
// Construct API URL
51+
const url = new URL(`/v1/agent/${agentId}/logs`, apiUrl);
52+
if (environment !== "personal") {
53+
url.searchParams.set("environment", environment);
54+
}
55+
56+
out.info(`Streaming logs for agent ${agentId}`, [
57+
`Environment: ${environment}`,
58+
"Press Ctrl+C to stop",
59+
]);
60+
out.blank();
61+
62+
try {
63+
const response = await fetch(url.toString(), {
64+
method: "GET",
65+
headers: {
66+
Accept: "text/event-stream",
67+
Authorization: `Bearer ${deployToken}`,
68+
},
69+
});
70+
71+
if (!response.ok) {
72+
const errorText = await response.text();
73+
out.error(
74+
`Failed to connect: ${response.status} ${response.statusText}`,
75+
errorText
76+
);
77+
process.exit(1);
78+
}
79+
80+
// Handle SSE stream with custom log formatting
81+
await handleSSEStream(response, {
82+
onProgress: (message) => {
83+
// Initial connection message
84+
out.plain(message);
85+
},
86+
onEvent: (event, data) => {
87+
if (event === "log") {
88+
// Format log entry
89+
const { timestamp, severity, message } = data;
90+
const time = new Date(timestamp).toLocaleTimeString();
91+
92+
// Color-code by severity
93+
let severityColor = "";
94+
let severityLabel = severity.toUpperCase().padEnd(5);
95+
96+
switch (severity) {
97+
case "error":
98+
severityColor = "\x1b[31m"; // Red
99+
break;
100+
case "warn":
101+
severityColor = "\x1b[33m"; // Yellow
102+
break;
103+
case "info":
104+
severityColor = "\x1b[36m"; // Cyan
105+
break;
106+
default:
107+
severityColor = "\x1b[37m"; // White
108+
}
109+
110+
const reset = "\x1b[0m";
111+
const gray = "\x1b[90m";
112+
113+
console.log(
114+
`${gray}[${time}]${reset} ${severityColor}${severityLabel}${reset} ${message}`
115+
);
116+
}
117+
},
118+
onError: (error) => {
119+
out.error("Stream error", error);
120+
},
121+
});
122+
} catch (error) {
123+
out.error("Connection failed", String(error));
124+
process.exit(1);
125+
}
126+
}

sdk/cli/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Command, Option } from "commander";
33
import { readFileSync } from "fs";
44
import { join } from "path";
55

6+
import { agentLogsCommand } from "./commands/agent-logs";
67
import { buildCommand } from "./commands/build";
78
import { createCommand } from "./commands/create";
89
import { deployCommand } from "./commands/deploy";
@@ -116,6 +117,29 @@ agent
116117
return deployCommand(opts);
117118
});
118119

120+
agent
121+
.command("logs <agent-id>")
122+
.description("Stream real-time logs from an agent")
123+
.option(
124+
"-e, --environment <env>",
125+
"Agent environment (personal, private, review)",
126+
"personal"
127+
)
128+
.option("--deploy-token <token>", "Authentication token")
129+
.action(function (this: Command, agentId: string) {
130+
const opts = this.optsWithGlobals() as {
131+
environment?: string;
132+
deployToken?: string;
133+
apiUrl: string;
134+
};
135+
return agentLogsCommand({
136+
agentId,
137+
environment: opts.environment,
138+
deployToken: opts.deployToken,
139+
apiUrl: opts.apiUrl,
140+
});
141+
});
142+
119143
// Priority subcommand group
120144
const priority = program
121145
.command("priority")

sdk/cli/utils/sse.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface SSEHandlers {
1212
onProgress?: (message: string) => void;
1313
onResult?: (data: any) => void;
1414
onError?: (error: string) => void;
15+
onEvent?: (event: string, data: any) => void;
1516
}
1617

1718
/**
@@ -73,6 +74,11 @@ export async function handleSSEStream(
7374
case "error":
7475
handlers.onError?.(parsedData.error);
7576
throw new Error(parsedData.error);
77+
default:
78+
// Handle custom events via onEvent handler
79+
if (handlers.onEvent) {
80+
handlers.onEvent(currentEvent.event, parsedData);
81+
}
7682
}
7783
}
7884

0 commit comments

Comments
 (0)