pi-mono-all 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/LICENCE.md +7 -0
- package/node_modules/pi-common/package.json +22 -0
- package/node_modules/pi-common/src/auth-config.ts +290 -0
- package/node_modules/pi-common/src/auth.ts +63 -0
- package/node_modules/pi-common/src/cache.ts +60 -0
- package/node_modules/pi-common/src/errors.ts +47 -0
- package/node_modules/pi-common/src/http-client.ts +118 -0
- package/node_modules/pi-common/src/index.ts +7 -0
- package/node_modules/pi-common/src/rate-limiter.ts +32 -0
- package/node_modules/pi-common/src/tool-result.ts +27 -0
- package/node_modules/pi-mono-ask-user-question/CHANGELOG.md +185 -0
- package/node_modules/pi-mono-ask-user-question/README.md +226 -0
- package/node_modules/pi-mono-ask-user-question/index.ts +923 -0
- package/node_modules/pi-mono-ask-user-question/package.json +29 -0
- package/node_modules/pi-mono-auto-fix/CHANGELOG.md +59 -0
- package/node_modules/pi-mono-auto-fix/README.md +77 -0
- package/node_modules/pi-mono-auto-fix/index.ts +488 -0
- package/node_modules/pi-mono-auto-fix/package.json +23 -0
- package/node_modules/pi-mono-btw/CHANGELOG.md +180 -0
- package/node_modules/pi-mono-btw/README.md +24 -0
- package/node_modules/pi-mono-btw/index.ts +499 -0
- package/node_modules/pi-mono-btw/package.json +29 -0
- package/node_modules/pi-mono-clear/CHANGELOG.md +180 -0
- package/node_modules/pi-mono-clear/README.md +40 -0
- package/node_modules/pi-mono-clear/index.ts +45 -0
- package/node_modules/pi-mono-clear/package.json +29 -0
- package/node_modules/pi-mono-context/CHANGELOG.md +12 -0
- package/node_modules/pi-mono-context/README.md +74 -0
- package/node_modules/pi-mono-context/index.ts +641 -0
- package/node_modules/pi-mono-context/package.json +29 -0
- package/node_modules/pi-mono-context-guard/CHANGELOG.md +195 -0
- package/node_modules/pi-mono-context-guard/README.md +81 -0
- package/node_modules/pi-mono-context-guard/index.ts +212 -0
- package/node_modules/pi-mono-context-guard/package.json +23 -0
- package/node_modules/pi-mono-figma/CHANGELOG.md +59 -0
- package/node_modules/pi-mono-figma/README.md +236 -0
- package/node_modules/pi-mono-figma/__tests__/code-connect.test.ts +32 -0
- package/node_modules/pi-mono-figma/__tests__/figma-assets.test.ts +38 -0
- package/node_modules/pi-mono-figma/__tests__/figma-component-hints.test.ts +23 -0
- package/node_modules/pi-mono-figma/__tests__/figma-implementation-layout.test.ts +47 -0
- package/node_modules/pi-mono-figma/__tests__/figma-search.test.ts +51 -0
- package/node_modules/pi-mono-figma/__tests__/figma-summarizer.test.ts +65 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/complex-auto-layout.json +115 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/component-instance.json +50 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/hidden-and-vectors.json +28 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/variables-and-styles.json +40 -0
- package/node_modules/pi-mono-figma/docs/live-selection-bridge.md +16 -0
- package/node_modules/pi-mono-figma/index.ts +6 -0
- package/node_modules/pi-mono-figma/package.json +33 -0
- package/node_modules/pi-mono-figma/skills/figma/SKILL.md +143 -0
- package/node_modules/pi-mono-figma/src/code-connect.ts +110 -0
- package/node_modules/pi-mono-figma/src/figma-assets.ts +146 -0
- package/node_modules/pi-mono-figma/src/figma-cache.ts +6 -0
- package/node_modules/pi-mono-figma/src/figma-client.ts +471 -0
- package/node_modules/pi-mono-figma/src/figma-component-hints.ts +87 -0
- package/node_modules/pi-mono-figma/src/figma-implementation.ts +264 -0
- package/node_modules/pi-mono-figma/src/figma-schemas.ts +139 -0
- package/node_modules/pi-mono-figma/src/figma-search.ts +195 -0
- package/node_modules/pi-mono-figma/src/figma-summarizer.ts +673 -0
- package/node_modules/pi-mono-figma/src/figma-tokens.ts +57 -0
- package/node_modules/pi-mono-figma/src/figma-tools.ts +352 -0
- package/node_modules/pi-mono-linear/CHANGELOG.md +44 -0
- package/node_modules/pi-mono-linear/README.md +159 -0
- package/node_modules/pi-mono-linear/index.ts +6 -0
- package/node_modules/pi-mono-linear/package.json +30 -0
- package/node_modules/pi-mono-linear/skills/linear/SKILL.md +107 -0
- package/node_modules/pi-mono-linear/src/linear-client.ts +339 -0
- package/node_modules/pi-mono-linear/src/linear-queries.ts +101 -0
- package/node_modules/pi-mono-linear/src/linear-schemas.ts +90 -0
- package/node_modules/pi-mono-linear/src/linear-tools.ts +362 -0
- package/node_modules/pi-mono-loop/CHANGELOG.md +163 -0
- package/node_modules/pi-mono-loop/README.md +54 -0
- package/node_modules/pi-mono-loop/index.ts +291 -0
- package/node_modules/pi-mono-loop/package.json +26 -0
- package/node_modules/pi-mono-multi-edit/CHANGELOG.md +232 -0
- package/node_modules/pi-mono-multi-edit/README.md +244 -0
- package/node_modules/pi-mono-multi-edit/__tests__/classic.test.ts +277 -0
- package/node_modules/pi-mono-multi-edit/__tests__/diff.test.ts +77 -0
- package/node_modules/pi-mono-multi-edit/__tests__/patch.test.ts +287 -0
- package/node_modules/pi-mono-multi-edit/benchmark-edits.ts +966 -0
- package/node_modules/pi-mono-multi-edit/classic.ts +435 -0
- package/node_modules/pi-mono-multi-edit/diff.ts +143 -0
- package/node_modules/pi-mono-multi-edit/index.ts +266 -0
- package/node_modules/pi-mono-multi-edit/package.json +37 -0
- package/node_modules/pi-mono-multi-edit/patch.ts +463 -0
- package/node_modules/pi-mono-multi-edit/types.ts +53 -0
- package/node_modules/pi-mono-multi-edit/workspace.ts +85 -0
- package/node_modules/pi-mono-review/CHANGELOG.md +190 -0
- package/node_modules/pi-mono-review/README.md +30 -0
- package/node_modules/pi-mono-review/common.ts +930 -0
- package/node_modules/pi-mono-review/index.ts +8 -0
- package/node_modules/pi-mono-review/package.json +29 -0
- package/node_modules/pi-mono-review/review-tui.ts +194 -0
- package/node_modules/pi-mono-review/review.ts +119 -0
- package/node_modules/pi-mono-review/reviewer.ts +339 -0
- package/node_modules/pi-mono-sentinel/CHANGELOG.md +158 -0
- package/node_modules/pi-mono-sentinel/README.md +87 -0
- package/node_modules/pi-mono-sentinel/__tests__/output-scanner.test.ts +109 -0
- package/node_modules/pi-mono-sentinel/__tests__/permissions.test.ts +202 -0
- package/node_modules/pi-mono-sentinel/__tests__/whitelist.test.ts +59 -0
- package/node_modules/pi-mono-sentinel/guards/execution-tracker.ts +281 -0
- package/node_modules/pi-mono-sentinel/guards/output-scanner.ts +232 -0
- package/node_modules/pi-mono-sentinel/guards/permission-gate.ts +170 -0
- package/node_modules/pi-mono-sentinel/index.ts +43 -0
- package/node_modules/pi-mono-sentinel/package.json +26 -0
- package/node_modules/pi-mono-sentinel/patterns/permissions.ts +175 -0
- package/node_modules/pi-mono-sentinel/patterns/read-targets.ts +104 -0
- package/node_modules/pi-mono-sentinel/patterns/secrets.ts +143 -0
- package/node_modules/pi-mono-sentinel/session.ts +95 -0
- package/node_modules/pi-mono-sentinel/specs/2026/04/sentinel/001-permission-gate.md +145 -0
- package/node_modules/pi-mono-sentinel/types.ts +39 -0
- package/node_modules/pi-mono-sentinel/whitelist.ts +86 -0
- package/node_modules/pi-mono-simplify/CHANGELOG.md +163 -0
- package/node_modules/pi-mono-simplify/README.md +56 -0
- package/node_modules/pi-mono-simplify/index.ts +78 -0
- package/node_modules/pi-mono-simplify/package.json +29 -0
- package/node_modules/pi-mono-status-line/CHANGELOG.md +180 -0
- package/node_modules/pi-mono-status-line/README.md +96 -0
- package/node_modules/pi-mono-status-line/basic.ts +89 -0
- package/node_modules/pi-mono-status-line/expert.ts +689 -0
- package/node_modules/pi-mono-status-line/index.ts +54 -0
- package/node_modules/pi-mono-status-line/package.json +29 -0
- package/node_modules/pi-mono-team-mode/CHANGELOG.md +278 -0
- package/node_modules/pi-mono-team-mode/README.md +246 -0
- package/node_modules/pi-mono-team-mode/__tests__/agent-manager-transient.test.ts +75 -0
- package/node_modules/pi-mono-team-mode/__tests__/delegation-manager.test.ts +118 -0
- package/node_modules/pi-mono-team-mode/__tests__/formatters.test.ts +104 -0
- package/node_modules/pi-mono-team-mode/__tests__/model-config.test.ts +272 -0
- package/node_modules/pi-mono-team-mode/__tests__/notification-box.test.ts +34 -0
- package/node_modules/pi-mono-team-mode/__tests__/parallel-utils.test.ts +32 -0
- package/node_modules/pi-mono-team-mode/__tests__/pi-stream-parser.test.ts +64 -0
- package/node_modules/pi-mono-team-mode/__tests__/prompts.test.ts +106 -0
- package/node_modules/pi-mono-team-mode/__tests__/store.test.ts +164 -0
- package/node_modules/pi-mono-team-mode/__tests__/tasks.test.ts +267 -0
- package/node_modules/pi-mono-team-mode/__tests__/teammate-specs.test.ts +114 -0
- package/node_modules/pi-mono-team-mode/__tests__/widget.test.ts +41 -0
- package/node_modules/pi-mono-team-mode/__tests__/worktree.test.ts +78 -0
- package/node_modules/pi-mono-team-mode/core/chain-utils.ts +90 -0
- package/node_modules/pi-mono-team-mode/core/fs-utils.ts +44 -0
- package/node_modules/pi-mono-team-mode/core/model-config.ts +432 -0
- package/node_modules/pi-mono-team-mode/core/parallel-utils.ts +48 -0
- package/node_modules/pi-mono-team-mode/core/prompts.ts +158 -0
- package/node_modules/pi-mono-team-mode/core/store.ts +156 -0
- package/node_modules/pi-mono-team-mode/core/tasks.ts +99 -0
- package/node_modules/pi-mono-team-mode/core/teammate-specs.ts +124 -0
- package/node_modules/pi-mono-team-mode/core/types.ts +160 -0
- package/node_modules/pi-mono-team-mode/index.ts +825 -0
- package/node_modules/pi-mono-team-mode/managers/agent-manager.ts +654 -0
- package/node_modules/pi-mono-team-mode/managers/delegation-manager.ts +211 -0
- package/node_modules/pi-mono-team-mode/managers/task-manager.ts +238 -0
- package/node_modules/pi-mono-team-mode/managers/team-manager.ts +59 -0
- package/node_modules/pi-mono-team-mode/package.json +33 -0
- package/node_modules/pi-mono-team-mode/runtime/pi-stream-parser.ts +194 -0
- package/node_modules/pi-mono-team-mode/runtime/subprocess.ts +183 -0
- package/node_modules/pi-mono-team-mode/runtime/transient-session.ts +196 -0
- package/node_modules/pi-mono-team-mode/runtime/worktree.ts +90 -0
- package/node_modules/pi-mono-team-mode/ui/formatters.ts +149 -0
- package/node_modules/pi-mono-team-mode/ui/notification-box.ts +55 -0
- package/node_modules/pi-mono-team-mode/ui/widget.ts +94 -0
- package/package.json +76 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface FigmaTokenMap {
|
|
2
|
+
styles: Record<string, { key?: string; name: string; type?: string; description?: string }>;
|
|
3
|
+
variables: Record<string, { key?: string; name: string; collectionName?: string; resolvedType?: string }>;
|
|
4
|
+
collections: Record<string, { name: string; modes?: Array<{ modeId: string; name: string }> }>;
|
|
5
|
+
warnings: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function buildFigmaTokenMap(stylesResponse: unknown, variablesResponse: unknown): FigmaTokenMap {
|
|
9
|
+
const warnings: string[] = [];
|
|
10
|
+
const styles: FigmaTokenMap["styles"] = {};
|
|
11
|
+
for (const style of getNestedArray(stylesResponse, ["meta", "styles"])) {
|
|
12
|
+
const record = asRecord(style);
|
|
13
|
+
const id = stringValue(record.node_id) ?? stringValue(record.nodeId) ?? stringValue(record.key);
|
|
14
|
+
if (!id) continue;
|
|
15
|
+
styles[id] = { key: stringValue(record.key), name: String(record.name ?? id), type: stringValue(record.style_type) ?? stringValue(record.styleType), description: stringValue(record.description) };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const variables: FigmaTokenMap["variables"] = {};
|
|
19
|
+
const collections: FigmaTokenMap["collections"] = {};
|
|
20
|
+
const meta = asRecord(asRecord(variablesResponse).meta ?? variablesResponse);
|
|
21
|
+
const rawCollections = asRecord(meta.variableCollections ?? meta.variable_collections);
|
|
22
|
+
for (const [collectionId, raw] of Object.entries(rawCollections)) {
|
|
23
|
+
const record = asRecord(raw);
|
|
24
|
+
collections[collectionId] = {
|
|
25
|
+
name: String(record.name ?? collectionId),
|
|
26
|
+
modes: Array.isArray(record.modes) ? record.modes.map((mode) => ({ modeId: String(asRecord(mode).modeId ?? asRecord(mode).mode_id ?? ""), name: String(asRecord(mode).name ?? "Mode") })) : undefined,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const rawVariables = asRecord(meta.variables);
|
|
30
|
+
for (const [variableId, raw] of Object.entries(rawVariables)) {
|
|
31
|
+
const record = asRecord(raw);
|
|
32
|
+
const collectionId = stringValue(record.variableCollectionId) ?? stringValue(record.variable_collection_id);
|
|
33
|
+
variables[variableId] = {
|
|
34
|
+
key: stringValue(record.key),
|
|
35
|
+
name: String(record.name ?? variableId),
|
|
36
|
+
collectionName: collectionId ? collections[collectionId]?.name : undefined,
|
|
37
|
+
resolvedType: stringValue(record.resolvedType) ?? stringValue(record.resolved_type),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (!Object.keys(styles).length) warnings.push("No named styles were available to resolve style IDs.");
|
|
41
|
+
if (!Object.keys(variables).length) warnings.push("No local variables were available to resolve variable IDs.");
|
|
42
|
+
return { styles, variables, collections, warnings };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getNestedArray(value: unknown, path: readonly string[]): unknown[] {
|
|
46
|
+
let current = value;
|
|
47
|
+
for (const segment of path) current = asRecord(current)[segment];
|
|
48
|
+
return Array.isArray(current) ? current : [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
52
|
+
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function stringValue(value: unknown): string | undefined {
|
|
56
|
+
return typeof value === "string" ? value : undefined;
|
|
57
|
+
}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { registerAuthConfigurator, runWithAuthRetry, type AuthConfiguratorOptions } from "pi-common/auth-config";
|
|
3
|
+
import { jsonToolResult, textToolResult } from "pi-common/tool-result";
|
|
4
|
+
import { FigmaClient, parseFigmaUrl } from "./figma-client.js";
|
|
5
|
+
import {
|
|
6
|
+
FigmaGetDesignContextParams,
|
|
7
|
+
FigmaComponentImplementationHintsParams,
|
|
8
|
+
FigmaExtractAssetsParams,
|
|
9
|
+
FigmaFindCodeConnectMappingParams,
|
|
10
|
+
FigmaGetFileParams,
|
|
11
|
+
FigmaGetNodesParams,
|
|
12
|
+
FigmaFindNodesParams,
|
|
13
|
+
FigmaImplementationContextParams,
|
|
14
|
+
FigmaProcessedNodeParams,
|
|
15
|
+
FigmaProcessedNodeWithRenderParams,
|
|
16
|
+
FigmaParseUrlParams,
|
|
17
|
+
FigmaRenderNodesParams,
|
|
18
|
+
FigmaSearchComponentsParams,
|
|
19
|
+
FigmaSingleFileParams,
|
|
20
|
+
} from "./figma-schemas.js";
|
|
21
|
+
|
|
22
|
+
const DEFAULT_PROCESSED_MAX_CHARS = 20_000;
|
|
23
|
+
const DEFAULT_RAW_MAX_CHARS = 40_000;
|
|
24
|
+
|
|
25
|
+
interface ProcessedNodeParams {
|
|
26
|
+
fileKey: string;
|
|
27
|
+
nodeId: string;
|
|
28
|
+
depth?: number;
|
|
29
|
+
includeHidden?: boolean;
|
|
30
|
+
includeVectors?: boolean;
|
|
31
|
+
includeComponentInternals?: boolean;
|
|
32
|
+
renderImage?: boolean;
|
|
33
|
+
outputDir?: string;
|
|
34
|
+
format?: "png" | "jpg" | "svg" | "pdf";
|
|
35
|
+
scale?: number;
|
|
36
|
+
maxResponseChars?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ImplementationContextParams extends ProcessedNodeParams {
|
|
40
|
+
framework?: "react" | "html" | "vue" | "angular" | "react-native";
|
|
41
|
+
styling?: "css" | "css-modules" | "styled-components" | "tailwind" | "inline";
|
|
42
|
+
resolveTokens?: boolean;
|
|
43
|
+
includeCodeSnippets?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const FIGMA_AUTH: AuthConfiguratorOptions = {
|
|
47
|
+
service: "figma",
|
|
48
|
+
displayName: "Figma",
|
|
49
|
+
envName: "FIGMA_TOKEN",
|
|
50
|
+
authPath: ["figma", "token"],
|
|
51
|
+
commandName: "figma-auth",
|
|
52
|
+
toolName: "figma_configure_auth",
|
|
53
|
+
tokenUrl: "https://www.figma.com/settings/tokens",
|
|
54
|
+
scopeInstructions: ["Enable File content/read access for the files, projects, or team you want pi to inspect.", "No write/admin scopes are required."],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function withFigmaAuth<T>(ctx: ExtensionContext, operation: () => Promise<T>): Promise<T> {
|
|
58
|
+
return runWithAuthRetry(ctx, FIGMA_AUTH, operation);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function registerFigmaTools(pi: ExtensionAPI): void {
|
|
62
|
+
const client = new FigmaClient();
|
|
63
|
+
registerAuthConfigurator(pi, FIGMA_AUTH);
|
|
64
|
+
|
|
65
|
+
pi.registerTool({
|
|
66
|
+
name: "figma_parse_url",
|
|
67
|
+
label: "Figma Parse URL",
|
|
68
|
+
description: "Parse a Figma URL into fileKey and nodeId values for the other figma_* tools.",
|
|
69
|
+
promptSnippet: "Parse Figma URLs into file key and node ID.",
|
|
70
|
+
parameters: FigmaParseUrlParams,
|
|
71
|
+
async execute(_toolCallId, params) {
|
|
72
|
+
return jsonToolResult(parseFigmaUrl(params.url));
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
pi.registerTool({
|
|
77
|
+
name: "figma_get_design_context",
|
|
78
|
+
label: "Figma Design Context",
|
|
79
|
+
description: "Fetch compact LLM-ready Figma context. With nodeId returns target node summary, ancestors/page, and sibling names; without nodeId returns canvases and top-level frames only.",
|
|
80
|
+
promptSnippet: "Explore compact Figma file structure and a target node summary without full raw JSON.",
|
|
81
|
+
promptGuidelines: [
|
|
82
|
+
"Use figma_configure_auth only when Figma auth is missing, invalid, expired, or the user asks to update the token; never ask the user to paste tokens in chat.",
|
|
83
|
+
"Use figma_parse_url, figma_render_nodes, and figma_explain_node or figma_get_node_summary as the default workflow.",
|
|
84
|
+
"Use figma_get_implementation_context when translating a design into code.",
|
|
85
|
+
"Do not call figma_get_nodes by default; use it only when raw Figma JSON is explicitly needed or when debugging the extension.",
|
|
86
|
+
"Use figma_render_nodes when screenshots or visual assets are needed.",
|
|
87
|
+
],
|
|
88
|
+
parameters: FigmaGetDesignContextParams,
|
|
89
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
90
|
+
const result = await withFigmaAuth(ctx, () => client.getDesignContext(params.fileKey, params.nodeId));
|
|
91
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
pi.registerTool({
|
|
96
|
+
name: "figma_find_nodes_by_name",
|
|
97
|
+
label: "Figma Find Nodes By Name",
|
|
98
|
+
description: "Search Figma layer/node names within a file or scoped subtree. Returns compact path-aware matches with result caps; prefer this before raw JSON exploration.",
|
|
99
|
+
promptSnippet: "Find Figma nodes/layers by name before choosing a target frame.",
|
|
100
|
+
parameters: FigmaFindNodesParams,
|
|
101
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
102
|
+
const result = await withFigmaAuth(ctx, () => client.findNodesByName(params.fileKey, params));
|
|
103
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
pi.registerTool({
|
|
108
|
+
name: "figma_find_nodes_by_text",
|
|
109
|
+
label: "Figma Find Nodes By Text",
|
|
110
|
+
description: "Search visible Figma text nodes within a file or scoped subtree and return compact path-aware matches with nearest parent context.",
|
|
111
|
+
promptSnippet: "Find Figma nodes by visible text before choosing a target frame.",
|
|
112
|
+
parameters: FigmaFindNodesParams,
|
|
113
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
114
|
+
const result = await withFigmaAuth(ctx, () => client.findNodesByText(params.fileKey, params));
|
|
115
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
pi.registerTool({
|
|
120
|
+
name: "figma_get_node_summary",
|
|
121
|
+
label: "Figma Node Summary",
|
|
122
|
+
description: "Fetch a compact structured summary of a Figma node: dimensions, layout, spacing, styles, visible text, component properties, and shallow child hierarchy. Default depth is 2; hidden nodes, vectors, and component internals are omitted by default.",
|
|
123
|
+
promptSnippet: "Get LLM-ready structured summaries of Figma frames/components.",
|
|
124
|
+
parameters: FigmaProcessedNodeParams,
|
|
125
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
126
|
+
const result = await withFigmaAuth(ctx, () => client.getNodeSummary(params.fileKey, params.nodeId, processedOptions(params)));
|
|
127
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
pi.registerTool({
|
|
132
|
+
name: "figma_extract_text",
|
|
133
|
+
label: "Figma Extract Text",
|
|
134
|
+
description: "Extract visible text nodes from a Figma node without raw JSON. Hidden text is excluded by default and results are capped for LLM readability.",
|
|
135
|
+
promptSnippet: "Extract visible text from Figma designs.",
|
|
136
|
+
parameters: FigmaProcessedNodeParams,
|
|
137
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
138
|
+
const result = await withFigmaAuth(ctx, () => client.extractText(params.fileKey, params.nodeId, processedOptions(params)));
|
|
139
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
pi.registerTool({
|
|
144
|
+
name: "figma_explain_node",
|
|
145
|
+
label: "Figma Explain Node",
|
|
146
|
+
description: "Explain a Figma node in human-readable Markdown using compact summary, visible text, shallow hierarchy, and optional rendered image asset. Primary tool for questions like 'Explain this component'.",
|
|
147
|
+
promptSnippet: "Explain a Figma component/frame in Markdown without raw JSON.",
|
|
148
|
+
parameters: FigmaProcessedNodeWithRenderParams,
|
|
149
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
150
|
+
const assets = params.renderImage ? await withFigmaAuth(ctx, () => renderAssets(client, ctx, params)) : undefined;
|
|
151
|
+
const result = await withFigmaAuth(ctx, () => client.explainNode(params.fileKey, params.nodeId, { ...processedOptions(params), assets }));
|
|
152
|
+
return limitedTextToolResult(result, params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS);
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
pi.registerTool({
|
|
157
|
+
name: "figma_get_implementation_context",
|
|
158
|
+
label: "Figma Implementation Context",
|
|
159
|
+
description: "Return concise design-to-code context for a Figma node: purpose, sections, fields/buttons, measurements, typography, colors, spacing, CSS layout/responsive hints, accessibility hints, design tokens, assets, and optional framework starter snippets.",
|
|
160
|
+
promptSnippet: "Get coding-ready Figma implementation context.",
|
|
161
|
+
parameters: FigmaImplementationContextParams,
|
|
162
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
163
|
+
const assets = params.renderImage ? await withFigmaAuth(ctx, () => renderAssets(client, ctx, params)) : undefined;
|
|
164
|
+
const result = await withFigmaAuth(ctx, () =>
|
|
165
|
+
client.getImplementationContext(params.fileKey, params.nodeId, { ...processedOptions(params), assets, ...implementationOptions(params) }),
|
|
166
|
+
);
|
|
167
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
pi.registerTool({
|
|
172
|
+
name: "figma_get_file",
|
|
173
|
+
label: "Figma File",
|
|
174
|
+
description: "Fetch a raw Figma file JSON document. Use only when raw Figma JSON is explicitly needed or when debugging the extension; prefer figma_get_node_summary, figma_explain_node, or figma_get_design_context.",
|
|
175
|
+
parameters: FigmaGetFileParams,
|
|
176
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
177
|
+
const result = await withFigmaAuth(ctx, () => client.getFile(params.fileKey, params.depth));
|
|
178
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_RAW_MAX_CHARS });
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
pi.registerTool({
|
|
183
|
+
name: "figma_get_nodes",
|
|
184
|
+
label: "Figma Nodes",
|
|
185
|
+
description: "Fetch raw Figma JSON for one or more nodes/frames/components by node ID. Use only when raw Figma JSON is explicitly needed or when debugging the extension; do not use by default.",
|
|
186
|
+
parameters: FigmaGetNodesParams,
|
|
187
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
188
|
+
const result = await withFigmaAuth(ctx, () => client.getNodes(params.fileKey, params.nodeIds));
|
|
189
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_RAW_MAX_CHARS });
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
pi.registerTool({
|
|
194
|
+
name: "figma_get_node_metadata",
|
|
195
|
+
label: "Figma Node Metadata",
|
|
196
|
+
description: "Fetch compact spatial/layout metadata for one or more Figma nodes.",
|
|
197
|
+
parameters: FigmaGetNodesParams,
|
|
198
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
199
|
+
const result = await withFigmaAuth(ctx, () => client.getNodeMetadata(params.fileKey, params.nodeIds));
|
|
200
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
pi.registerTool({
|
|
205
|
+
name: "figma_get_styles",
|
|
206
|
+
label: "Figma Styles",
|
|
207
|
+
description: "Fetch named styles from a Figma file, including colors, text, effects, and grids.",
|
|
208
|
+
parameters: FigmaSingleFileParams,
|
|
209
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
210
|
+
const result = await withFigmaAuth(ctx, () => client.getStyles(params.fileKey));
|
|
211
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
pi.registerTool({
|
|
216
|
+
name: "figma_get_variables",
|
|
217
|
+
label: "Figma Variables",
|
|
218
|
+
description: "Fetch local Figma variables and collections for design tokens.",
|
|
219
|
+
parameters: FigmaSingleFileParams,
|
|
220
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
221
|
+
const result = await withFigmaAuth(ctx, () => client.getVariables(params.fileKey));
|
|
222
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
pi.registerTool({
|
|
227
|
+
name: "figma_get_components",
|
|
228
|
+
label: "Figma Components",
|
|
229
|
+
description: "Fetch Figma component metadata for a file.",
|
|
230
|
+
parameters: FigmaSingleFileParams,
|
|
231
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
232
|
+
const result = await withFigmaAuth(ctx, () => client.getComponents(params.fileKey));
|
|
233
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
pi.registerTool({
|
|
238
|
+
name: "figma_get_component_sets",
|
|
239
|
+
label: "Figma Component Sets",
|
|
240
|
+
description: "Fetch Figma component set metadata for a file.",
|
|
241
|
+
parameters: FigmaSingleFileParams,
|
|
242
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
243
|
+
const result = await withFigmaAuth(ctx, () => client.getComponentSets(params.fileKey));
|
|
244
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
pi.registerTool({
|
|
249
|
+
name: "figma_search_components",
|
|
250
|
+
label: "Figma Search Components",
|
|
251
|
+
description: "Search Figma components in a file by name or description.",
|
|
252
|
+
parameters: FigmaSearchComponentsParams,
|
|
253
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
254
|
+
const result = await withFigmaAuth(ctx, () => client.searchComponents(params.fileKey, params.query));
|
|
255
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
pi.registerTool({
|
|
260
|
+
name: "figma_render_nodes",
|
|
261
|
+
label: "Figma Render Nodes",
|
|
262
|
+
description: "Render one or more Figma nodes to image URLs and optionally download them as local assets.",
|
|
263
|
+
parameters: FigmaRenderNodesParams,
|
|
264
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
265
|
+
const result = await withFigmaAuth(ctx, () =>
|
|
266
|
+
client.renderNodes(params.fileKey, params.nodeIds, {
|
|
267
|
+
cwd: ctx.cwd,
|
|
268
|
+
outputDir: params.outputDir,
|
|
269
|
+
format: params.format,
|
|
270
|
+
scale: params.scale,
|
|
271
|
+
download: params.download,
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
pi.registerTool({
|
|
279
|
+
name: "figma_extract_assets",
|
|
280
|
+
label: "Figma Extract Assets",
|
|
281
|
+
description: "Extract a compact asset manifest for a Figma subtree, including SVG/icon exports, node renders, image fills, node paths, hashes, sizes, and local file paths when downloaded.",
|
|
282
|
+
promptSnippet: "Extract icons, renders, and image fills from a Figma frame as an asset manifest.",
|
|
283
|
+
parameters: FigmaExtractAssetsParams,
|
|
284
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
285
|
+
const result = await withFigmaAuth(ctx, () => client.extractAssets(params.fileKey, params.nodeId, { ...params, cwd: ctx.cwd }));
|
|
286
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
pi.registerTool({
|
|
291
|
+
name: "figma_find_code_connect_mapping",
|
|
292
|
+
label: "Figma Find Code Connect Mapping",
|
|
293
|
+
description: "Scan the local repo for Code Connect files, figma.connect calls, Figma URLs, node IDs, and component keys matching a target. No live Figma request is required.",
|
|
294
|
+
promptSnippet: "Find local Code Connect or Figma-node-to-component mappings in the current repo.",
|
|
295
|
+
parameters: FigmaFindCodeConnectMappingParams,
|
|
296
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
297
|
+
const result = await client.findCodeConnectMapping({ ...params, cwd: ctx.cwd });
|
|
298
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
pi.registerTool({
|
|
303
|
+
name: "figma_get_component_implementation_hints",
|
|
304
|
+
label: "Figma Component Implementation Hints",
|
|
305
|
+
description: "Combine Figma summary, implementation context, variants/properties, accessibility, token dependencies, assets, optional local Code Connect matches, and starter snippets into compact component implementation guidance.",
|
|
306
|
+
promptSnippet: "Get high-level implementation hints for turning a Figma component into code.",
|
|
307
|
+
parameters: FigmaComponentImplementationHintsParams,
|
|
308
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
309
|
+
const result = await withFigmaAuth(ctx, () => client.getComponentImplementationHints(params.fileKey, params.nodeId, { ...processedOptions(params), ...params, cwd: ctx.cwd }));
|
|
310
|
+
return jsonToolResult(result, { maxChars: params.maxResponseChars ?? DEFAULT_PROCESSED_MAX_CHARS });
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function processedOptions(params: ProcessedNodeParams) {
|
|
316
|
+
return {
|
|
317
|
+
depth: params.depth,
|
|
318
|
+
includeHidden: params.includeHidden,
|
|
319
|
+
includeVectors: params.includeVectors,
|
|
320
|
+
includeComponentInternals: params.includeComponentInternals,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function implementationOptions(params: ImplementationContextParams) {
|
|
325
|
+
return {
|
|
326
|
+
framework: params.framework,
|
|
327
|
+
styling: params.styling,
|
|
328
|
+
resolveTokens: params.resolveTokens,
|
|
329
|
+
includeCodeSnippets: params.includeCodeSnippets,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function renderAssets(client: FigmaClient, ctx: ExtensionContext, params: ProcessedNodeParams): Promise<Array<{ nodeId: string; url?: string | null; path?: string }>> {
|
|
334
|
+
const rendered = await client.renderNodes(params.fileKey, [params.nodeId], {
|
|
335
|
+
cwd: ctx.cwd,
|
|
336
|
+
outputDir: params.outputDir,
|
|
337
|
+
format: params.format,
|
|
338
|
+
scale: params.scale,
|
|
339
|
+
download: true,
|
|
340
|
+
});
|
|
341
|
+
return Object.entries(rendered.images).map(([nodeId, url]) => ({
|
|
342
|
+
nodeId,
|
|
343
|
+
url,
|
|
344
|
+
path: rendered.savedFiles.find((file) => file.nodeId === nodeId)?.path,
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function limitedTextToolResult(text: string, maxChars: number) {
|
|
349
|
+
const truncated = text.length > maxChars;
|
|
350
|
+
const output = truncated ? `${text.slice(0, maxChars)}\n\n[truncated ${text.length - maxChars} characters; call figma_get_node_summary on a narrower child node]` : text;
|
|
351
|
+
return textToolResult(output, { truncated, characters: text.length });
|
|
352
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# pi-mono-linear
|
|
2
|
+
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Add the all-in-one pi package and bundle the shared pi-common workspace package into distributed packages.
|
|
8
|
+
|
|
9
|
+
## 0.2.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- Added `linear_upload_file` to upload local images, videos, and generic files to Linear and return sanitized asset metadata.
|
|
14
|
+
- Added `linear_upload_file_to_issue_comment` to upload a local file and create a Markdown issue comment with the resulting Linear asset URL.
|
|
15
|
+
|
|
16
|
+
## 0.1.2
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
### Fixed: package extension entrypoint
|
|
21
|
+
|
|
22
|
+
- Added the package root `index.ts` extension entrypoint so pi can load the Linear tools from the published package manifest.
|
|
23
|
+
|
|
24
|
+
## 0.1.1
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
### Enhanced: auth setup
|
|
29
|
+
|
|
30
|
+
- Added `/linear-auth` command and `linear_configure_auth` tool for masked local API-key capture.
|
|
31
|
+
- Linear tools now prompt for a fresh key and retry once when auth is missing, invalid, or expired.
|
|
32
|
+
- Key setup writes to `~/.pi/agent/auth.json` without returning the key to the model.
|
|
33
|
+
|
|
34
|
+
## 0.1.0
|
|
35
|
+
|
|
36
|
+
### Minor Changes
|
|
37
|
+
|
|
38
|
+
### New Extension: linear
|
|
39
|
+
|
|
40
|
+
- Added native Linear GraphQL tools for workspace metadata, teams, issues, projects, workflow states, labels, users, comments, cycles, and documents.
|
|
41
|
+
- Added mutation tools for creating issues, updating issues, and creating comments.
|
|
42
|
+
- Added bundled `linear` skill that explains when and how to use the native `linear_*` tools.
|
|
43
|
+
- Added authentication support via `LINEAR_API_KEY` and `~/.pi/agent/auth.json` at `.linear.key`.
|
|
44
|
+
- Added README documentation for API key creation, required Linear read/write permissions, and setup/troubleshooting.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# pi-mono-linear
|
|
2
|
+
|
|
3
|
+
A pi extension and skill package that exposes native Linear GraphQL tools for issue, project, team, user, comment, file upload, cycle, label, workflow-state, and document workflows.
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
### Workspace and users
|
|
8
|
+
|
|
9
|
+
- `linear_whoami`
|
|
10
|
+
- `linear_workspace_metadata`
|
|
11
|
+
- `linear_list_teams`
|
|
12
|
+
- `linear_get_team`
|
|
13
|
+
- `linear_list_users`
|
|
14
|
+
- `linear_get_user`
|
|
15
|
+
|
|
16
|
+
### Issues
|
|
17
|
+
|
|
18
|
+
- `linear_list_issues`
|
|
19
|
+
- `linear_get_issue`
|
|
20
|
+
- `linear_search_issues`
|
|
21
|
+
- `linear_list_my_issues`
|
|
22
|
+
- `linear_create_issue`
|
|
23
|
+
- `linear_update_issue`
|
|
24
|
+
|
|
25
|
+
### Metadata and related records
|
|
26
|
+
|
|
27
|
+
- `linear_list_projects`
|
|
28
|
+
- `linear_get_project`
|
|
29
|
+
- `linear_list_issue_statuses`
|
|
30
|
+
- `linear_get_issue_status`
|
|
31
|
+
- `linear_list_labels`
|
|
32
|
+
- `linear_list_cycles`
|
|
33
|
+
- `linear_list_documents`
|
|
34
|
+
- `linear_get_document`
|
|
35
|
+
|
|
36
|
+
### Comments
|
|
37
|
+
|
|
38
|
+
- `linear_list_comments`
|
|
39
|
+
- `linear_create_comment`
|
|
40
|
+
- `linear_configure_auth`
|
|
41
|
+
|
|
42
|
+
### Files
|
|
43
|
+
|
|
44
|
+
- `linear_upload_file`
|
|
45
|
+
- `linear_upload_file_to_issue_comment`
|
|
46
|
+
|
|
47
|
+
The package also bundles the `linear` skill under `skills/linear/SKILL.md`.
|
|
48
|
+
|
|
49
|
+
## Authentication
|
|
50
|
+
|
|
51
|
+
The extension looks for a Linear API key in this order:
|
|
52
|
+
|
|
53
|
+
1. in-memory key override created by `/linear-auth --force` or `linear_configure_auth`
|
|
54
|
+
2. `LINEAR_API_KEY` environment variable
|
|
55
|
+
3. `~/.pi/agent/auth.json` at `.linear.key`
|
|
56
|
+
|
|
57
|
+
If no key is found, or if Linear rejects the key as invalid/expired, the native tools can prompt you with a masked local dialog and store the replacement key without returning it to the model.
|
|
58
|
+
|
|
59
|
+
Do **not** paste API keys into an LLM chat.
|
|
60
|
+
|
|
61
|
+
### Recommended: native auth command
|
|
62
|
+
|
|
63
|
+
Run this in pi:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
/linear-auth --force
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The command shows the Linear API-key URL and required permissions, prompts for the key with masked input, and writes it to `~/.pi/agent/auth.json` at `.linear.key`.
|
|
70
|
+
|
|
71
|
+
The same flow is available to the agent through the `linear_configure_auth` tool. That tool returns only metadata such as `stored: true`; it never returns the key.
|
|
72
|
+
|
|
73
|
+
If a normal `linear_*` request fails because the key is missing, invalid, or expired, the extension retries once after prompting you for a fresh key.
|
|
74
|
+
|
|
75
|
+
### Option A: environment variable
|
|
76
|
+
|
|
77
|
+
For the current shell session:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
export LINEAR_API_KEY="lin_api_xxx"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
To persist it, add that export to your shell profile (`~/.zshrc`, `~/.bashrc`, etc.) or your preferred secrets manager.
|
|
84
|
+
|
|
85
|
+
### Option B: pi auth file
|
|
86
|
+
|
|
87
|
+
Create or update `~/.pi/agent/auth.json`:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"linear": {
|
|
92
|
+
"key": "lin_api_xxx"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Recommended file permissions:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
mkdir -p ~/.pi/agent
|
|
101
|
+
chmod 700 ~/.pi/agent
|
|
102
|
+
chmod 600 ~/.pi/agent/auth.json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
If the file already contains other credentials, merge the `linear.key` entry instead of overwriting the file.
|
|
106
|
+
|
|
107
|
+
## Creating a Linear API key
|
|
108
|
+
|
|
109
|
+
1. Open Linear.
|
|
110
|
+
2. Go to **Settings → Account → Security & Access**.
|
|
111
|
+
3. Under **Personal API keys**, click **Create key**.
|
|
112
|
+
4. Name it something recognizable, for example `pi-agent`.
|
|
113
|
+
5. Choose the minimum access level needed for your workflow.
|
|
114
|
+
6. If your workspace supports team restrictions, restrict the key to only the teams pi needs.
|
|
115
|
+
7. Copy the key once and store it via `LINEAR_API_KEY` or `~/.pi/agent/auth.json`.
|
|
116
|
+
|
|
117
|
+
## Required scopes / permissions
|
|
118
|
+
|
|
119
|
+
Use the least privilege that supports the tools you need:
|
|
120
|
+
|
|
121
|
+
- **Read** is enough for read-only tools such as `linear_workspace_metadata`, `linear_search_issues`, `linear_get_issue`, list tools, and document/comment reads.
|
|
122
|
+
- **Write** is required for mutation tools: `linear_create_issue`, `linear_update_issue`, `linear_create_comment`, `linear_upload_file`, and `linear_upload_file_to_issue_comment`.
|
|
123
|
+
- **Admin** is not required for this extension's tools.
|
|
124
|
+
|
|
125
|
+
If you restrict the key to specific teams, the key must include the teams/projects/issues you want to query or mutate.
|
|
126
|
+
|
|
127
|
+
Linear API keys are sent in the `Authorization` header as the raw key value; do not prefix them with `Bearer`.
|
|
128
|
+
|
|
129
|
+
## Usage tips
|
|
130
|
+
|
|
131
|
+
- Use `linear_workspace_metadata` first when team/project/state/label/user IDs are unknown.
|
|
132
|
+
- Use `linear_search_issues` for keyword lookup.
|
|
133
|
+
- Use `linear_get_issue` before updating an issue or creating a comment.
|
|
134
|
+
- Use `linear_list_issues` for filtered issue lists by team, assignee, status, and limit.
|
|
135
|
+
- Use `linear_upload_file` to upload a local image, video, or generic file and return a Linear asset URL.
|
|
136
|
+
- Use `linear_upload_file_to_issue_comment` after `linear_get_issue` to upload a local file and post a Markdown comment. Images are rendered with image Markdown; other files use links.
|
|
137
|
+
- File upload tool results return sanitized metadata and the stable Linear asset URL. They do not return local file bytes, signed upload URLs, or upload headers.
|
|
138
|
+
|
|
139
|
+
## Troubleshooting
|
|
140
|
+
|
|
141
|
+
### Missing auth key
|
|
142
|
+
|
|
143
|
+
Set `LINEAR_API_KEY` or add `.linear.key` to `~/.pi/agent/auth.json`.
|
|
144
|
+
|
|
145
|
+
### Permission errors
|
|
146
|
+
|
|
147
|
+
Confirm the key has the right access level:
|
|
148
|
+
|
|
149
|
+
- Read-only workflows need **Read**.
|
|
150
|
+
- Create/update/comment workflows need **Write**.
|
|
151
|
+
- Team-restricted keys must include the relevant team.
|
|
152
|
+
|
|
153
|
+
### Custom Linear endpoint
|
|
154
|
+
|
|
155
|
+
By default, the extension uses `https://api.linear.app/graphql`. Override it with:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
export LINEAR_GRAPHQL_URL="https://api.linear.app/graphql"
|
|
159
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-mono-linear",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Pi extension and skill for Linear GraphQL tools",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi-extension",
|
|
9
|
+
"pi-skill",
|
|
10
|
+
"linear"
|
|
11
|
+
],
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"pi-common": "workspace:*"
|
|
14
|
+
},
|
|
15
|
+
"bundledDependencies": [
|
|
16
|
+
"pi-common"
|
|
17
|
+
],
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
20
|
+
"@sinclair/typebox": "*"
|
|
21
|
+
},
|
|
22
|
+
"pi": {
|
|
23
|
+
"extensions": [
|
|
24
|
+
"./index.ts"
|
|
25
|
+
],
|
|
26
|
+
"skills": [
|
|
27
|
+
"./skills"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|