Files
elizaos--eliza/plugins/plugin-linear/src/actions/getActivity.ts
T
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

334 lines
10 KiB
TypeScript

/**
* Handles the get_activity Linear op. Extracts time-range, action, resource, and
* success filters from the message via the getActivity prompt, reads the matching
* slice of LinearService's in-memory activity log, and formats it into the reply.
*/
import {
type Action,
type ActionResult,
type HandlerCallback,
type HandlerOptions,
type IAgentRuntime,
logger,
type Memory,
ModelType,
type State,
} from "@elizaos/core";
import { getActivityTemplate } from "../prompts.js";
import type { LinearService } from "../services/linear";
import { getLinearAccountId, linearAccountIdParameter } from "./account-options";
import { formatUnknownError, getMessageSource } from "./message-source";
import {
getNumberValue,
getRecordValue,
getStringArrayValue,
getStringValue,
parseLinearPromptResponse,
} from "./parseLinearPrompt.js";
import { validateLinearActionIntent } from "./validate-linear-intent";
function formatActivityDetail(value: unknown): string {
if (value === null || value === undefined) {
return "none";
}
if (Array.isArray(value)) {
return value.map(formatActivityDetail).join(", ");
}
if (typeof value === "object") {
return Object.entries(value as Record<string, unknown>)
.map(([key, nestedValue]) => `${key}=${formatActivityDetail(nestedValue)}`)
.join("; ");
}
return String(value);
}
type ActivityParams = {
filters?: Record<string, unknown>;
limit?: number;
};
export const getActivityAction: Action = {
name: "GET_LINEAR_ACTIVITY",
contexts: ["tasks", "connectors", "automation"],
contextGate: { anyOf: ["tasks", "connectors", "automation"] },
roleGate: { minRole: "USER" },
description: "Get recent Linear activity log with filters.",
descriptionCompressed: "get recent Linear activity log w/ optional filter",
similes: [
"get-linear-activity",
"show-linear-activity",
"view-linear-activity",
"check-linear-activity",
],
parameters: [
{
name: "filters",
description:
"Activity filters: fromDate ISO timestamp, action, resource_type, resource_id, success.",
required: false,
schema: { type: "object" as const },
},
{
name: "limit",
description: "Max activity entries.",
required: false,
schema: { type: "number" as const },
},
linearAccountIdParameter,
],
examples: [
[
{
name: "User",
content: {
text: "Show me recent Linear activity",
},
},
{
name: "Assistant",
content: {
text: "I'll show you the recent Linear activity.",
actions: ["GET_LINEAR_ACTIVITY"],
},
},
],
[
{
name: "User",
content: {
text: "What happened in Linear today?",
},
},
{
name: "Assistant",
content: {
text: "Let me check today's Linear activity for you.",
actions: ["GET_LINEAR_ACTIVITY"],
},
},
],
[
{
name: "User",
content: {
text: "Show me what issues John created this week",
},
},
{
name: "Assistant",
content: {
text: "I'll find the issues John created this week.",
actions: ["GET_LINEAR_ACTIVITY"],
},
},
],
],
validate: async (runtime: IAgentRuntime, message: Memory, state?: State): Promise<boolean> =>
validateLinearActionIntent(runtime, message, state),
async handler(
runtime: IAgentRuntime,
message: Memory,
_state?: State,
_options?: HandlerOptions,
callback?: HandlerCallback
): Promise<ActionResult> {
try {
const linearService = runtime.getService<LinearService>("linear");
if (!linearService) {
throw new Error("Linear service not available");
}
const accountId = getLinearAccountId(runtime, _options);
const content = message.content.text || "";
const params = (_options?.parameters ?? {}) as ActivityParams;
const filters: Record<string, unknown> = { ...(params.filters ?? {}) };
let limit =
typeof params.limit === "number" && Number.isFinite(params.limit)
? Math.max(1, Math.floor(params.limit))
: 10;
if (content) {
const prompt = getActivityTemplate.replace("{{userMessage}}", content);
const response = await runtime.useModel(ModelType.TEXT_LARGE, {
prompt: prompt,
});
if (response) {
try {
const parsed = parseLinearPromptResponse(response);
if (Object.keys(parsed).length === 0) {
throw new Error("No fields found in model response");
}
const timeRange = getRecordValue(parsed.timeRange);
if (timeRange) {
const now = new Date();
let fromDate: Date | undefined;
const from = getStringValue(timeRange.from);
const period = getStringValue(timeRange.period);
if (from) {
fromDate = new Date(from);
} else if (period) {
switch (period) {
case "today":
fromDate = new Date(now.setHours(0, 0, 0, 0));
break;
case "yesterday":
fromDate = new Date(now.setDate(now.getDate() - 1));
fromDate.setHours(0, 0, 0, 0);
break;
case "this-week":
fromDate = new Date(now.setDate(now.getDate() - now.getDay()));
fromDate.setHours(0, 0, 0, 0);
break;
case "last-week":
fromDate = new Date(now.setDate(now.getDate() - now.getDay() - 7));
fromDate.setHours(0, 0, 0, 0);
break;
case "this-month":
fromDate = new Date(now.getFullYear(), now.getMonth(), 1);
break;
}
}
if (fromDate) {
filters.fromDate = fromDate.toISOString();
}
}
const actionTypes = getStringArrayValue(parsed.actionTypes);
if (actionTypes && actionTypes.length > 0) {
filters.action = actionTypes[0];
}
const resourceTypes = getStringArrayValue(parsed.resourceTypes);
if (resourceTypes && resourceTypes.length > 0) {
filters.resource_type = resourceTypes[0];
}
const resourceId = getStringValue(parsed.resourceId);
if (resourceId) {
filters.resource_id = resourceId;
}
const successFilter = getStringValue(parsed.successFilter);
if (successFilter && successFilter !== "all") {
filters.success = successFilter === "success";
}
limit = getNumberValue(parsed.limit) || 10;
} catch (parseError) {
logger.warn("Failed to parse activity filters:", formatUnknownError(parseError));
}
}
}
let activity = linearService.getActivityLog(limit * 2, filters, accountId);
if (filters.fromDate) {
const fromDateValue = filters.fromDate;
const fromDate =
typeof fromDateValue === "string"
? fromDateValue
: fromDateValue instanceof Date
? fromDateValue.toISOString()
: String(fromDateValue);
const fromTime = new Date(fromDate).getTime();
if (!Number.isNaN(fromTime)) {
activity = activity.filter((item) => new Date(item.timestamp).getTime() >= fromTime);
}
}
activity = activity.slice(0, limit);
if (activity.length === 0) {
const noActivityMessage = filters.fromDate
? `No Linear activity found for the specified filters.`
: "No recent Linear activity found.";
await callback?.({
text: noActivityMessage,
source: getMessageSource(message),
});
return {
text: noActivityMessage,
success: true,
data: {
activity: [],
accountId,
},
};
}
const activityText = activity
.map((item, index) => {
const time = new Date(item.timestamp).toLocaleString();
const status = item.success ? "✅" : "❌";
const details = Object.entries(item.details)
.filter(([key]) => key !== "filters")
.map(([key, value]) => `${key}: ${formatActivityDetail(value)}`)
.join(", ");
return `${index + 1}. ${status} ${item.action} on ${item.resource_type} ${item.resource_id}\n Time: ${time}\n ${details ? `Details: ${details}` : ""}${item.error ? `\n Error: ${item.error}` : ""}`;
})
.join("\n\n");
const headerText = filters.fromDate
? `📊 Linear activity ${content}:`
: "📊 Recent Linear activity:";
const resultMessage = `${headerText}\n\n${activityText}`;
await callback?.({
text: resultMessage,
source: getMessageSource(message),
});
return {
text: `Found ${activity.length} activity item${activity.length === 1 ? "" : "s"}`,
success: true,
data: {
activity: activity.map((item) => ({
id: item.id,
action: item.action,
resource_type: item.resource_type,
resource_id: item.resource_id,
success: item.success,
error: item.error,
details: formatActivityDetail(item.details),
timestamp:
typeof item.timestamp === "string"
? item.timestamp
: new Date(item.timestamp).toISOString(),
})) as Array<Record<string, string | boolean | undefined>>,
filters: filters
? {
...filters,
fromDate: filters.fromDate
? typeof filters.fromDate === "string"
? filters.fromDate
: String(filters.fromDate)
: undefined,
}
: undefined,
count: activity.length,
accountId,
},
};
} catch (error) {
logger.error("Failed to get activity:", formatUnknownError(error));
const errorMessage = `❌ Failed to get activity: ${error instanceof Error ? error.message : "Unknown error"}`;
await callback?.({
text: errorMessage,
source: getMessageSource(message),
});
return {
text: errorMessage,
success: false,
};
}
},
};