Compare commits
6 Commits
db41ec6e93
...
nixos
| Author | SHA1 | Date | |
|---|---|---|---|
| 85632c2e29 | |||
| 63caa82199 | |||
| 39e7bddb35 | |||
| d5b4042b06 | |||
| 4d19e7d320 | |||
| 227c1638f6 |
@@ -377,7 +377,7 @@
|
|||||||
"osdPosition": 5,
|
"osdPosition": 5,
|
||||||
"osdVolumeEnabled": true,
|
"osdVolumeEnabled": true,
|
||||||
"osdMediaVolumeEnabled": true,
|
"osdMediaVolumeEnabled": true,
|
||||||
"osdMediaPlaybackEnabled": true,
|
"osdMediaPlaybackEnabled": false,
|
||||||
"osdBrightnessEnabled": true,
|
"osdBrightnessEnabled": true,
|
||||||
"osdIdleInhibitorEnabled": true,
|
"osdIdleInhibitorEnabled": true,
|
||||||
"osdMicMuteEnabled": true,
|
"osdMicMuteEnabled": true,
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---@class SigilConfig
|
||||||
|
---@field target table<string, string|boolean>
|
||||||
|
---@field ignore? string[]
|
||||||
|
|
||||||
|
---@type SigilConfig
|
||||||
|
local config = {
|
||||||
|
target = {
|
||||||
|
linux = "~/.config/gsf",
|
||||||
|
default = "~/.config/gsf",
|
||||||
|
},
|
||||||
|
ignore = {
|
||||||
|
-- "**/.DS_Store",
|
||||||
|
-- "**/*.tmp",
|
||||||
|
-- "cache/**",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"mode": 0,
|
||||||
|
"sens_mult": 1.5,
|
||||||
|
"yx_ratio": 1.0,
|
||||||
|
"input_dpi": 400.0,
|
||||||
|
"angle_rotation": 0.0,
|
||||||
|
"accel": 2.0,
|
||||||
|
"offset_linear": 3.5,
|
||||||
|
"output_cap": 30.0,
|
||||||
|
"decay_rate": 0.1,
|
||||||
|
"offset_natural": 0.0,
|
||||||
|
"limit": 2.0,
|
||||||
|
"gamma": 1.0,
|
||||||
|
"smooth": 0.5,
|
||||||
|
"motivity": 1.5,
|
||||||
|
"sync_speed": 5.0
|
||||||
|
}
|
||||||
@@ -89,8 +89,7 @@ vim.o.confirm = true
|
|||||||
|
|
||||||
-- vim.o.winborder = "rounded"
|
-- vim.o.winborder = "rounded"
|
||||||
|
|
||||||
-- Clipboard: prefer system clipboard, and over SSH use OSC52 so yanks can reach local clipboard
|
-- Clipboard: keep default y/p behavior; over SSH, route + register through OSC52
|
||||||
vim.opt.clipboard = "unnamedplus"
|
|
||||||
if vim.env.SSH_TTY then
|
if vim.env.SSH_TTY then
|
||||||
vim.g.clipboard = "osc52"
|
vim.g.clipboard = "osc52"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"activePack": "glados",
|
"activePack": "glados",
|
||||||
"volume": 1,
|
"volume": 1,
|
||||||
"muted": true,
|
"muted": false,
|
||||||
"enabledCategories": {
|
"enabledCategories": {
|
||||||
"session.start": true,
|
"session.start": true,
|
||||||
"task.acknowledge": true,
|
"task.acknowledge": true,
|
||||||
|
|||||||
@@ -10,4 +10,4 @@
|
|||||||
"input.required": true,
|
"input.required": true,
|
||||||
"resource.limit": true
|
"resource.limit": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"lastChangelogVersion": "0.60.0",
|
"lastChangelogVersion": "0.63.1",
|
||||||
"defaultProvider": "openai-codex",
|
"defaultProvider": "openai-codex",
|
||||||
"defaultModel": "gpt-5.3-codex",
|
"defaultModel": "gpt-5.3-codex",
|
||||||
"defaultThinkingLevel": "medium",
|
"defaultThinkingLevel": "high",
|
||||||
"theme": "matugen",
|
"theme": "matugen",
|
||||||
"lsp": {
|
"lsp": {
|
||||||
"hookMode": "edit_write"
|
"hookMode": "edit_write"
|
||||||
|
|||||||
@@ -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
|
# Attio Frontend Rules
|
||||||
|
|
||||||
Guidelines and conventions for working on the Attio frontend codebase. Use whenever modifying the frontend.
|
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).
|
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
|
||||||
|
<Layout.Stack direction="column" align="flex-start">
|
||||||
|
<Typography.Body.Standard.Component>Title</Typography.Body.Standard.Component>
|
||||||
|
<Typography.Caption.Standard.Component>Description</Typography.Caption.Standard.Component>
|
||||||
|
</Layout.Stack>
|
||||||
|
|
||||||
|
// Bad — text will be centered, not left-aligned
|
||||||
|
<Layout.Stack direction="column">
|
||||||
|
<Typography.Body.Standard.Component>Title</Typography.Body.Standard.Component>
|
||||||
|
<Typography.Caption.Standard.Component>Description</Typography.Caption.Standard.Component>
|
||||||
|
</Layout.Stack>
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
### 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.
|
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.
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -9,8 +9,8 @@
|
|||||||
* The editor is determined by $VISUAL, then $EDITOR, then falls back to 'vi'.
|
* The editor is determined by $VISUAL, then $EDITOR, then falls back to 'vi'.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@mariozechner/pi-coding-agent";
|
||||||
import type { TUI, Theme, KeybindingsManager, Component } from "@mariozechner/pi-tui";
|
import type { TUI, KeybindingsManager, Component } from "@mariozechner/pi-tui";
|
||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
|
|
||||||
export default function editSessionExtension(pi: ExtensionAPI) {
|
export default function editSessionExtension(pi: ExtensionAPI) {
|
||||||
@@ -59,7 +59,7 @@ export default function editSessionExtension(pi: ExtensionAPI) {
|
|||||||
ctx.ui.notify(`Editor exited with code ${result.status}`, "warning");
|
ctx.ui.notify(`Editor exited with code ${result.status}`, "warning");
|
||||||
}
|
}
|
||||||
|
|
||||||
done();
|
done(undefined);
|
||||||
|
|
||||||
// Return dummy component
|
// Return dummy component
|
||||||
return createDummyComponent();
|
return createDummyComponent();
|
||||||
@@ -69,7 +69,7 @@ export default function editSessionExtension(pi: ExtensionAPI) {
|
|||||||
await ctx.ui.custom<void>(factory);
|
await ctx.ui.custom<void>(factory);
|
||||||
|
|
||||||
// Signal that we're about to reload the session (so confirm-destructive skips)
|
// Signal that we're about to reload the session (so confirm-destructive skips)
|
||||||
pi.events.emit("edit-session:reload");
|
pi.events.emit("edit-session:reload", undefined);
|
||||||
|
|
||||||
// Reload the session by switching to the same file (forces re-read from disk)
|
// Reload the session by switching to the same file (forces re-read from disk)
|
||||||
ctx.ui.notify("Reloading session...", "info");
|
ctx.ui.notify("Reloading session...", "info");
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ export default function (pi: ExtensionAPI) {
|
|||||||
loader.onAbort = () => done(null);
|
loader.onAbort = () => done(null);
|
||||||
|
|
||||||
const doGenerate = async () => {
|
const doGenerate = async () => {
|
||||||
const apiKey = await ctx.modelRegistry.getApiKey(ctx.model!);
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model!);
|
||||||
|
if (!auth.ok) throw new Error(auth.error);
|
||||||
|
|
||||||
const userMessage: Message = {
|
const userMessage: Message = {
|
||||||
role: "user",
|
role: "user",
|
||||||
@@ -96,7 +97,7 @@ export default function (pi: ExtensionAPI) {
|
|||||||
const response = await complete(
|
const response = await complete(
|
||||||
ctx.model!,
|
ctx.model!,
|
||||||
{ systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
|
{ systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
|
||||||
{ apiKey, signal: loader.signal },
|
{ apiKey: auth.apiKey, headers: auth.headers, signal: loader.signal },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.stopReason === "aborted") {
|
if (response.stopReason === "aborted") {
|
||||||
|
|||||||
@@ -0,0 +1,535 @@
|
|||||||
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { basename, dirname, join, resolve } from "node:path";
|
||||||
|
import type { ExtensionAPI, ExtensionContext, ToolResultEvent } from "@mariozechner/pi-coding-agent";
|
||||||
|
|
||||||
|
const HOOK_TIMEOUT_MS = 10 * 60 * 1000;
|
||||||
|
|
||||||
|
type HookEventName = "PostToolUse" | "PostToolUseFailure";
|
||||||
|
|
||||||
|
type ResolvedCommandHook = {
|
||||||
|
eventName: HookEventName;
|
||||||
|
matcher?: RegExp;
|
||||||
|
matcherText?: string;
|
||||||
|
command: string;
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type HookState = {
|
||||||
|
projectDir: string;
|
||||||
|
hooks: ResolvedCommandHook[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type CommandRunResult = {
|
||||||
|
code: number;
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
elapsedMs: number;
|
||||||
|
timedOut: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isFile(path: string): boolean {
|
||||||
|
try {
|
||||||
|
return statSync(path).isFile();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
||||||
|
if (typeof value !== "object" || value === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return value as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkUpDirectories(startDir: string, stopDir?: string): string[] {
|
||||||
|
const directories: string[] = [];
|
||||||
|
const hasStopDir = stopDir !== undefined;
|
||||||
|
let current = resolve(startDir);
|
||||||
|
let parent = dirname(current);
|
||||||
|
let reachedStopDir = hasStopDir && current === stopDir;
|
||||||
|
let reachedFilesystemRoot = parent === current;
|
||||||
|
|
||||||
|
directories.push(current);
|
||||||
|
while (!reachedStopDir && !reachedFilesystemRoot) {
|
||||||
|
current = parent;
|
||||||
|
parent = dirname(current);
|
||||||
|
reachedStopDir = hasStopDir && current === stopDir;
|
||||||
|
reachedFilesystemRoot = parent === current;
|
||||||
|
directories.push(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return directories;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNearestGitRoot(startDir: string): string | undefined {
|
||||||
|
for (const directory of walkUpDirectories(startDir)) {
|
||||||
|
if (existsSync(join(directory, ".git"))) {
|
||||||
|
return directory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasHooksConfig(directory: string): boolean {
|
||||||
|
const claudeSettingsPath = join(directory, ".claude", "settings.json");
|
||||||
|
const ruleSyncHooksPath = join(directory, ".rulesync", "hooks.json");
|
||||||
|
const piHooksPath = join(directory, ".pi", "hooks.json");
|
||||||
|
return isFile(claudeSettingsPath) || isFile(ruleSyncHooksPath) || isFile(piHooksPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findProjectDir(cwd: string): string {
|
||||||
|
const gitRoot = findNearestGitRoot(cwd);
|
||||||
|
for (const directory of walkUpDirectories(cwd, gitRoot)) {
|
||||||
|
if (hasHooksConfig(directory)) {
|
||||||
|
return directory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return gitRoot ?? resolve(cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJsonFile(path: string): unknown | undefined {
|
||||||
|
if (!isFile(path)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(readFileSync(path, "utf8")) as unknown;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveHookCommand(command: string, projectDir: string): string {
|
||||||
|
return command.replace(/\$CLAUDE_PROJECT_DIR\b/g, projectDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileMatcher(matcherText: string | undefined): RegExp | undefined {
|
||||||
|
if (matcherText === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new RegExp(matcherText);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createHook(
|
||||||
|
eventName: HookEventName,
|
||||||
|
matcherText: string | undefined,
|
||||||
|
command: string,
|
||||||
|
source: string,
|
||||||
|
projectDir: string,
|
||||||
|
): ResolvedCommandHook | undefined {
|
||||||
|
const matcher = compileMatcher(matcherText);
|
||||||
|
if (matcherText !== undefined && matcher === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventName,
|
||||||
|
matcher,
|
||||||
|
matcherText,
|
||||||
|
command: resolveHookCommand(command, projectDir),
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHookEntries(
|
||||||
|
hooksRecord: Record<string, unknown>,
|
||||||
|
eventName: HookEventName,
|
||||||
|
): unknown[] {
|
||||||
|
const keys =
|
||||||
|
eventName === "PostToolUse"
|
||||||
|
? ["PostToolUse", "postToolUse"]
|
||||||
|
: ["PostToolUseFailure", "postToolUseFailure"];
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = hooksRecord[key];
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseClaudeSettingsHooks(
|
||||||
|
config: unknown,
|
||||||
|
source: string,
|
||||||
|
projectDir: string,
|
||||||
|
): ResolvedCommandHook[] {
|
||||||
|
const root = asRecord(config);
|
||||||
|
const hooksRoot = root ? asRecord(root.hooks) : undefined;
|
||||||
|
if (!hooksRoot) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const hooks: ResolvedCommandHook[] = [];
|
||||||
|
const events: HookEventName[] = ["PostToolUse", "PostToolUseFailure"];
|
||||||
|
|
||||||
|
for (const eventName of events) {
|
||||||
|
const entries = getHookEntries(hooksRoot, eventName);
|
||||||
|
for (const entry of entries) {
|
||||||
|
const entryRecord = asRecord(entry);
|
||||||
|
if (!entryRecord || !Array.isArray(entryRecord.hooks)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matcherText =
|
||||||
|
typeof entryRecord.matcher === "string" ? entryRecord.matcher : undefined;
|
||||||
|
for (const nestedHook of entryRecord.hooks) {
|
||||||
|
const nestedHookRecord = asRecord(nestedHook);
|
||||||
|
if (!nestedHookRecord) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nestedHookRecord.type !== "command") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof nestedHookRecord.command !== "string") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hook = createHook(
|
||||||
|
eventName,
|
||||||
|
matcherText,
|
||||||
|
nestedHookRecord.command,
|
||||||
|
source,
|
||||||
|
projectDir,
|
||||||
|
);
|
||||||
|
if (hook) {
|
||||||
|
hooks.push(hook);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hooks;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSimpleHooksFile(
|
||||||
|
config: unknown,
|
||||||
|
source: string,
|
||||||
|
projectDir: string,
|
||||||
|
): ResolvedCommandHook[] {
|
||||||
|
const root = asRecord(config);
|
||||||
|
const hooksRoot = root ? asRecord(root.hooks) : undefined;
|
||||||
|
if (!hooksRoot) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const hooks: ResolvedCommandHook[] = [];
|
||||||
|
const events: HookEventName[] = ["PostToolUse", "PostToolUseFailure"];
|
||||||
|
|
||||||
|
for (const eventName of events) {
|
||||||
|
const entries = getHookEntries(hooksRoot, eventName);
|
||||||
|
for (const entry of entries) {
|
||||||
|
const entryRecord = asRecord(entry);
|
||||||
|
if (!entryRecord || typeof entryRecord.command !== "string") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matcherText =
|
||||||
|
typeof entryRecord.matcher === "string" ? entryRecord.matcher : undefined;
|
||||||
|
const hook = createHook(
|
||||||
|
eventName,
|
||||||
|
matcherText,
|
||||||
|
entryRecord.command,
|
||||||
|
source,
|
||||||
|
projectDir,
|
||||||
|
);
|
||||||
|
if (hook) {
|
||||||
|
hooks.push(hook);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hooks;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadHooks(cwd: string): HookState {
|
||||||
|
const projectDir = findProjectDir(cwd);
|
||||||
|
const claudeSettingsPath = join(projectDir, ".claude", "settings.json");
|
||||||
|
const ruleSyncHooksPath = join(projectDir, ".rulesync", "hooks.json");
|
||||||
|
const piHooksPath = join(projectDir, ".pi", "hooks.json");
|
||||||
|
|
||||||
|
const hooks: ResolvedCommandHook[] = [];
|
||||||
|
|
||||||
|
const claudeSettings = readJsonFile(claudeSettingsPath);
|
||||||
|
if (claudeSettings !== undefined) {
|
||||||
|
hooks.push(...parseClaudeSettingsHooks(claudeSettings, claudeSettingsPath, projectDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
const ruleSyncHooks = readJsonFile(ruleSyncHooksPath);
|
||||||
|
if (ruleSyncHooks !== undefined) {
|
||||||
|
hooks.push(...parseSimpleHooksFile(ruleSyncHooks, ruleSyncHooksPath, projectDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
const piHooks = readJsonFile(piHooksPath);
|
||||||
|
if (piHooks !== undefined) {
|
||||||
|
hooks.push(...parseSimpleHooksFile(piHooks, piHooksPath, projectDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectDir,
|
||||||
|
hooks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toClaudeToolName(toolName: string): string {
|
||||||
|
if (toolName === "ls") {
|
||||||
|
return "LS";
|
||||||
|
}
|
||||||
|
if (toolName.length === 0) {
|
||||||
|
return toolName;
|
||||||
|
}
|
||||||
|
return toolName[0].toUpperCase() + toolName.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesHook(hook: ResolvedCommandHook, toolName: string): boolean {
|
||||||
|
if (!hook.matcher) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const claudeToolName = toClaudeToolName(toolName);
|
||||||
|
hook.matcher.lastIndex = 0;
|
||||||
|
if (hook.matcher.test(toolName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
hook.matcher.lastIndex = 0;
|
||||||
|
return hook.matcher.test(claudeToolName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTextContent(content: unknown): string {
|
||||||
|
if (!Array.isArray(content)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
for (const item of content) {
|
||||||
|
if (!item || typeof item !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemRecord = item as Record<string, unknown>;
|
||||||
|
if (itemRecord.type === "text" && typeof itemRecord.text === "string") {
|
||||||
|
parts.push(itemRecord.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeToolInput(input: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const normalized: Record<string, unknown> = { ...input };
|
||||||
|
const pathValue = typeof input.path === "string" ? input.path : undefined;
|
||||||
|
|
||||||
|
if (pathValue !== undefined) {
|
||||||
|
normalized.file_path = pathValue;
|
||||||
|
normalized.filePath = pathValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildToolResponse(
|
||||||
|
event: ToolResultEvent,
|
||||||
|
normalizedInput: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const response: Record<string, unknown> = {
|
||||||
|
is_error: event.isError,
|
||||||
|
isError: event.isError,
|
||||||
|
content: event.content,
|
||||||
|
text: extractTextContent(event.content),
|
||||||
|
details: event.details ?? null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const filePath =
|
||||||
|
typeof normalizedInput.file_path === "string" ? normalizedInput.file_path : undefined;
|
||||||
|
if (filePath !== undefined) {
|
||||||
|
response.file_path = filePath;
|
||||||
|
response.filePath = filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHookPayload(
|
||||||
|
event: ToolResultEvent,
|
||||||
|
eventName: HookEventName,
|
||||||
|
ctx: ExtensionContext,
|
||||||
|
projectDir: string,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const normalizedInput = normalizeToolInput(event.input);
|
||||||
|
const sessionId = ctx.sessionManager.getSessionFile() ?? "ephemeral";
|
||||||
|
|
||||||
|
return {
|
||||||
|
session_id: sessionId,
|
||||||
|
cwd: ctx.cwd,
|
||||||
|
claude_project_dir: projectDir,
|
||||||
|
hook_event_name: eventName,
|
||||||
|
tool_name: toClaudeToolName(event.toolName),
|
||||||
|
tool_call_id: event.toolCallId,
|
||||||
|
tool_input: normalizedInput,
|
||||||
|
tool_response: buildToolResponse(event, normalizedInput),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCommandHook(
|
||||||
|
command: string,
|
||||||
|
cwd: string,
|
||||||
|
payload: Record<string, unknown>,
|
||||||
|
): Promise<CommandRunResult> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const child = spawn("bash", ["-lc", command], {
|
||||||
|
cwd,
|
||||||
|
env: { ...process.env, CLAUDE_PROJECT_DIR: cwd },
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
let timedOut = false;
|
||||||
|
let resolved = false;
|
||||||
|
|
||||||
|
const finish = (code: number) => {
|
||||||
|
if (resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolved = true;
|
||||||
|
resolve({
|
||||||
|
code,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
elapsedMs: Date.now() - startedAt,
|
||||||
|
timedOut,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
timedOut = true;
|
||||||
|
child.kill("SIGTERM");
|
||||||
|
|
||||||
|
const killTimer = setTimeout(() => {
|
||||||
|
child.kill("SIGKILL");
|
||||||
|
}, 1000);
|
||||||
|
(killTimer as NodeJS.Timeout & { unref?: () => void }).unref?.();
|
||||||
|
}, HOOK_TIMEOUT_MS);
|
||||||
|
(timeout as NodeJS.Timeout & { unref?: () => void }).unref?.();
|
||||||
|
|
||||||
|
child.stdout.on("data", (chunk: Buffer) => {
|
||||||
|
stdout += chunk.toString("utf8");
|
||||||
|
});
|
||||||
|
child.stderr.on("data", (chunk: Buffer) => {
|
||||||
|
stderr += chunk.toString("utf8");
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("error", (error) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
stderr += `${error.message}\n`;
|
||||||
|
finish(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("close", (code) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
finish(code ?? -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
child.stdin.write(JSON.stringify(payload));
|
||||||
|
child.stdin.end();
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
stderr += `${message}\n`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hookEventNameForResult(event: ToolResultEvent): HookEventName {
|
||||||
|
return event.isError ? "PostToolUseFailure" : "PostToolUse";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(elapsedMs: number): string {
|
||||||
|
if (elapsedMs < 1000) {
|
||||||
|
return `${elapsedMs}ms`;
|
||||||
|
}
|
||||||
|
return `${(elapsedMs / 1000).toFixed(1)}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hookName(command: string): string {
|
||||||
|
const shPathMatch = command.match(/[^\s|;&]+\.sh\b/);
|
||||||
|
if (shPathMatch) {
|
||||||
|
return basename(shPathMatch[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstToken = command.trim().split(/\s+/)[0] ?? "hook";
|
||||||
|
return basename(firstToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function(pi: ExtensionAPI) {
|
||||||
|
let state: HookState = {
|
||||||
|
projectDir: process.cwd(),
|
||||||
|
hooks: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshHooks = (cwd: string) => {
|
||||||
|
state = loadHooks(cwd);
|
||||||
|
};
|
||||||
|
|
||||||
|
pi.on("session_start", (_event, ctx) => {
|
||||||
|
refreshHooks(ctx.cwd);
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("session_switch", (_event, ctx) => {
|
||||||
|
refreshHooks(ctx.cwd);
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("tool_result", async (event, ctx) => {
|
||||||
|
if (state.hooks.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventName = hookEventNameForResult(event);
|
||||||
|
const matchingHooks = state.hooks.filter(
|
||||||
|
(hook) => hook.eventName === eventName && matchesHook(hook, event.toolName),
|
||||||
|
);
|
||||||
|
if (matchingHooks.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = buildHookPayload(event, eventName, ctx, state.projectDir);
|
||||||
|
const executedCommands = new Set<string>();
|
||||||
|
|
||||||
|
for (const hook of matchingHooks) {
|
||||||
|
if (executedCommands.has(hook.command)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
executedCommands.add(hook.command);
|
||||||
|
|
||||||
|
const result = await runCommandHook(hook.command, state.projectDir, payload);
|
||||||
|
const name = hookName(hook.command);
|
||||||
|
const duration = formatDuration(result.elapsedMs);
|
||||||
|
|
||||||
|
if (result.code === 0) {
|
||||||
|
ctx.ui.notify(` Hook \`${name}\` executed, took ${duration}`, "info");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matcherLabel = hook.matcherText ?? "*";
|
||||||
|
const errorLine =
|
||||||
|
result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
|
||||||
|
ctx.ui.notify(
|
||||||
|
` Hook \`${name}\` failed after ${duration} (${matcherLabel}) from ${hook.source}: ${errorLine}`,
|
||||||
|
"warning",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -13,10 +13,11 @@
|
|||||||
"vscode-languageserver-protocol": "^3.17.5"
|
"vscode-languageserver-protocol": "^3.17.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@mariozechner/pi-ai": "^0.56.3",
|
"@mariozechner/pi-ai": "^0.63.1",
|
||||||
"@mariozechner/pi-coding-agent": "^0.56.3",
|
"@mariozechner/pi-coding-agent": "^0.63.1",
|
||||||
"@mariozechner/pi-tui": "^0.56.3",
|
"@mariozechner/pi-tui": "^0.63.1",
|
||||||
"@types/node": "^25.3.3",
|
"@types/node": "^25.3.3",
|
||||||
|
"@types/turndown": "^5.0.6",
|
||||||
"typescript": "^5.7.0"
|
"typescript": "^5.7.0"
|
||||||
},
|
},
|
||||||
"pi": {},
|
"pi": {},
|
||||||
|
|||||||
@@ -211,12 +211,12 @@ function updateWidget(ctx: ExtensionContext): void {
|
|||||||
(resetMs > 0 ? theme.fg("dim", ` (resets in ${resetSec}s)`) : ""),
|
(resetMs > 0 ? theme.fg("dim", ` (resets in ${resetSec}s)`) : ""),
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.ui.setWidget("web-activity", new Text(lines.join("\n"), 0, 0));
|
ctx.ui.setWidget("web-activity", lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatEntryLine(
|
function formatEntryLine(
|
||||||
entry: ActivityEntry,
|
entry: ActivityEntry,
|
||||||
theme: { fg: (color: string, text: string) => string },
|
theme: ExtensionContext["ui"]["theme"],
|
||||||
): string {
|
): string {
|
||||||
const typeStr = entry.type === "api" ? "API" : "GET";
|
const typeStr = entry.type === "api" ? "API" : "GET";
|
||||||
const target =
|
const target =
|
||||||
@@ -550,7 +550,7 @@ export default function (pi: ExtensionAPI) {
|
|||||||
} else {
|
} else {
|
||||||
widgetUnsubscribe?.();
|
widgetUnsubscribe?.();
|
||||||
widgetUnsubscribe = null;
|
widgetUnsubscribe = null;
|
||||||
ctx.ui.setWidget("web-activity", null);
|
ctx.ui.setWidget("web-activity", undefined);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -598,7 +598,7 @@ export default function (pi: ExtensionAPI) {
|
|||||||
})),
|
})),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
async execute(_toolCallId, params, signal, onUpdate, ctx): Promise<any> {
|
||||||
const queryList = params.queries ?? (params.query ? [params.query] : []);
|
const queryList = params.queries ?? (params.query ? [params.query] : []);
|
||||||
const isMultiQuery = queryList.length > 1;
|
const isMultiQuery = queryList.length > 1;
|
||||||
const shouldCurate = params.curate !== false && ctx?.hasUI !== false;
|
const shouldCurate = params.curate !== false && ctx?.hasUI !== false;
|
||||||
@@ -613,7 +613,10 @@ export default function (pi: ExtensionAPI) {
|
|||||||
if (shouldCurate) {
|
if (shouldCurate) {
|
||||||
closeCurator();
|
closeCurator();
|
||||||
|
|
||||||
const { promise, resolve: resolvePromise } = Promise.withResolvers<unknown>();
|
let resolvePromise!: (value: unknown) => void;
|
||||||
|
const promise = new Promise<unknown>((resolve) => {
|
||||||
|
resolvePromise = resolve;
|
||||||
|
});
|
||||||
const includeContent = params.includeContent ?? false;
|
const includeContent = params.includeContent ?? false;
|
||||||
const searchResults = new Map<number, QueryResultData>();
|
const searchResults = new Map<number, QueryResultData>();
|
||||||
const allUrls: string[] = [];
|
const allUrls: string[] = [];
|
||||||
@@ -637,7 +640,7 @@ export default function (pi: ExtensionAPI) {
|
|||||||
queryList,
|
queryList,
|
||||||
includeContent,
|
includeContent,
|
||||||
numResults: params.numResults,
|
numResults: params.numResults,
|
||||||
recencyFilter: params.recencyFilter,
|
recencyFilter: params.recencyFilter as "day" | "week" | "month" | "year" | undefined,
|
||||||
domainFilter: params.domainFilter,
|
domainFilter: params.domainFilter,
|
||||||
availableProviders,
|
availableProviders,
|
||||||
defaultProvider,
|
defaultProvider,
|
||||||
@@ -684,7 +687,7 @@ export default function (pi: ExtensionAPI) {
|
|||||||
const { answer, results } = await search(queryList[qi], {
|
const { answer, results } = await search(queryList[qi], {
|
||||||
provider: defaultProvider as SearchProvider | undefined,
|
provider: defaultProvider as SearchProvider | undefined,
|
||||||
numResults: params.numResults,
|
numResults: params.numResults,
|
||||||
recencyFilter: params.recencyFilter,
|
recencyFilter: params.recencyFilter as "day" | "week" | "month" | "year" | undefined,
|
||||||
domainFilter: params.domainFilter,
|
domainFilter: params.domainFilter,
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
@@ -754,7 +757,7 @@ export default function (pi: ExtensionAPI) {
|
|||||||
text = `${searchResults.size} searches (${totalSources} sources) · ${curateLabel} to review · sending in ${remaining}s`;
|
text = `${searchResults.size} searches (${totalSources} sources) · ${curateLabel} to review · sending in ${remaining}s`;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text }],
|
content: [{ type: "text" as const, text }],
|
||||||
details: {
|
details: {
|
||||||
phase: "curate-window",
|
phase: "curate-window",
|
||||||
searchCount: searchResults.size,
|
searchCount: searchResults.size,
|
||||||
@@ -824,7 +827,7 @@ export default function (pi: ExtensionAPI) {
|
|||||||
const { answer, results } = await search(query, {
|
const { answer, results } = await search(query, {
|
||||||
provider: resolvedProvider as SearchProvider | undefined,
|
provider: resolvedProvider as SearchProvider | undefined,
|
||||||
numResults: params.numResults,
|
numResults: params.numResults,
|
||||||
recencyFilter: params.recencyFilter,
|
recencyFilter: params.recencyFilter as "day" | "week" | "month" | "year" | undefined,
|
||||||
domainFilter: params.domainFilter,
|
domainFilter: params.domainFilter,
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
@@ -1117,7 +1120,10 @@ export default function (pi: ExtensionAPI) {
|
|||||||
`Use get_search_content({ responseId: "${responseId}", urlIndex: 0 }) for full content.`;
|
`Use get_search_content({ responseId: "${responseId}", urlIndex: 0 }) for full content.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content: Array<{ type: string; text?: string; data?: string; mimeType?: string }> = [];
|
const content: Array<
|
||||||
|
| { type: "image"; data: string; mimeType: string }
|
||||||
|
| { type: "text"; text: string }
|
||||||
|
> = [];
|
||||||
if (result.frames?.length) {
|
if (result.frames?.length) {
|
||||||
for (const frame of result.frames) {
|
for (const frame of result.frames) {
|
||||||
content.push({ type: "image", data: frame.data, mimeType: frame.mimeType });
|
content.push({ type: "image", data: frame.data, mimeType: frame.mimeType });
|
||||||
@@ -1290,7 +1296,7 @@ export default function (pi: ExtensionAPI) {
|
|||||||
urlIndex: Type.Optional(Type.Number({ description: "Get content for URL at index" })),
|
urlIndex: Type.Optional(Type.Number({ description: "Get content for URL at index" })),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
async execute(_toolCallId, params) {
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx): Promise<any> {
|
||||||
const data = getResult(params.responseId);
|
const data = getResult(params.responseId);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return {
|
return {
|
||||||
@@ -1477,7 +1483,7 @@ export default function (pi: ExtensionAPI) {
|
|||||||
pi.sendMessage({
|
pi.sendMessage({
|
||||||
customType: "web-search-results",
|
customType: "web-search-results",
|
||||||
content: [{ type: "text", text }],
|
content: [{ type: "text", text }],
|
||||||
display: "tool",
|
display: true,
|
||||||
details: { queryCount: results.length, totalResults: urls.length },
|
details: { queryCount: results.length, totalResults: urls.length },
|
||||||
}, { triggerTurn: true, deliverAs: "followUp" });
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,9 +42,10 @@ export async function extractPDFToMarkdown(
|
|||||||
|
|
||||||
const pdf = await getDocumentProxy(new Uint8Array(buffer));
|
const pdf = await getDocumentProxy(new Uint8Array(buffer));
|
||||||
const metadata = await pdf.getMetadata();
|
const metadata = await pdf.getMetadata();
|
||||||
|
const info = (metadata.info ?? {}) as Record<string, unknown>;
|
||||||
|
|
||||||
// Extract title from metadata or URL
|
// Extract title from metadata or URL
|
||||||
const metaTitle = metadata.info?.Title as string | undefined;
|
const metaTitle = typeof info.Title === "string" ? info.Title : undefined;
|
||||||
const urlTitle = extractTitleFromURL(url);
|
const urlTitle = extractTitleFromURL(url);
|
||||||
const title = metaTitle?.trim() || urlTitle;
|
const title = metaTitle?.trim() || urlTitle;
|
||||||
|
|
||||||
@@ -79,8 +80,9 @@ export async function extractPDFToMarkdown(
|
|||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push(`> Source: ${url}`);
|
lines.push(`> Source: ${url}`);
|
||||||
lines.push(`> Pages: ${pdf.numPages}${truncated ? ` (extracted first ${pagesToExtract})` : ""}`);
|
lines.push(`> Pages: ${pdf.numPages}${truncated ? ` (extracted first ${pagesToExtract})` : ""}`);
|
||||||
if (metadata.info?.Author) {
|
const author = typeof info.Author === "string" ? info.Author : undefined;
|
||||||
lines.push(`> Author: ${metadata.info.Author}`);
|
if (author) {
|
||||||
|
lines.push(`> Author: ${author}`);
|
||||||
}
|
}
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("---");
|
lines.push("---");
|
||||||
|
|||||||
@@ -245,8 +245,8 @@ export async function condenseSearchResults(
|
|||||||
const model = ctx.modelRegistry.find(provider, modelId);
|
const model = ctx.modelRegistry.find(provider, modelId);
|
||||||
if (!model) return null;
|
if (!model) return null;
|
||||||
|
|
||||||
const apiKey = await ctx.modelRegistry.getApiKey(model);
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
||||||
if (!apiKey) return null;
|
if (!auth.ok) return null;
|
||||||
|
|
||||||
const queryData = [...results.entries()]
|
const queryData = [...results.entries()]
|
||||||
.sort((a, b) => a[0] - b[0])
|
.sort((a, b) => a[0] - b[0])
|
||||||
@@ -281,7 +281,8 @@ export async function condenseSearchResults(
|
|||||||
: timeoutSignal;
|
: timeoutSignal;
|
||||||
|
|
||||||
const response = await complete(model, aiContext, {
|
const response = await complete(model, aiContext, {
|
||||||
apiKey,
|
apiKey: auth.apiKey,
|
||||||
|
headers: auth.headers,
|
||||||
signal: combinedSignal,
|
signal: combinedSignal,
|
||||||
max_tokens: MAX_TOKENS,
|
max_tokens: MAX_TOKENS,
|
||||||
} as any);
|
} as any);
|
||||||
|
|||||||
Generated
+31
-22
@@ -34,17 +34,20 @@ importers:
|
|||||||
version: 3.17.5
|
version: 3.17.5
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@mariozechner/pi-ai':
|
'@mariozechner/pi-ai':
|
||||||
specifier: ^0.56.3
|
specifier: ^0.63.1
|
||||||
version: 0.56.3(ws@8.19.0)(zod@4.3.6)
|
version: 0.63.1(ws@8.19.0)(zod@4.3.6)
|
||||||
'@mariozechner/pi-coding-agent':
|
'@mariozechner/pi-coding-agent':
|
||||||
specifier: ^0.56.3
|
specifier: ^0.63.1
|
||||||
version: 0.56.3(ws@8.19.0)(zod@4.3.6)
|
version: 0.63.1(ws@8.19.0)(zod@4.3.6)
|
||||||
'@mariozechner/pi-tui':
|
'@mariozechner/pi-tui':
|
||||||
specifier: ^0.56.3
|
specifier: ^0.63.1
|
||||||
version: 0.56.3
|
version: 0.63.1
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^25.3.3
|
specifier: ^25.3.3
|
||||||
version: 25.3.3
|
version: 25.3.3
|
||||||
|
'@types/turndown':
|
||||||
|
specifier: ^5.0.6
|
||||||
|
version: 5.0.6
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.7.0
|
specifier: ^5.7.0
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -289,22 +292,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==}
|
resolution: {integrity: sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@mariozechner/pi-agent-core@0.56.3':
|
'@mariozechner/pi-agent-core@0.63.1':
|
||||||
resolution: {integrity: sha512-TsI1zENf3wqqKPaERnj486Q4i6Y/y6lAZipLNcfDYUDxDrLwNfQ9EW9xukkbJfTZ8zjG3VZ2pBZe3C7wM51dVQ==}
|
resolution: {integrity: sha512-h0B20xfs/iEVR2EC4gwiE8hKI1TPeB8REdRJMgV+uXKH7gpeIZ9+s8Dp9nX35ZR0QUjkNey2+ULk2DxQtdg14Q==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@mariozechner/pi-ai@0.56.3':
|
'@mariozechner/pi-ai@0.63.1':
|
||||||
resolution: {integrity: sha512-l4J+cVyVeBLAlGOY/osGDvsbTz0DySCQmR171G6SdbPvIeLGhIi6siZ+zHwq91GJYjv/wtu/08M08ag2mGZKeA==}
|
resolution: {integrity: sha512-wjgwY+yfrFO6a9QdAfjWpH7iSrDean6GsKDDMohNcLCy6PreMxHOZvNM0NwJARL1tZoZovr7ikAQfLGFZbnjsw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@mariozechner/pi-coding-agent@0.56.3':
|
'@mariozechner/pi-coding-agent@0.63.1':
|
||||||
resolution: {integrity: sha512-yHgnadye+TT/4NWKBirZUjw/LWdNWTa7M4HJdX2RxRbwuj4q7RZ0Aqy+lQbOHEPDQYhxK3kZb9hjiAbbGficZQ==}
|
resolution: {integrity: sha512-XSoMyLtuMA7ePK1UBWqSJ/BBdtBdJUHY9nbtnNyG6GeW7Gbgd+iqljIuwmAUf8wlYL981UIfYM/WIPQ6t/dIxw==}
|
||||||
engines: {node: '>=20.6.0'}
|
engines: {node: '>=20.6.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@mariozechner/pi-tui@0.56.3':
|
'@mariozechner/pi-tui@0.63.1':
|
||||||
resolution: {integrity: sha512-eZ1P9QRKHp78hwx+lITr/mujZqe+eCwL/bOS9vXXkFP070RW4VYum0j7TJ4BrFEH/nNkXRS1tYCXYU05une1bA==}
|
resolution: {integrity: sha512-G5p+eh1EPkFCNaaggX6vRrqttnDscK6npgmEOknoCQXZtch8XNgh9Lf3VJ0A2lZXSgR7IntG5dfXHPH/Ki64wA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@mistralai/mistralai@1.14.1':
|
'@mistralai/mistralai@1.14.1':
|
||||||
@@ -568,6 +571,9 @@ packages:
|
|||||||
'@types/retry@0.12.0':
|
'@types/retry@0.12.0':
|
||||||
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
|
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
|
||||||
|
|
||||||
|
'@types/turndown@5.0.6':
|
||||||
|
resolution: {integrity: sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==}
|
||||||
|
|
||||||
'@types/yauzl@2.10.3':
|
'@types/yauzl@2.10.3':
|
||||||
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
|
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
|
||||||
|
|
||||||
@@ -1722,9 +1728,9 @@ snapshots:
|
|||||||
std-env: 3.10.0
|
std-env: 3.10.0
|
||||||
yoctocolors: 2.1.2
|
yoctocolors: 2.1.2
|
||||||
|
|
||||||
'@mariozechner/pi-agent-core@0.56.3(ws@8.19.0)(zod@4.3.6)':
|
'@mariozechner/pi-agent-core@0.63.1(ws@8.19.0)(zod@4.3.6)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mariozechner/pi-ai': 0.56.3(ws@8.19.0)(zod@4.3.6)
|
'@mariozechner/pi-ai': 0.63.1(ws@8.19.0)(zod@4.3.6)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@modelcontextprotocol/sdk'
|
- '@modelcontextprotocol/sdk'
|
||||||
- aws-crt
|
- aws-crt
|
||||||
@@ -1734,7 +1740,7 @@ snapshots:
|
|||||||
- ws
|
- ws
|
||||||
- zod
|
- zod
|
||||||
|
|
||||||
'@mariozechner/pi-ai@0.56.3(ws@8.19.0)(zod@4.3.6)':
|
'@mariozechner/pi-ai@0.63.1(ws@8.19.0)(zod@4.3.6)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@anthropic-ai/sdk': 0.73.0(zod@4.3.6)
|
'@anthropic-ai/sdk': 0.73.0(zod@4.3.6)
|
||||||
'@aws-sdk/client-bedrock-runtime': 3.1002.0
|
'@aws-sdk/client-bedrock-runtime': 3.1002.0
|
||||||
@@ -1758,13 +1764,14 @@ snapshots:
|
|||||||
- ws
|
- ws
|
||||||
- zod
|
- zod
|
||||||
|
|
||||||
'@mariozechner/pi-coding-agent@0.56.3(ws@8.19.0)(zod@4.3.6)':
|
'@mariozechner/pi-coding-agent@0.63.1(ws@8.19.0)(zod@4.3.6)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mariozechner/jiti': 2.6.5
|
'@mariozechner/jiti': 2.6.5
|
||||||
'@mariozechner/pi-agent-core': 0.56.3(ws@8.19.0)(zod@4.3.6)
|
'@mariozechner/pi-agent-core': 0.63.1(ws@8.19.0)(zod@4.3.6)
|
||||||
'@mariozechner/pi-ai': 0.56.3(ws@8.19.0)(zod@4.3.6)
|
'@mariozechner/pi-ai': 0.63.1(ws@8.19.0)(zod@4.3.6)
|
||||||
'@mariozechner/pi-tui': 0.56.3
|
'@mariozechner/pi-tui': 0.63.1
|
||||||
'@silvia-odwyer/photon-node': 0.3.4
|
'@silvia-odwyer/photon-node': 0.3.4
|
||||||
|
ajv: 8.18.0
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
cli-highlight: 2.1.11
|
cli-highlight: 2.1.11
|
||||||
diff: 8.0.3
|
diff: 8.0.3
|
||||||
@@ -1790,7 +1797,7 @@ snapshots:
|
|||||||
- ws
|
- ws
|
||||||
- zod
|
- zod
|
||||||
|
|
||||||
'@mariozechner/pi-tui@0.56.3':
|
'@mariozechner/pi-tui@0.63.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mime-types': 2.1.4
|
'@types/mime-types': 2.1.4
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
@@ -2166,6 +2173,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/retry@0.12.0': {}
|
'@types/retry@0.12.0': {}
|
||||||
|
|
||||||
|
'@types/turndown@5.0.6': {}
|
||||||
|
|
||||||
'@types/yauzl@2.10.3':
|
'@types/yauzl@2.10.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.3.3
|
'@types/node': 25.3.3
|
||||||
|
|||||||
@@ -135,11 +135,11 @@ export default function(pi: ExtensionAPI) {
|
|||||||
|
|
||||||
// Fire-and-forget: run auto-naming in background without blocking
|
// Fire-and-forget: run auto-naming in background without blocking
|
||||||
const doAutoName = async () => {
|
const doAutoName = async () => {
|
||||||
const apiKey = await ctx.modelRegistry.getApiKey(AUTO_NAME_MODEL);
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(AUTO_NAME_MODEL);
|
||||||
log(`Got API key: ${apiKey ? "yes" : "no"}`);
|
log(`Got API key: ${auth.ok ? "yes" : "no"}`);
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!auth.ok) {
|
||||||
log("No API key available, aborting");
|
log(`No API key available, aborting: ${auth.error}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ export default function(pi: ExtensionAPI) {
|
|||||||
const response = await complete(
|
const response = await complete(
|
||||||
AUTO_NAME_MODEL,
|
AUTO_NAME_MODEL,
|
||||||
{ systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
|
{ systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
|
||||||
{ apiKey },
|
{ apiKey: auth.apiKey, headers: auth.headers },
|
||||||
);
|
);
|
||||||
|
|
||||||
log(`Response received, stopReason: ${response.stopReason}`);
|
log(`Response received, stopReason: ${response.stopReason}`);
|
||||||
@@ -273,7 +273,8 @@ export default function(pi: ExtensionAPI) {
|
|||||||
loader.onAbort = () => done(null);
|
loader.onAbort = () => done(null);
|
||||||
|
|
||||||
const doGenerate = async () => {
|
const doGenerate = async () => {
|
||||||
const apiKey = await ctx.modelRegistry.getApiKey(AUTO_NAME_MODEL);
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(AUTO_NAME_MODEL);
|
||||||
|
if (!auth.ok) throw new Error(auth.error);
|
||||||
|
|
||||||
const userMessage: Message = {
|
const userMessage: Message = {
|
||||||
role: "user",
|
role: "user",
|
||||||
@@ -289,7 +290,7 @@ export default function(pi: ExtensionAPI) {
|
|||||||
const response = await complete(
|
const response = await complete(
|
||||||
AUTO_NAME_MODEL,
|
AUTO_NAME_MODEL,
|
||||||
{ systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
|
{ systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
|
||||||
{ apiKey, signal: loader.signal },
|
{ apiKey: auth.apiKey, headers: auth.headers, signal: loader.signal },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.stopReason === "aborted") {
|
if (response.stopReason === "aborted") {
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { existsSync, statSync } from "node:fs";
|
||||||
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
|
|
||||||
|
function isDirectory(path: string): boolean {
|
||||||
|
try {
|
||||||
|
return statSync(path).isDirectory();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkUpDirectories(startDir: string, stopDir?: string): string[] {
|
||||||
|
const directories: string[] = [];
|
||||||
|
const hasStopDir = stopDir !== undefined;
|
||||||
|
|
||||||
|
let current = resolve(startDir);
|
||||||
|
let parent = dirname(current);
|
||||||
|
let reachedStopDir = hasStopDir && current === stopDir;
|
||||||
|
let reachedFilesystemRoot = parent === current;
|
||||||
|
|
||||||
|
directories.push(current);
|
||||||
|
while (!reachedStopDir && !reachedFilesystemRoot) {
|
||||||
|
current = parent;
|
||||||
|
parent = dirname(current);
|
||||||
|
reachedStopDir = hasStopDir && current === stopDir;
|
||||||
|
reachedFilesystemRoot = parent === current;
|
||||||
|
directories.push(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return directories;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNearestGitRoot(startDir: string): string | undefined {
|
||||||
|
for (const directory of walkUpDirectories(startDir)) {
|
||||||
|
if (existsSync(join(directory, ".git"))) {
|
||||||
|
return directory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findClaudeSkillDirs(cwd: string): string[] {
|
||||||
|
const gitRoot = findNearestGitRoot(cwd);
|
||||||
|
|
||||||
|
return walkUpDirectories(cwd, gitRoot)
|
||||||
|
.map((directory) => join(directory, ".claude", "skills"))
|
||||||
|
.filter(isDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function(pi: ExtensionAPI) {
|
||||||
|
pi.on("resources_discover", (event) => {
|
||||||
|
const skillPaths = findClaudeSkillDirs(event.cwd);
|
||||||
|
if (skillPaths.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { skillPaths };
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
* - Injects timestamp markers without triggering extra turns
|
* - Injects timestamp markers without triggering extra turns
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
||||||
import { Box, Text } from "@mariozechner/pi-tui";
|
import { Box, Text } from "@mariozechner/pi-tui";
|
||||||
|
|
||||||
// Track session time
|
// Track session time
|
||||||
@@ -41,12 +41,7 @@ function formatDuration(ms: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
export default function (pi: ExtensionAPI) {
|
||||||
const updateStatus = (ctx: {
|
const updateStatus = (ctx: ExtensionContext) => {
|
||||||
ui: {
|
|
||||||
setStatus: (id: string, text: string | undefined) => void;
|
|
||||||
theme: { fg: (color: string, text: string) => string };
|
|
||||||
};
|
|
||||||
}) => {
|
|
||||||
const elapsed = Date.now() - sessionStart;
|
const elapsed = Date.now() - sessionStart;
|
||||||
let status = ctx.ui.theme.fg("dim", `⏱ ${formatElapsed(elapsed)}`);
|
let status = ctx.ui.theme.fg("dim", `⏱ ${formatElapsed(elapsed)}`);
|
||||||
if (lastTurnDuration !== null) {
|
if (lastTurnDuration !== null) {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"selectModel": "ctrl+space"
|
"app.model.select": "ctrl+space"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
Executable
+23
@@ -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;
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// Add a comment to a Linear issue
|
||||||
|
// Usage: linear-comment.js <identifier> <body>
|
||||||
|
|
||||||
|
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 <identifier> <body>");
|
||||||
|
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}.`);
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// Create a new Linear issue
|
||||||
|
// Usage: linear-create.js --team <key> --title <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}`);
|
||||||
Executable
+87
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
+90
@@ -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.`);
|
||||||
Executable
+33
@@ -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)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -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("");
|
||||||
|
}
|
||||||
+67
@@ -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.");
|
||||||
|
}
|
||||||
Executable
+15
@@ -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}`);
|
||||||
|
}
|
||||||
+93
@@ -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}`);
|
||||||
+107
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,22 @@
|
|||||||
layout {
|
layout {
|
||||||
default_tab_template {
|
default_tab_template {
|
||||||
children
|
children
|
||||||
pane size=1 borderless=true {
|
pane size=1 borderless=true {
|
||||||
plugin location="zellij:compact-bar"
|
plugin location="zellij:compact-bar"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tab name="dotfiles" cwd="/home/thomasgl/.dotfiles" {
|
tab name="nvim + jjui" {
|
||||||
pane split_direction="vertical" {
|
pane stacked=true {
|
||||||
pane stacked=true {
|
pane command="nvim"
|
||||||
pane
|
pane command="jjui"
|
||||||
pane command="nvim"
|
}
|
||||||
}
|
}
|
||||||
pane size="40%" command="pi"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tab name="NixOS" cwd="/home/thomasgl/etc/nixos" {
|
tab name="pi + shell" {
|
||||||
pane split_direction="vertical" {
|
pane stacked=true {
|
||||||
pane stacked=true {
|
pane command="pi"
|
||||||
pane
|
pane
|
||||||
pane command="nvim"
|
}
|
||||||
}
|
}
|
||||||
pane size="40%" command="pi"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user