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
6 changes: 6 additions & 0 deletions .changeset/heavy-steaks-fall.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@plotday/tool-outlook-calendar": patch
"@plotday/tool-google-calendar": patch
---

Changed: Remove defunct static tool id
5 changes: 5 additions & 0 deletions .changeset/silent-otters-follow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@plotday/sdk": patch
---

Fixed: Improper use of tools in Agent and Tool base classes causing "Tool not found" errors
5 changes: 5 additions & 0 deletions .changeset/wise-baths-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@plotday/sdk": minor
---

Added: plot agent logs
126 changes: 126 additions & 0 deletions sdk/cli/commands/agent-logs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import * as fs from "fs";

import * as out from "../utils/output";
import { getGlobalTokenPath } from "../utils/token";
import { handleSSEStream } from "../utils/sse";

interface AgentLogsOptions {
agentId: string;
environment?: string;
deployToken?: string;
apiUrl: string;
}

/**
* Stream agent logs in real-time
*/
export async function agentLogsCommand(options: AgentLogsOptions) {
const { agentId, environment = "personal", apiUrl } = options;

// Load deploy token
let deployToken = options.deployToken;

if (!deployToken) {
// Try to load from PLOT_DEPLOY_TOKEN environment variable
deployToken = process.env.PLOT_DEPLOY_TOKEN;
}

if (!deployToken) {
// Try to load from global token file
const globalTokenPath = getGlobalTokenPath();
if (fs.existsSync(globalTokenPath)) {
try {
deployToken = fs.readFileSync(globalTokenPath, "utf-8").trim();
} catch (error) {
console.warn(
`Warning: Failed to read global token file: ${globalTokenPath}`
);
}
}
}

if (!deployToken) {
out.error(
"Authentication required",
"Run 'plot login' or provide token via --deploy-token or PLOT_DEPLOY_TOKEN env var"
);
process.exit(1);
}

// Construct API URL
const url = new URL(`/v1/agent/${agentId}/logs`, apiUrl);
if (environment !== "personal") {
url.searchParams.set("environment", environment);
}

out.info(`Streaming logs for agent ${agentId}`, [
`Environment: ${environment}`,
"Press Ctrl+C to stop",
]);
out.blank();

try {
const response = await fetch(url.toString(), {
method: "GET",
headers: {
Accept: "text/event-stream",
Authorization: `Bearer ${deployToken}`,
},
});

if (!response.ok) {
const errorText = await response.text();
out.error(
`Failed to connect: ${response.status} ${response.statusText}`,
errorText
);
process.exit(1);
}

// Handle SSE stream with custom log formatting
await handleSSEStream(response, {
onProgress: (message) => {
// Initial connection message
out.plain(message);
},
onEvent: (event, data) => {
if (event === "log") {
// Format log entry
const { timestamp, severity, message } = data;
const time = new Date(timestamp).toLocaleTimeString();

// Color-code by severity
let severityColor = "";
let severityLabel = severity.toUpperCase().padEnd(5);

switch (severity) {
case "error":
severityColor = "\x1b[31m"; // Red
break;
case "warn":
severityColor = "\x1b[33m"; // Yellow
break;
case "info":
severityColor = "\x1b[36m"; // Cyan
break;
default:
severityColor = "\x1b[37m"; // White
}

const reset = "\x1b[0m";
const gray = "\x1b[90m";

console.log(
`${gray}[${time}]${reset} ${severityColor}${severityLabel}${reset} ${message}`
);
}
},
onError: (error) => {
out.error("Stream error", error);
},
});
} catch (error) {
out.error("Connection failed", String(error));
process.exit(1);
}
}
36 changes: 21 additions & 15 deletions sdk/cli/commands/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import prompts from "prompts";

import * as out from "../utils/output";
import { detectPackageManager } from "../utils/packageManager";
import { getGlobalTokenPath } from "../utils/token";
import { handleSSEStream } from "../utils/sse";
import { getGlobalTokenPath } from "../utils/token";

interface GenerateOptions {
dir: string;
Expand Down Expand Up @@ -311,32 +311,38 @@ export async function generateCommand(options: GenerateOptions) {

// Update @plotday/sdk to latest and install packages
try {
out.progress("Updating @plotday/sdk to latest version...");
out.progress("Updating SDK to latest version...");

const updateCommand =
packageManager === "npm" ? "npm install @plotday/sdk@latest" :
packageManager === "pnpm" ? "pnpm add @plotday/sdk@latest" :
"yarn add @plotday/sdk@latest";
packageManager === "npm"
? "npm install @plotday/sdk@latest"
: packageManager === "pnpm"
? "pnpm add @plotday/sdk@latest"
: "yarn add @plotday/sdk@latest";

execSync(updateCommand, { cwd: agentPath, stdio: "ignore" });

out.progress("Installing dependencies...");

const installCommand =
packageManager === "yarn" ? "yarn" :
`${packageManager} install`;
packageManager === "yarn" ? "yarn" : `${packageManager} install`;

execSync(installCommand, { cwd: agentPath, stdio: "ignore" });

out.success("Dependencies installed successfully!");
out.success("Dependencies installed.");
} catch (error) {
out.warning(
"Couldn't install dependencies",
[
`Run '${packageManager === "npm" ? "npm install @plotday/sdk@latest" : packageManager === "pnpm" ? "pnpm add @plotday/sdk@latest" : "yarn add @plotday/sdk@latest"}' in ${options.dir}`,
`Then run '${packageManager === "yarn" ? "yarn" : `${packageManager} install`}'`
]
);
out.warning("Couldn't install dependencies", [
`Run '${
packageManager === "npm"
? "npm install @plotday/sdk@latest"
: packageManager === "pnpm"
? "pnpm add @plotday/sdk@latest"
: "yarn add @plotday/sdk@latest"
}' in ${options.dir}`,
`Then run '${
packageManager === "yarn" ? "yarn" : `${packageManager} install`
}'`,
]);
}

out.blank();
Expand Down
24 changes: 24 additions & 0 deletions sdk/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Command, Option } from "commander";
import { readFileSync } from "fs";
import { join } from "path";

import { agentLogsCommand } from "./commands/agent-logs";
import { buildCommand } from "./commands/build";
import { createCommand } from "./commands/create";
import { deployCommand } from "./commands/deploy";
Expand Down Expand Up @@ -116,6 +117,29 @@ agent
return deployCommand(opts);
});

agent
.command("logs <agent-id>")
.description("Stream real-time logs from an agent")
.option(
"-e, --environment <env>",
"Agent environment (personal, private, review)",
"personal"
)
.option("--deploy-token <token>", "Authentication token")
.action(function (this: Command, agentId: string) {
const opts = this.optsWithGlobals() as {
environment?: string;
deployToken?: string;
apiUrl: string;
};
return agentLogsCommand({
agentId,
environment: opts.environment,
deployToken: opts.deployToken,
apiUrl: opts.apiUrl,
});
});

// Priority subcommand group
const priority = program
.command("priority")
Expand Down
19 changes: 9 additions & 10 deletions sdk/cli/templates/AGENTS.template.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,16 +374,15 @@ try {

## Common Pitfalls

1. **Don't use instance variables for state** - Anything stored in memory is lost after function execution. Always use the Store tool for data that needs to persist.
2. **Don't forget timeout limits** - Each execution has ~10 seconds. Break long operations into batches with the Run tool.
3. **Don't assume execution order** - Batches may run on different workers. Store all necessary state between executions.
4. **Always use Callback tool for persistent references** - Direct function references don't survive worker restarts.
5. **Store auth tokens** - Don't re-request authentication unnecessarily.
6. **Clean up callbacks and stored state** - Delete callbacks and Store entries when no longer needed.
7. **Handle missing auth gracefully** - Check for stored auth before operations.
8. **Batch size matters** - Process enough items per batch to be efficient, but few enough to stay under time limits.
9. **Processing self-created activities** - Other users may change an Activity created by the agent, resulting in an \`activity\` call. Be sure to check the \`changes === null\` and/or \`activity.author.id !== this.id\` to avoid re-processing.
10. Activity with type ActivityType.Note typically do not have a start or end set, unless they're a note about a specific day or time that shouldn't be shown until then.
- **Don't use instance variables for state** - Anything stored in memory is lost after function execution. Always use the Store tool for data that needs to persist.
- **Processing self-created activities** - Other users may change an Activity created by the agent, resulting in an \`activity\` call. Be sure to check the \`changes === null\` and/or \`activity.author.id !== this.id\` to avoid re-processing.
- Activity with type ActivityType.Note typically do not have a start or end set, unless they're a note about a specific day or time that shouldn't be shown until then.
- Don't add the Tools instance as an instance variable. Any tools needed must bet rieved via \`this.tools.get(ToolClass)\` in the constructor and assigned to instance variables.
- **Don't forget runtime limits** - Each execution has ~10 seconds. Break long operations into batches with the Run tool. Process enough items per batch to be efficient, but few enough to stay under time limits.
- **Always use Callback tool for persistent references** - Direct function references don't survive worker restarts.
- **Store auth tokens** - Don't re-request authentication unnecessarily.
- **Clean up callbacks and stored state** - Delete callbacks and Store entries when no longer needed.
- **Handle missing auth gracefully** - Check for stored auth before operations.

## Type Patterns

Expand Down
6 changes: 6 additions & 0 deletions sdk/cli/utils/sse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface SSEHandlers {
onProgress?: (message: string) => void;
onResult?: (data: any) => void;
onError?: (error: string) => void;
onEvent?: (event: string, data: any) => void;
}

/**
Expand Down Expand Up @@ -73,6 +74,11 @@ export async function handleSSEStream(
case "error":
handlers.onError?.(parsedData.error);
throw new Error(parsedData.error);
default:
// Handle custom events via onEvent handler
if (handlers.onEvent) {
handlers.onEvent(currentEvent.event, parsedData);
}
}
}

Expand Down
Loading