diff --git a/openpeon/files.macos/config.json b/openpeon/files.macos/config.json index d607421..de0a431 100644 --- a/openpeon/files.macos/config.json +++ b/openpeon/files.macos/config.json @@ -10,4 +10,4 @@ "input.required": true, "resource.limit": true } -} +} \ No newline at end of file diff --git a/pi/files.macos/agent/skills/attio-frontend-rules/SKILL.md b/pi/files.macos/agent/skills/attio-frontend-rules/SKILL.md index 55b129b..509e548 100644 --- a/pi/files.macos/agent/skills/attio-frontend-rules/SKILL.md +++ b/pi/files.macos/agent/skills/attio-frontend-rules/SKILL.md @@ -1,3 +1,8 @@ +--- +name: attio-frontend-rules +description: Styling conventions and component guidelines for the Attio frontend codebase. Covers styled-components patterns, transient props, data attributes, spacing, color tokens, and design system usage. Use when modifying frontend UI code in the Attio monorepo. +--- + # Attio Frontend Rules Guidelines and conventions for working on the Attio frontend codebase. Use whenever modifying the frontend. @@ -52,6 +57,26 @@ export function Stack({..., className}: {..., className: string | undefined}) { If the same re-styling is applied multiple times, it should become its own reusable component (or component variant). +### Layout.Stack defaults + +`Layout.Stack` defaults `align` to `"center"` (i.e. `align-items: center`). **Always explicitly set `align="flex-start"`** when you need left/top alignment — don't assume it will be the default. + +```tsx +// Good — explicit alignment + + Title + Description + + +// Bad — text will be centered, not left-aligned + + Title + Description + +``` + +Other useful `Layout.Stack` props: `direction`, `justify`, `gap`, `flex`, `shrink`, `minWidth`, `width`, `height`, and all spacing props (`p`, `px`, `py`, `pt`, `pb`, `pl`, `pr`, `m`, `mx`, `my`, etc.). **Always prefer these props over writing custom styled divs with `display: flex`.** + ### Avoid layout assumptions Components should not generally include external layout styles such as `width`, `z-index`, `margin` or `flex`. These properties should instead be set by the parent component using a `styled(MyComponent)` override. diff --git a/pi/files/agent/skills/linear/SKILL.md b/pi/files/agent/skills/linear/SKILL.md new file mode 100644 index 0000000..620601d --- /dev/null +++ b/pi/files/agent/skills/linear/SKILL.md @@ -0,0 +1,105 @@ +--- +name: linear +description: Access Linear issue tracker - search, view, create, update issues, list teams/projects, and manage comments. Use when the user asks about Linear issues, tasks, tickets, or project management in Linear. +--- + +# Linear + +Manage Linear issues, projects, and teams via the Linear SDK. + +## Setup + +Run once before first use: + +```bash +cd {baseDir} && npm install +``` + +Requires a `LINEAR_API_KEY` environment variable. Generate one at: https://linear.app/settings/api (Personal API keys). + +Set it in your shell profile or pi settings: + +```bash +export LINEAR_API_KEY=lin_api_... +``` + +## Current User + +```bash +node {baseDir}/linear-me.js # Show authenticated user +node {baseDir}/linear-me.js --issues # Show user + their active issues +``` + +## Search Issues + +```bash +node {baseDir}/linear-search.js "query" # Text search +node {baseDir}/linear-search.js "query" -n 20 # More results +node {baseDir}/linear-search.js "query" --team ENG # Filter by team +node {baseDir}/linear-search.js "query" --state "In Progress" # Filter by state +``` + +## List Issues (with filters) + +```bash +node {baseDir}/linear-issues.js # All recent issues +node {baseDir}/linear-issues.js --team ENG # By team +node {baseDir}/linear-issues.js --state "In Progress" # By state +node {baseDir}/linear-issues.js --assignee me # My issues +node {baseDir}/linear-issues.js --assignee "John" # By assignee name +node {baseDir}/linear-issues.js --label "Bug" # By label +node {baseDir}/linear-issues.js --project "Q1 Goals" # By project +node {baseDir}/linear-issues.js --team ENG --state Todo -n 50 # Combined filters +``` + +## View Issue Details + +```bash +node {baseDir}/linear-issue.js ATT-1234 # Full issue details +node {baseDir}/linear-issue.js ATT-1234 --comments # Include comments +``` + +## Create Issue + +```bash +node {baseDir}/linear-create.js --team ENG --title "Fix login bug" +node {baseDir}/linear-create.js --team ENG --title "New feature" --description "Details here" --state Todo --priority 2 --assignee me --label "Feature" +node {baseDir}/linear-create.js --team ENG --title "Sub-task" --parent ATT-100 +``` + +Priority values: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low + +## Update Issue + +```bash +node {baseDir}/linear-update.js ATT-1234 --state "In Progress" +node {baseDir}/linear-update.js ATT-1234 --assignee me --priority 2 +node {baseDir}/linear-update.js ATT-1234 --title "New title" --description "Updated desc" +``` + +## Add Comment + +```bash +node {baseDir}/linear-comment.js ATT-1234 "This is done in PR #567" +``` + +## List Teams + +```bash +node {baseDir}/linear-teams.js +``` + +## List Projects + +```bash +node {baseDir}/linear-projects.js # All projects +node {baseDir}/linear-projects.js --team ENG # By team +``` + +## Tips + +- Use `--assignee me` to filter by the authenticated user +- Issue identifiers follow the pattern `TEAM-NUMBER` (e.g. `ATT-1234`, `ENG-567`) +- Descriptions support markdown formatting +- State names are case-insensitive (e.g. "todo", "Todo", "TODO" all work) +- When creating issues, the team key is required; use `linear-teams.js` to find available teams diff --git a/pi/files/agent/skills/linear/lib.js b/pi/files/agent/skills/linear/lib.js new file mode 100755 index 0000000..46cb062 --- /dev/null +++ b/pi/files/agent/skills/linear/lib.js @@ -0,0 +1,23 @@ +import { LinearClient } from "@linear/sdk"; + +export function getClient() { + const apiKey = process.env.LINEAR_API_KEY; + if (!apiKey) { + console.error("Error: LINEAR_API_KEY environment variable is required."); + console.error( + "Generate one at: https://linear.app/settings/api (Personal API keys)" + ); + process.exit(1); + } + return new LinearClient({ apiKey }); +} + +export function formatDate(date) { + if (!date) return ""; + return new Date(date).toISOString().split("T")[0]; +} + +export function truncate(str, len = 120) { + if (!str) return ""; + return str.length > len ? str.slice(0, len) + "…" : str; +} diff --git a/pi/files/agent/skills/linear/linear-comment.js b/pi/files/agent/skills/linear/linear-comment.js new file mode 100755 index 0000000..d677f33 --- /dev/null +++ b/pi/files/agent/skills/linear/linear-comment.js @@ -0,0 +1,29 @@ +#!/usr/bin/env node + +// Add a comment to a Linear issue +// Usage: linear-comment.js + +import { getClient } from "./lib.js"; + +const args = process.argv.slice(2); +const identifier = args[0]; +const body = args.slice(1).join(" "); + +if (!identifier || !body) { + console.log("Usage: linear-comment.js "); + console.log("\nExamples:"); + console.log(' linear-comment.js ATT-1234 "This is fixed in the latest PR"'); + process.exit(1); +} + +const client = getClient(); + +const results = await client.searchIssues(identifier, { first: 1 }); +const issue = results.nodes[0]; +if (!issue) { + console.error(`Issue '${identifier}' not found.`); + process.exit(1); +} + +await client.createComment({ issueId: issue.id, body }); +console.log(`Comment added to ${issue.identifier}.`); diff --git a/pi/files/agent/skills/linear/linear-create.js b/pi/files/agent/skills/linear/linear-create.js new file mode 100755 index 0000000..92e9d7b --- /dev/null +++ b/pi/files/agent/skills/linear/linear-create.js @@ -0,0 +1,102 @@ +#!/usr/bin/env node + +// Create a new Linear issue +// Usage: linear-create.js --team --title [--description <desc>] [--state <name>] [--priority <0-4>] [--assignee <name|me>] [--label <name>] [--parent <identifier>] + +import { getClient } from "./lib.js"; + +const args = process.argv.slice(2); + +function extractArg(flag) { + const idx = args.indexOf(flag); + if (idx !== -1 && args[idx + 1]) { + const val = args[idx + 1]; + args.splice(idx, 2); + return val; + } + return null; +} + +const teamKey = extractArg("--team"); +const title = extractArg("--title"); +const description = extractArg("--description"); +const stateName = extractArg("--state"); +const priority = extractArg("--priority"); +const assigneeName = extractArg("--assignee"); +const labelName = extractArg("--label"); +const parentId = extractArg("--parent"); + +if (!teamKey || !title) { + console.log("Usage: linear-create.js --team <key> --title <title> [options]"); + console.log("\nRequired:"); + console.log(" --team <key> Team key (e.g. ENG)"); + console.log(' --title <title> Issue title'); + console.log("\nOptional:"); + console.log(" --description <text> Issue description (markdown)"); + console.log(" --state <name> Initial state (e.g. 'Todo')"); + console.log(" --priority <0-4> Priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low"); + console.log(" --assignee <name|me> Assignee name or 'me'"); + console.log(" --label <name> Label name"); + console.log(" --parent <id> Parent issue identifier (e.g. ATT-100)"); + process.exit(1); +} + +const client = getClient(); + +// Resolve team +const teams = await client.teams({ filter: { key: { eq: teamKey.toUpperCase() } } }); +const team = teams.nodes[0]; +if (!team) { + console.error(`Team '${teamKey}' not found.`); + process.exit(1); +} + +const input = { + teamId: team.id, + title, +}; + +if (description) input.description = description; +if (priority) input.priority = parseInt(priority, 10); + +// Resolve state +if (stateName) { + const states = await team.states(); + const state = states.nodes.find( + (s) => s.name.toLowerCase() === stateName.toLowerCase() + ); + if (state) input.stateId = state.id; + else console.warn(`Warning: State '${stateName}' not found, using default.`); +} + +// Resolve assignee +if (assigneeName) { + if (assigneeName.toLowerCase() === "me") { + const me = await client.viewer; + input.assigneeId = me.id; + } else { + const users = await client.users({ filter: { name: { containsIgnoreCase: assigneeName } } }); + if (users.nodes[0]) input.assigneeId = users.nodes[0].id; + else console.warn(`Warning: User '${assigneeName}' not found.`); + } +} + +// Resolve label +if (labelName) { + const labels = await client.issueLabels({ filter: { name: { eqIgnoreCase: labelName } } }); + if (labels.nodes[0]) input.labelIds = [labels.nodes[0].id]; + else console.warn(`Warning: Label '${labelName}' not found.`); +} + +// Resolve parent +if (parentId) { + const parentSearch = await client.searchIssues(parentId, { first: 1 }); + if (parentSearch.nodes[0]) input.parentId = parentSearch.nodes[0].id; + else console.warn(`Warning: Parent '${parentId}' not found.`); +} + +const result = await client.createIssue(input); +const issue = await result.issue; + +console.log(`Created: ${issue.identifier} - ${issue.title}`); +console.log(`URL: ${issue.url}`); diff --git a/pi/files/agent/skills/linear/linear-issue.js b/pi/files/agent/skills/linear/linear-issue.js new file mode 100755 index 0000000..f9fa5d7 --- /dev/null +++ b/pi/files/agent/skills/linear/linear-issue.js @@ -0,0 +1,87 @@ +#!/usr/bin/env node + +// Get details for a specific Linear issue +// Usage: linear-issue.js <identifier> [--comments] + +import { getClient, formatDate } from "./lib.js"; + +const args = process.argv.slice(2); + +const showComments = args.includes("--comments"); +const filtered = args.filter((a) => a !== "--comments"); + +const identifier = filtered[0]; + +if (!identifier) { + console.log("Usage: linear-issue.js <identifier> [--comments]"); + console.log("\nExamples:"); + console.log(" linear-issue.js ATT-1234"); + console.log(" linear-issue.js ATT-1234 --comments"); + process.exit(1); +} + +const client = getClient(); + +// Parse team key and issue number from identifier (e.g. "SIP-1205") +const parts = identifier.match(/^([A-Za-z]+)-(\d+)$/); +if (!parts) { + console.error(`Invalid identifier format: ${identifier}. Expected format: TEAM-123`); + process.exit(1); +} + +const teamKey = parts[1].toUpperCase(); +const issueNumber = parseInt(parts[2], 10); + +// Find the issue by team key + number +const issues = await client.issues({ + filter: { + team: { key: { eq: teamKey } }, + number: { eq: issueNumber }, + }, + first: 1, +}); + +const issue = issues.nodes[0]; +if (!issue) { + console.error(`Issue ${identifier} not found.`); + process.exit(1); +} + +const state = await issue.state; +const team = await issue.team; +const assignee = await issue.assignee; +const labels = await issue.labels(); +const parent = await issue.parent; +const project = await issue.project; +const cycle = await issue.cycle; + +console.log(`=== ${issue.identifier}: ${issue.title} ===`); +console.log(`URL: ${issue.url}`); +console.log(`State: ${state?.name || "Unknown"}`); +console.log(`Priority: ${issue.priorityLabel}`); +console.log(`Team: ${team?.key || "?"}`); +console.log(`Assignee: ${assignee?.name || "Unassigned"}`); +if (project) console.log(`Project: ${project.name}`); +if (cycle) console.log(`Cycle: ${cycle.name || cycle.number}`); +if (parent) console.log(`Parent: ${parent.identifier} - ${parent.title}`); +if (labels.nodes.length > 0) { + console.log(`Labels: ${labels.nodes.map((l) => l.name).join(", ")}`); +} +console.log(`Created: ${formatDate(issue.createdAt)}`); +console.log(`Updated: ${formatDate(issue.updatedAt)}`); +if (issue.dueDate) console.log(`Due: ${issue.dueDate}`); +console.log(`\nDescription:\n${issue.description || "(empty)"}`); + +if (showComments) { + const comments = await issue.comments(); + if (comments.nodes.length > 0) { + console.log(`\n--- Comments (${comments.nodes.length}) ---`); + for (const comment of comments.nodes) { + const author = await comment.user; + console.log(`\n[${formatDate(comment.createdAt)}] ${author?.name || "Unknown"}:`); + console.log(comment.body); + } + } else { + console.log("\nNo comments."); + } +} diff --git a/pi/files/agent/skills/linear/linear-issues.js b/pi/files/agent/skills/linear/linear-issues.js new file mode 100755 index 0000000..06a3761 --- /dev/null +++ b/pi/files/agent/skills/linear/linear-issues.js @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +// List Linear issues with filters +// Usage: linear-issues.js [--team <key>] [--state <name>] [--assignee <name|me>] [--label <name>] [--project <name>] [-n <num>] + +import { getClient, formatDate, truncate } from "./lib.js"; + +const args = process.argv.slice(2); + +function extractArg(flag) { + const idx = args.indexOf(flag); + if (idx !== -1 && args[idx + 1]) { + const val = args[idx + 1]; + args.splice(idx, 2); + return val; + } + return null; +} + +const numResults = parseInt(extractArg("-n") || "25", 10); +const teamKey = extractArg("--team"); +const stateName = extractArg("--state"); +const assigneeName = extractArg("--assignee"); +const labelName = extractArg("--label"); +const projectName = extractArg("--project"); + +if (args.includes("--help") || args.includes("-h")) { + console.log("Usage: linear-issues.js [options]"); + console.log("\nOptions:"); + console.log(" --team <key> Filter by team key (e.g. ENG)"); + console.log(" --state <name> Filter by state (e.g. 'In Progress', 'Todo')"); + console.log(" --assignee <name> Filter by assignee name or 'me'"); + console.log(" --label <name> Filter by label name"); + console.log(" --project <name> Filter by project name"); + console.log(" -n <num> Number of results (default: 25)"); + process.exit(0); +} + +const client = getClient(); + +// Build filter +const filter = {}; + +if (teamKey) { + filter.team = { key: { eq: teamKey.toUpperCase() } }; +} + +if (stateName) { + filter.state = { name: { eqIgnoreCase: stateName } }; +} + +if (assigneeName) { + if (assigneeName.toLowerCase() === "me") { + const me = await client.viewer; + filter.assignee = { id: { eq: me.id } }; + } else { + filter.assignee = { name: { containsIgnoreCase: assigneeName } }; + } +} + +if (labelName) { + filter.labels = { name: { eqIgnoreCase: labelName } }; +} + +if (projectName) { + filter.project = { name: { containsIgnoreCase: projectName } }; +} + +const issues = await client.issues({ + filter, + first: numResults, + orderBy: "updatedAt", +}); + +if (issues.nodes.length === 0) { + console.log("No issues found matching filters."); + process.exit(0); +} + +for (const issue of issues.nodes) { + const state = await issue.state; + const team = await issue.team; + const assignee = await issue.assignee; + + console.log( + `${issue.identifier.padEnd(12)} ${(state?.name || "?").padEnd(14)} ${(issue.priorityLabel || "").padEnd(8)} ${(assignee?.name || "Unassigned").padEnd(20)} ${truncate(issue.title, 80)}` + ); +} + +console.log(`\n${issues.nodes.length} issue(s) shown.`); diff --git a/pi/files/agent/skills/linear/linear-me.js b/pi/files/agent/skills/linear/linear-me.js new file mode 100755 index 0000000..cbe2e20 --- /dev/null +++ b/pi/files/agent/skills/linear/linear-me.js @@ -0,0 +1,33 @@ +#!/usr/bin/env node + +// Show current authenticated user and their assigned issues +// Usage: linear-me.js [--issues] + +import { getClient, truncate } from "./lib.js"; + +const showIssues = process.argv.includes("--issues"); + +const client = getClient(); + +const me = await client.viewer; +console.log(`User: ${me.name}`); +console.log(`Email: ${me.email}`); +console.log(`ID: ${me.id}`); + +if (showIssues) { + const issues = await me.assignedIssues({ + first: 25, + filter: { + state: { type: { nin: ["completed", "canceled"] } }, + }, + orderBy: "updatedAt", + }); + + console.log(`\n--- Active Assigned Issues (${issues.nodes.length}) ---`); + for (const issue of issues.nodes) { + const state = await issue.state; + console.log( + `${issue.identifier.padEnd(12)} ${(state?.name || "?").padEnd(14)} ${(issue.priorityLabel || "").padEnd(8)} ${truncate(issue.title, 80)}` + ); + } +} diff --git a/pi/files/agent/skills/linear/linear-projects.js b/pi/files/agent/skills/linear/linear-projects.js new file mode 100755 index 0000000..99813e8 --- /dev/null +++ b/pi/files/agent/skills/linear/linear-projects.js @@ -0,0 +1,45 @@ +#!/usr/bin/env node + +// List Linear projects +// Usage: linear-projects.js [--team <key>] [-n <num>] + +import { getClient, formatDate } from "./lib.js"; + +const args = process.argv.slice(2); + +function extractArg(flag) { + const idx = args.indexOf(flag); + if (idx !== -1 && args[idx + 1]) { + const val = args[idx + 1]; + args.splice(idx, 2); + return val; + } + return null; +} + +const numResults = parseInt(extractArg("-n") || "25", 10); +const teamKey = extractArg("--team"); + +const client = getClient(); + +const filter = {}; +if (teamKey) { + filter.accessibleTeams = { key: { eq: teamKey.toUpperCase() } }; +} + +const projects = await client.projects({ filter, first: numResults }); + +if (projects.nodes.length === 0) { + console.log("No projects found."); + process.exit(0); +} + +for (const project of projects.nodes) { + const lead = await project.lead; + console.log(`--- ${project.name} ---`); + console.log(`State: ${project.state} | Progress: ${Math.round(project.progress * 100)}%`); + if (lead) console.log(`Lead: ${lead.name}`); + if (project.targetDate) console.log(`Target: ${project.targetDate}`); + console.log(`URL: ${project.url}`); + console.log(""); +} diff --git a/pi/files/agent/skills/linear/linear-search.js b/pi/files/agent/skills/linear/linear-search.js new file mode 100755 index 0000000..2fa5aa6 --- /dev/null +++ b/pi/files/agent/skills/linear/linear-search.js @@ -0,0 +1,67 @@ +#!/usr/bin/env node + +// Search Linear issues by text query +// Usage: linear-search.js <query> [-n <num>] [--team <key>] [--state <name>] + +import { getClient, formatDate, truncate } from "./lib.js"; + +const args = process.argv.slice(2); + +let numResults = 10; +const nIdx = args.indexOf("-n"); +if (nIdx !== -1 && args[nIdx + 1]) { + numResults = parseInt(args[nIdx + 1], 10); + args.splice(nIdx, 2); +} + +let teamFilter = null; +const teamIdx = args.indexOf("--team"); +if (teamIdx !== -1 && args[teamIdx + 1]) { + teamFilter = args[teamIdx + 1]; + args.splice(teamIdx, 2); +} + +let stateFilter = null; +const stateIdx = args.indexOf("--state"); +if (stateIdx !== -1 && args[stateIdx + 1]) { + stateFilter = args[stateIdx + 1]; + args.splice(stateIdx, 2); +} + +const query = args.join(" "); + +if (!query) { + console.log("Usage: linear-search.js <query> [-n <num>] [--team <key>] [--state <name>]"); + console.log("\nOptions:"); + console.log(" -n <num> Number of results (default: 10)"); + console.log(" --team <key> Filter by team key (e.g. ENG)"); + console.log(" --state <name> Filter by state name (e.g. 'In Progress')"); + process.exit(1); +} + +const client = getClient(); + +const results = await client.searchIssues(query, { first: numResults }); + +for (const issue of results.nodes) { + const state = await issue.state; + const team = await issue.team; + const assignee = await issue.assignee; + + if (teamFilter && team?.key?.toLowerCase() !== teamFilter.toLowerCase()) continue; + if (stateFilter && state?.name?.toLowerCase() !== stateFilter.toLowerCase()) continue; + + console.log(`--- ${issue.identifier} ---`); + console.log(`Title: ${issue.title}`); + console.log(`State: ${state?.name || "Unknown"}`); + console.log(`Priority: ${issue.priorityLabel}`); + console.log(`Team: ${team?.key || "?"} | Assignee: ${assignee?.name || "Unassigned"}`); + console.log(`Created: ${formatDate(issue.createdAt)} | Updated: ${formatDate(issue.updatedAt)}`); + if (issue.description) console.log(`Description: ${truncate(issue.description, 200)}`); + console.log(`URL: ${issue.url}`); + console.log(""); +} + +if (results.nodes.length === 0) { + console.log("No results found."); +} diff --git a/pi/files/agent/skills/linear/linear-teams.js b/pi/files/agent/skills/linear/linear-teams.js new file mode 100755 index 0000000..e6e888a --- /dev/null +++ b/pi/files/agent/skills/linear/linear-teams.js @@ -0,0 +1,15 @@ +#!/usr/bin/env node + +// List all Linear teams +// Usage: linear-teams.js + +import { getClient } from "./lib.js"; + +const client = getClient(); + +const teams = await client.teams(); + +console.log("Teams:"); +for (const team of teams.nodes) { + console.log(` ${team.key.padEnd(8)} ${team.name}`); +} diff --git a/pi/files/agent/skills/linear/linear-update.js b/pi/files/agent/skills/linear/linear-update.js new file mode 100755 index 0000000..883e926 --- /dev/null +++ b/pi/files/agent/skills/linear/linear-update.js @@ -0,0 +1,93 @@ +#!/usr/bin/env node + +// Update an existing Linear issue +// Usage: linear-update.js <identifier> [--title <title>] [--state <name>] [--priority <0-4>] [--assignee <name|me>] [--description <text>] + +import { getClient } from "./lib.js"; + +const args = process.argv.slice(2); + +const identifier = args[0]; +if (!identifier || identifier.startsWith("--")) { + console.log("Usage: linear-update.js <identifier> [options]"); + console.log("\nOptions:"); + console.log(" --title <title> New title"); + console.log(" --state <name> New state (e.g. 'In Progress')"); + console.log(" --priority <0-4> New priority"); + console.log(" --assignee <name|me> New assignee"); + console.log(" --description <text> New description"); + process.exit(1); +} + +args.shift(); + +function extractArg(flag) { + const idx = args.indexOf(flag); + if (idx !== -1 && args[idx + 1]) { + const val = args[idx + 1]; + args.splice(idx, 2); + return val; + } + return null; +} + +const title = extractArg("--title"); +const stateName = extractArg("--state"); +const priority = extractArg("--priority"); +const assigneeName = extractArg("--assignee"); +const description = extractArg("--description"); + +const client = getClient(); + +// Find the issue +const results = await client.searchIssues(identifier, { first: 1 }); +const issue = results.nodes[0]; +if (!issue) { + console.error(`Issue '${identifier}' not found.`); + process.exit(1); +} + +const input = {}; + +if (title) input.title = title; +if (description) input.description = description; +if (priority) input.priority = parseInt(priority, 10); + +// Resolve state +if (stateName) { + const team = await issue.team; + const states = await team.states(); + const state = states.nodes.find( + (s) => s.name.toLowerCase() === stateName.toLowerCase() + ); + if (state) input.stateId = state.id; + else { + console.error(`State '${stateName}' not found. Available states:`); + for (const s of states.nodes) console.error(` - ${s.name}`); + process.exit(1); + } +} + +// Resolve assignee +if (assigneeName) { + if (assigneeName.toLowerCase() === "me") { + const me = await client.viewer; + input.assigneeId = me.id; + } else { + const users = await client.users({ filter: { name: { containsIgnoreCase: assigneeName } } }); + if (users.nodes[0]) input.assigneeId = users.nodes[0].id; + else { + console.error(`User '${assigneeName}' not found.`); + process.exit(1); + } + } +} + +if (Object.keys(input).length === 0) { + console.log("No updates specified. Use --title, --state, --priority, --assignee, or --description."); + process.exit(1); +} + +await client.updateIssue(issue.id, input); +console.log(`Updated ${issue.identifier}: ${issue.title}`); +console.log(`URL: ${issue.url}`); diff --git a/pi/files/agent/skills/linear/package-lock.json b/pi/files/agent/skills/linear/package-lock.json new file mode 100644 index 0000000..ed7445a --- /dev/null +++ b/pi/files/agent/skills/linear/package-lock.json @@ -0,0 +1,107 @@ +{ + "name": "linear-skill", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "linear-skill", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@linear/sdk": "^37.0.0" + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@linear/sdk": { + "version": "37.0.0", + "resolved": "https://registry.npmjs.org/@linear/sdk/-/sdk-37.0.0.tgz", + "integrity": "sha512-EAZCXtV414Nwtvrwn7Ucu3E8BbYYKsc3HqZCGf1mHUE7FhZGtfISu295DOVv89WhhXlp2N344EMg3K0nnhLxtA==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.0", + "graphql": "^15.4.0", + "isomorphic-unfetch": "^3.1.0" + }, + "engines": { + "node": ">=12.x", + "yarn": "1.x" + } + }, + "node_modules/graphql": { + "version": "15.10.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.10.1.tgz", + "integrity": "sha512-BL/Xd/T9baO6NFzoMpiMD7YUZ62R6viR5tp/MULVEnbYJXZA//kRNW7J0j1w/wXArgL0sCxhDfK5dczSKn3+cg==", + "license": "MIT", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/isomorphic-unfetch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz", + "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "unfetch": "^4.2.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/unfetch": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", + "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==", + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } +} diff --git a/pi/files/agent/skills/linear/package.json b/pi/files/agent/skills/linear/package.json new file mode 100644 index 0000000..b488118 --- /dev/null +++ b/pi/files/agent/skills/linear/package.json @@ -0,0 +1,10 @@ +{ + "name": "linear-skill", + "version": "1.0.0", + "type": "module", + "description": "Linear API skill for pi - manage issues, projects, and teams", + "license": "MIT", + "dependencies": { + "@linear/sdk": "^37.0.0" + } +}