refactor(extensions): simplify lib helpers, tool registration and kit-doctor
Behavior-preserving cleanups: - extract shared resolveBaseUrl helper for jira/confluence base-URL logic - extract shared registerTool helper; port jira/gitlab/confluence tools to it - make scanSecrets declarative (filter/map) - collapse ok/warn pair into status() and drop double existsSync in kit-doctor Typecheck clean; npm test 71 PASS, 0 FAIL.
This commit is contained in:
@@ -6,31 +6,16 @@
|
||||
* DISABLE: remove/rename this file from extensions/.
|
||||
*/
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { registerTool, type ToolSpec } from "./lib/tools.ts";
|
||||
import { search, pageGet, pageCreate } from "./lib/confluence.ts";
|
||||
|
||||
export default function confluenceToolsExtension(pi: ExtensionAPI) {
|
||||
const register = (name: string, description: string, parameters: Record<string, unknown>, run: (p: any) => Promise<unknown>) => {
|
||||
pi.registerTool({
|
||||
name,
|
||||
label: name,
|
||||
description,
|
||||
parameters: parameters as any,
|
||||
async execute(_id: string, params: unknown) {
|
||||
try {
|
||||
const out = await run(params ?? {});
|
||||
return { content: [{ type: "text", text: JSON.stringify(out ?? {}, null, 2) }], details: {} };
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
return { content: [{ type: "text", text: `confluence error: ${msg}` }], isError: true, details: {} };
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
register(
|
||||
"confluence_search",
|
||||
"Найти страницы в Confluence по CQL-запросу (текст/заголовок/пространство).",
|
||||
const tools: ToolSpec[] = [
|
||||
{
|
||||
name: "confluence_search",
|
||||
description: "Найти страницы в Confluence по CQL-запросу (текст/заголовок/пространство).",
|
||||
errorPrefix: "confluence",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
cql: { type: "string", description: 'CQL, например text="деплой" AND space = TECHRUN' },
|
||||
@@ -39,13 +24,13 @@ export default function confluenceToolsExtension(pi: ExtensionAPI) {
|
||||
},
|
||||
required: ["cql"],
|
||||
},
|
||||
async (p) => search(p.cql, { limit: p.limit, expandBody: p.expandBody }),
|
||||
);
|
||||
|
||||
register(
|
||||
"confluence_page_get",
|
||||
"Получить страницу Confluence по ID (с содержимым, если expandBody).",
|
||||
run: async (p) => search(p.cql, { limit: p.limit, expandBody: p.expandBody }),
|
||||
},
|
||||
{
|
||||
name: "confluence_page_get",
|
||||
description: "Получить страницу Confluence по ID (с содержимым, если expandBody).",
|
||||
errorPrefix: "confluence",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "ID страницы" },
|
||||
@@ -53,13 +38,13 @@ export default function confluenceToolsExtension(pi: ExtensionAPI) {
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
async (p) => pageGet(p.id, { expandBody: p.expandBody }),
|
||||
);
|
||||
|
||||
register(
|
||||
"confluence_page_create",
|
||||
"Создать страницу в Confluence. body — HTML в Storage format.",
|
||||
run: async (p) => pageGet(p.id, { expandBody: p.expandBody }),
|
||||
},
|
||||
{
|
||||
name: "confluence_page_create",
|
||||
description: "Создать страницу в Confluence. body — HTML в Storage format.",
|
||||
errorPrefix: "confluence",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
spaceKey: { type: "string", description: "Ключ пространства" },
|
||||
@@ -69,6 +54,9 @@ export default function confluenceToolsExtension(pi: ExtensionAPI) {
|
||||
},
|
||||
required: ["spaceKey", "title", "body"],
|
||||
},
|
||||
async (p) => pageCreate(p),
|
||||
);
|
||||
run: async (p) => pageCreate(p),
|
||||
},
|
||||
];
|
||||
|
||||
for (const t of tools) registerTool(pi, t);
|
||||
}
|
||||
|
||||
+32
-36
@@ -6,31 +6,16 @@
|
||||
* DISABLE: remove/rename this file from extensions/.
|
||||
*/
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { mrCreate, pipelineStatus, codeSearch, deriveProjectFromOrigin } from "./lib/gitlab.ts";
|
||||
import { registerTool, type ToolSpec } from "./lib/tools.ts";
|
||||
import { mrCreate, pipelineStatus, codeSearch } from "./lib/gitlab.ts";
|
||||
|
||||
export default function gitlabToolsExtension(pi: ExtensionAPI) {
|
||||
const register = (name: string, description: string, parameters: Record<string, unknown>, run: (p: any) => Promise<unknown>) => {
|
||||
pi.registerTool({
|
||||
name,
|
||||
label: name,
|
||||
description,
|
||||
parameters: parameters as any,
|
||||
async execute(_id: string, params: unknown) {
|
||||
try {
|
||||
const out = await run(params ?? {});
|
||||
return { content: [{ type: "text", text: JSON.stringify(out ?? {}, null, 2) }], details: {} };
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
return { content: [{ type: "text", text: `gitlab error: ${msg}` }], isError: true, details: {} };
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
register(
|
||||
"gitlab_mr_create",
|
||||
"Создать merge request в GitLab. Проект определяется из git remote origin (или укажи project).",
|
||||
const tools: ToolSpec[] = [
|
||||
{
|
||||
name: "gitlab_mr_create",
|
||||
description: "Создать merge request в GitLab. Проект определяется из git remote origin (или укажи project).",
|
||||
errorPrefix: "gitlab",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
title: { type: "string", description: "Заголовок MR" },
|
||||
@@ -42,13 +27,21 @@ export default function gitlabToolsExtension(pi: ExtensionAPI) {
|
||||
},
|
||||
required: ["title"],
|
||||
},
|
||||
async (p) => mrCreate({ title: p.title, sourceBranch: p.sourceBranch || undefined, targetBranch: p.targetBranch, project: p.project, removeSourceBranch: p.removeSourceBranch, mergeWhenPipelineSucceeds: p.mergeWhenPipelineSucceeds }),
|
||||
);
|
||||
|
||||
register(
|
||||
"gitlab_pipeline_status",
|
||||
"Показать статусы CI/CD пайплайнов проекта GitLab (по ветке).",
|
||||
run: async (p) =>
|
||||
mrCreate({
|
||||
title: p.title,
|
||||
sourceBranch: p.sourceBranch || undefined,
|
||||
targetBranch: p.targetBranch,
|
||||
project: p.project,
|
||||
removeSourceBranch: p.removeSourceBranch,
|
||||
mergeWhenPipelineSucceeds: p.mergeWhenPipelineSucceeds,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "gitlab_pipeline_status",
|
||||
description: "Показать статусы CI/CD пайплайнов проекта GitLab (по ветке).",
|
||||
errorPrefix: "gitlab",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
project: { type: "string", description: "Группа/репозиторий (обязательно; например group/name)" },
|
||||
@@ -57,13 +50,13 @@ export default function gitlabToolsExtension(pi: ExtensionAPI) {
|
||||
},
|
||||
required: ["project"],
|
||||
},
|
||||
async (p) => pipelineStatus(p.project, { ref: p.ref, perPage: p.perPage }),
|
||||
);
|
||||
|
||||
register(
|
||||
"gitlab_code_search",
|
||||
"Поиск по коду в GitLab (blobs). Требует scope api/read_api у токена.",
|
||||
run: async (p) => pipelineStatus(p.project, { ref: p.ref, perPage: p.perPage }),
|
||||
},
|
||||
{
|
||||
name: "gitlab_code_search",
|
||||
description: "Поиск по коду в GitLab (blobs). Требует scope api/read_api у токена.",
|
||||
errorPrefix: "gitlab",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "Строка поиска (до 512 символов)" },
|
||||
@@ -72,6 +65,9 @@ export default function gitlabToolsExtension(pi: ExtensionAPI) {
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
async (p) => codeSearch(p.query, { project: p.project, perPage: p.perPage }),
|
||||
);
|
||||
run: async (p) => codeSearch(p.query, { project: p.project, perPage: p.perPage }),
|
||||
},
|
||||
];
|
||||
|
||||
for (const t of tools) registerTool(pi, t);
|
||||
}
|
||||
|
||||
+46
-62
@@ -9,6 +9,7 @@
|
||||
* DISABLE: remove/rename this file from extensions/.
|
||||
*/
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { registerTool, type ToolSpec } from "./lib/tools.ts";
|
||||
import {
|
||||
issueGet,
|
||||
issueCreate,
|
||||
@@ -19,22 +20,20 @@ import {
|
||||
linkMr,
|
||||
} from "./lib/jira.ts";
|
||||
|
||||
function tool(name: string, description: string, parameters: Record<string, unknown>, run: (params: any) => Promise<unknown>) {
|
||||
return { name, description, parameters, run };
|
||||
}
|
||||
|
||||
export default function jiraToolsExtension(pi: ExtensionAPI) {
|
||||
const tools = [
|
||||
tool(
|
||||
"jira_issue_get",
|
||||
"Получить тикет Jira по ключу (summary, status, description).",
|
||||
{ type: "object", properties: { key: { type: "string", description: "Ключ задачи, например PROJ-123" } }, required: ["key"] },
|
||||
async (p) => issueGet(p.key),
|
||||
),
|
||||
tool(
|
||||
"jira_issue_create",
|
||||
"Создать задачу в Jira.",
|
||||
const tools: ToolSpec[] = [
|
||||
{
|
||||
name: "jira_issue_get",
|
||||
description: "Получить тикет Jira по ключу (summary, status, description).",
|
||||
errorPrefix: "jira",
|
||||
parameters: { type: "object", properties: { key: { type: "string", description: "Ключ задачи, например PROJ-123" } }, required: ["key"] },
|
||||
run: async (p) => issueGet(p.key),
|
||||
},
|
||||
{
|
||||
name: "jira_issue_create",
|
||||
description: "Создать задачу в Jira.",
|
||||
errorPrefix: "jira",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectKey: { type: "string", description: "Ключ проекта, например PROJ" },
|
||||
@@ -47,12 +46,13 @@ export default function jiraToolsExtension(pi: ExtensionAPI) {
|
||||
},
|
||||
required: ["projectKey", "summary", "issuetype"],
|
||||
},
|
||||
async (p) => issueCreate(p),
|
||||
),
|
||||
tool(
|
||||
"jira_issue_update",
|
||||
"Обновить поля задачи Jira (summary/description/status и т.п.). Status переводит по доступным переходам.",
|
||||
run: async (p) => issueCreate(p),
|
||||
},
|
||||
{
|
||||
name: "jira_issue_update",
|
||||
description: "Обновить поля задачи Jira (summary/description/status и т.п.). Status переводит по доступным переходам.",
|
||||
errorPrefix: "jira",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
key: { type: "string", description: "Ключ задачи" },
|
||||
@@ -65,12 +65,13 @@ export default function jiraToolsExtension(pi: ExtensionAPI) {
|
||||
},
|
||||
required: ["key"],
|
||||
},
|
||||
async (p) => issueUpdate(p.key, p),
|
||||
),
|
||||
tool(
|
||||
"jira_issue_comment",
|
||||
"Добавить комментарий к тикету Jira.",
|
||||
run: async (p) => issueUpdate(p.key, p),
|
||||
},
|
||||
{
|
||||
name: "jira_issue_comment",
|
||||
description: "Добавить комментарий к тикету Jira.",
|
||||
errorPrefix: "jira",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
key: { type: "string", description: "Ключ задачи" },
|
||||
@@ -78,12 +79,13 @@ export default function jiraToolsExtension(pi: ExtensionAPI) {
|
||||
},
|
||||
required: ["key", "text"],
|
||||
},
|
||||
async (p) => issueComment(p.key, p.text),
|
||||
),
|
||||
tool(
|
||||
"jira_issue_transition",
|
||||
"Перевести тикет Jira в статус (To Do / In Progress / Review / Done).",
|
||||
run: async (p) => issueComment(p.key, p.text),
|
||||
},
|
||||
{
|
||||
name: "jira_issue_transition",
|
||||
description: "Перевести тикет Jira в статус (To Do / In Progress / Review / Done).",
|
||||
errorPrefix: "jira",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
key: { type: "string", description: "Ключ задачи" },
|
||||
@@ -91,12 +93,13 @@ export default function jiraToolsExtension(pi: ExtensionAPI) {
|
||||
},
|
||||
required: ["key", "target"],
|
||||
},
|
||||
async (p) => setStatus(p.key, p.target),
|
||||
),
|
||||
tool(
|
||||
"jira_search",
|
||||
"Найти тикеты Jira по JQL-запросу.",
|
||||
run: async (p) => setStatus(p.key, p.target),
|
||||
},
|
||||
{
|
||||
name: "jira_search",
|
||||
description: "Найти тикеты Jira по JQL-запросу.",
|
||||
errorPrefix: "jira",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
jql: { type: "string", description: "JQL, например 'project = PROJ AND status != Done'" },
|
||||
@@ -104,12 +107,13 @@ export default function jiraToolsExtension(pi: ExtensionAPI) {
|
||||
},
|
||||
required: ["jql"],
|
||||
},
|
||||
async (p) => search(p.jql, p.maxResults),
|
||||
),
|
||||
tool(
|
||||
"jira_link_mr",
|
||||
"Привязать ссылку на merge request к тикету Jira (комментарий со ссылкой).",
|
||||
run: async (p) => search(p.jql, p.maxResults),
|
||||
},
|
||||
{
|
||||
name: "jira_link_mr",
|
||||
description: "Привязать ссылку на merge request к тикету Jira (комментарий со ссылкой).",
|
||||
errorPrefix: "jira",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
key: { type: "string", description: "Ключ задачи" },
|
||||
@@ -118,29 +122,9 @@ export default function jiraToolsExtension(pi: ExtensionAPI) {
|
||||
},
|
||||
required: ["key", "mrUrl"],
|
||||
},
|
||||
async (p) => linkMr(p.key, p.mrUrl, p.title),
|
||||
),
|
||||
run: async (p) => linkMr(p.key, p.mrUrl, p.title),
|
||||
},
|
||||
];
|
||||
|
||||
for (const t of tools) {
|
||||
pi.registerTool({
|
||||
name: t.name,
|
||||
label: t.name,
|
||||
description: t.description,
|
||||
parameters: t.parameters as any,
|
||||
async execute(_id: string, params: unknown) {
|
||||
try {
|
||||
const out = await t.run(params ?? {});
|
||||
return { content: [{ type: "text", text: JSON.stringify(out ?? {}, null, 2) }], details: {} };
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
return {
|
||||
content: [{ type: "text", text: `jira error: ${msg}` }],
|
||||
isError: true,
|
||||
details: {},
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
for (const t of tools) registerTool(pi, t);
|
||||
}
|
||||
|
||||
+11
-11
@@ -63,40 +63,40 @@ export default function kitCliExtension(pi: ExtensionAPI) {
|
||||
pi.registerCommand("kit-doctor", {
|
||||
description: "Проверка окружения pi-kit (node, версия, конфиг, env, токены)",
|
||||
handler: async (_args, ctx) => {
|
||||
const ok = (s: string) => `✓ ${s}`;
|
||||
const warn = (s: string) => `⚠ ${s}`;
|
||||
const status = (ok: boolean, text: string) => `${ok ? "✓" : "⚠"} ${text}`;
|
||||
const cfg = readConfig();
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(ok(`Node.js ${process.version}`));
|
||||
lines.push(ok(`pi-kit v${readVersion()}${process.env.PI_KIT_CHANNEL ? ` (канал: ${process.env.PI_KIT_CHANNEL})` : ""}`));
|
||||
lines.push(status(true, `Node.js ${process.version}`));
|
||||
lines.push(status(true, `pi-kit v${readVersion()}${process.env.PI_KIT_CHANNEL ? ` (канал: ${process.env.PI_KIT_CHANNEL})` : ""}`));
|
||||
|
||||
// Config
|
||||
const cfgOk = typeof cfg.gitHost === "string" && cfg.gitHost !== "TODO";
|
||||
lines.push((cfgOk ? ok : warn)(`config: gitHost=${cfg.gitHost ?? "?"}, tracker=${cfg.trackerUrl ?? "?"}, docs=${cfg.docsUrl ?? "?"}`));
|
||||
if (cfg.remoteConfigUrl) lines.push(ok(`remoteConfigUrl задан`));
|
||||
lines.push(status(cfgOk, `config: gitHost=${cfg.gitHost ?? "?"}, tracker=${cfg.trackerUrl ?? "?"}, docs=${cfg.docsUrl ?? "?"}`));
|
||||
if (cfg.remoteConfigUrl) lines.push(status(true, `remoteConfigUrl задан`));
|
||||
// TODO-placeholder values are a readiness signal, not an error: warn so
|
||||
// maintainers notice they haven't filled the single source of truth yet.
|
||||
for (const [field, value] of Object.entries({ repoMap: cfg.repoMap, trackerUrl: cfg.trackerUrl, docsUrl: cfg.docsUrl })) {
|
||||
if (typeof value !== "string" || value === "TODO" || value.trim() === "" || value.startsWith("TODO")) {
|
||||
lines.push(warn(`config: ${field} не заполнен (TODO) — скиллы repo-map/docs-map/jira будут просить уточнить`));
|
||||
lines.push(status(false, `config: ${field} не заполнен (TODO) — скиллы repo-map/docs-map/jira будут просить уточнить`));
|
||||
}
|
||||
}
|
||||
|
||||
// Env vars (presence only, never values)
|
||||
for (const v of ["TRACKER_URL", "JIRA_TOKEN", "CONFLUENCE_TOKEN", "GITLAB_TOKEN"]) {
|
||||
lines.push((process.env[v] ? ok : warn)(`${v} ${process.env[v] ? "задан" : "не задан"}`));
|
||||
lines.push(status(Boolean(process.env[v]), `${v} ${process.env[v] ? "задан" : "не задан"}`));
|
||||
}
|
||||
const provider = PROVIDER_KEYS.find((k) => process.env[k]);
|
||||
lines.push((provider ? ok : warn)(provider ? `LLM-ключ: ${provider}` : "LLM-ключ провайдера не найден"));
|
||||
lines.push(status(Boolean(provider), provider ? `LLM-ключ: ${provider}` : "LLM-ключ провайдера не найден"));
|
||||
|
||||
// env.sh
|
||||
lines.push((fs.existsSync(ENV_FILE) ? ok : warn)(`env.sh ${fs.existsSync(ENV_FILE) ? "есть" : "нет"} (${ENV_FILE})`));
|
||||
const envShExists = fs.existsSync(ENV_FILE);
|
||||
lines.push(status(envShExists, `env.sh ${envShExists ? "есть" : "нет"} (${ENV_FILE})`));
|
||||
|
||||
// Loaded commands/skills
|
||||
const cmds = pi.getCommands();
|
||||
const skills = cmds.filter((c) => c.source === "skill").length;
|
||||
lines.push(ok(`команд загружено: ${cmds.length} (скилов: ${skills})`));
|
||||
lines.push(status(true, `команд загружено: ${cmds.length} (скилов: ${skills})`));
|
||||
|
||||
const warns = lines.filter((l) => l.startsWith("⚠")).length;
|
||||
lines.push("", warns === 0 ? "Итог: всё ок." : `Итог: предупреждений — ${warns}.`);
|
||||
|
||||
@@ -36,6 +36,18 @@ function base(url: string): string {
|
||||
return url.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an effective base URL from company.json + an env override.
|
||||
* The env var wins when set (and not "TODO"); otherwise the config value
|
||||
* (if set and not "TODO") is used. Trailing slashes are trimmed. Returns ""
|
||||
* when neither is configured; callers decide whether to throw.
|
||||
*/
|
||||
export function resolveBaseUrl(envName: string, cfgValue: string | null | undefined): string {
|
||||
const fromCfg = cfgValue && cfgValue !== "TODO" ? cfgValue : "";
|
||||
const env = process.env[envName];
|
||||
return (env && env !== "TODO" ? env : fromCfg).replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
/** Build a URL with query params; undefined/empty values are dropped. */
|
||||
export function buildUrl(
|
||||
baseUrl: string,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* `docsUrl` comes from config/company.json; `CONFLUENCE_TOKEN` from the env.
|
||||
* No pi API — unit-testable.
|
||||
*/
|
||||
import { apiRequest, requireToken, ApiError } from "./atlassian.ts";
|
||||
import { apiRequest, requireToken, ApiError, resolveBaseUrl } from "./atlassian.ts";
|
||||
import { readLocalConfig } from "./company-config.ts";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -14,8 +14,7 @@ const CONFIG_PATH = path.join(HERE, "..", "..", "config", "company.json");
|
||||
/** Resolve the Confluence base URL from company.json (env override for labs). */
|
||||
export function confluenceBaseUrl(): string {
|
||||
const cfg = readLocalConfig(CONFIG_PATH);
|
||||
const fromCfg = cfg.docsUrl && cfg.docsUrl !== "TODO" ? cfg.docsUrl : "";
|
||||
const url = (process.env.CONFLUENCE_URL && process.env.CONFLUENCE_URL !== "TODO" ? process.env.CONFLUENCE_URL : fromCfg).replace(/\/+$/, "");
|
||||
const url = resolveBaseUrl("CONFLUENCE_URL", cfg.docsUrl);
|
||||
if (!url) throw new ApiError("Не задан docsUrl в config/company.json и нет CONFLUENCE_URL.");
|
||||
return url;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* `trackerUrl` comes from config/company.json; `JIRA_TOKEN` from the env.
|
||||
* No pi API — unit-testable (see test/api.test.ts).
|
||||
*/
|
||||
import { apiRequest, requireToken, ApiError } from "./atlassian.ts";
|
||||
import { apiRequest, requireToken, ApiError, resolveBaseUrl } from "./atlassian.ts";
|
||||
import { readLocalConfig } from "./company-config.ts";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -11,12 +11,11 @@ import { fileURLToPath } from "node:url";
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const CONFIG_PATH = path.join(HERE, "..", "..", "config", "company.json");
|
||||
|
||||
/** Resolve the tracker base URL — prefer company.json; env override wins. */
|
||||
/** Resolve the tracker base URL — company.json default, env override wins. */
|
||||
export function jiraBaseUrl(): string {
|
||||
const cfg = readLocalConfig(CONFIG_PATH);
|
||||
const fromCfg = cfg.trackerUrl && cfg.trackerUrl !== "TODO" ? cfg.trackerUrl : "";
|
||||
// env TRACKER_URL may override the packaged default for local labs.
|
||||
return (process.env.TRACKER_URL && process.env.TRACKER_URL !== "TODO" ? process.env.TRACKER_URL : fromCfg).replace(/\/+$/, "");
|
||||
return resolveBaseUrl("TRACKER_URL", cfg.trackerUrl);
|
||||
}
|
||||
|
||||
function token(): string {
|
||||
|
||||
@@ -29,12 +29,8 @@ export const SECRET_PATTERNS: SecretPattern[] = [
|
||||
/** Return the names of secret patterns found in `text` (empty if none). */
|
||||
export function scanSecrets(text: string): string[] {
|
||||
if (!text) return [];
|
||||
const hits: string[] = [];
|
||||
for (const p of SECRET_PATTERNS) {
|
||||
// Use a non-stateful copy (no global flag) to avoid lastIndex surprises.
|
||||
if (p.re.test(text)) hits.push(p.name);
|
||||
}
|
||||
return hits;
|
||||
// No pattern carries the /g flag, so `test` stays stateless and order is preserved.
|
||||
return SECRET_PATTERNS.filter((p) => p.re.test(text)).map((p) => p.name);
|
||||
}
|
||||
|
||||
/** Replace every secret occurrence with a labelled placeholder. */
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Shared native pi tool registration.
|
||||
* The three *-tools.ts extensions each register tools whose `execute` bodies
|
||||
* are byte-identical apart from the error prefix in the failure payload.
|
||||
* This helper DRYs that boilerplate; extensions only supply the spec.
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
export interface ToolSpec {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: Record<string, unknown>;
|
||||
/** Prefix in the error payload, e.g. "jira" → "jira error: …". */
|
||||
errorPrefix: string;
|
||||
run: (params: any) => Promise<unknown>;
|
||||
}
|
||||
|
||||
/** Register a native tool whose result/error payload is formatted uniformly. */
|
||||
export function registerTool(pi: ExtensionAPI, spec: ToolSpec): void {
|
||||
pi.registerTool({
|
||||
name: spec.name,
|
||||
label: spec.name,
|
||||
description: spec.description,
|
||||
parameters: spec.parameters as any,
|
||||
async execute(_id: string, params: unknown) {
|
||||
try {
|
||||
const out = await spec.run(params ?? {});
|
||||
return { content: [{ type: "text", text: JSON.stringify(out ?? {}, null, 2) }], details: {} };
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
return { content: [{ type: "text", text: `${spec.errorPrefix} error: ${msg}` }], isError: true, details: {} };
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user