@tplog/pi-zendy 0.2.17 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,81 @@
1
+ // Config store: read/write ~/.zendy/config.json with env override.
2
+ // Priority: env vars > config file > defaults.
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { homedir } from "node:os";
6
+ const CONFIG_DIR = join(homedir(), ".zendy");
7
+ const CONFIG_PATH = join(CONFIG_DIR, "config.json");
8
+ // ── Env overrides ─────────────────────────────────────────────────────
9
+ function envZendeskConfig() {
10
+ const c = {};
11
+ const v = process.env;
12
+ if (v["ZENDY_ZENDESK_SUBDOMAIN"])
13
+ c.subdomain = v["ZENDY_ZENDESK_SUBDOMAIN"];
14
+ if (v["ZENDY_ZENDESK_EMAIL"])
15
+ c.email = v["ZENDY_ZENDESK_EMAIL"];
16
+ if (v["ZENDY_ZENDESK_API_TOKEN"])
17
+ c.apiToken = v["ZENDY_ZENDESK_API_TOKEN"];
18
+ return c;
19
+ }
20
+ function envKgConfig() {
21
+ const c = {};
22
+ const v = process.env;
23
+ if (v["ZENDY_KG_API_URL"])
24
+ c.apiUrl = v["ZENDY_KG_API_URL"];
25
+ if (v["ZENDY_KG_API_KEY"])
26
+ c.apiKey = v["ZENDY_KG_API_KEY"];
27
+ return c;
28
+ }
29
+ // ── File read ─────────────────────────────────────────────────────────
30
+ function readConfigFile() {
31
+ try {
32
+ if (!existsSync(CONFIG_PATH))
33
+ return null;
34
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
35
+ return JSON.parse(raw);
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ // ── Public API ────────────────────────────────────────────────────────
42
+ export function getConfig() {
43
+ const fileConfig = readConfigFile() ?? {};
44
+ const envZ = envZendeskConfig();
45
+ const envK = envKgConfig();
46
+ return {
47
+ zendesk: {
48
+ ...fileConfig.zendesk,
49
+ ...envZ,
50
+ },
51
+ zendeskKg: {
52
+ ...fileConfig.zendeskKg,
53
+ ...envK,
54
+ },
55
+ };
56
+ }
57
+ export function getZendeskConfig() {
58
+ const c = getConfig().zendesk;
59
+ if (c?.subdomain && c?.email && c?.apiToken)
60
+ return c;
61
+ return undefined;
62
+ }
63
+ export function getZendeskKgConfig() {
64
+ const c = getConfig().zendeskKg;
65
+ if (c?.apiKey)
66
+ return c;
67
+ return undefined;
68
+ }
69
+ export function writeConfig(config) {
70
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
71
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
72
+ encoding: "utf-8",
73
+ mode: 0o600,
74
+ });
75
+ }
76
+ export function configPath() {
77
+ return CONFIG_PATH;
78
+ }
79
+ export function configExists() {
80
+ return existsSync(CONFIG_PATH);
81
+ }
package/dist/index.js CHANGED
@@ -15,27 +15,13 @@ const VERSION = pkg.version;
15
15
  // Resolve package root (one level up from dist/)
16
16
  const PKG_ROOT = join(__dirname, "..");
17
17
  // Embedded file paths relative to package root
18
- const AGENTS_MD = join(PKG_ROOT, "agents.md");
19
- const SKILL_ZCLI = join(PKG_ROOT, "skills", "zendesk-cli", "SKILL.md");
20
- const SKILL_HELM_WATCHDOG = join(PKG_ROOT, "skills", "helm-watchdog", "SKILL.md");
21
- const SKILL_SOURCE_CHECK = join(PKG_ROOT, "skills", "source-check", "SKILL.md");
22
- const SKILL_ZENDESK_KG = join(PKG_ROOT, "skills", "zendesk-kg", "SKILL.md");
23
- const EXT_CUSTOM_HEADER = join(PKG_ROOT, "extensions", "custom-header.ts");
24
- const EXT_SOURCE_CLEANUP = join(PKG_ROOT, "extensions", "source-cleanup.ts");
25
- const EXT_STATUS = join(PKG_ROOT, "extensions", "status.ts");
18
+ const EXT_ZENDY = join(PKG_ROOT, "extensions", "zendy.ts");
26
19
  function cacheDir() {
27
20
  return join(homedir(), ".zendy", VERSION);
28
21
  }
29
22
  // All source files that get extracted to cache
30
23
  const SOURCE_FILES = [
31
- SKILL_ZCLI,
32
- SKILL_HELM_WATCHDOG,
33
- SKILL_SOURCE_CHECK,
34
- SKILL_ZENDESK_KG,
35
- EXT_CUSTOM_HEADER,
36
- EXT_SOURCE_CLEANUP,
37
- EXT_STATUS,
38
- AGENTS_MD,
24
+ EXT_ZENDY,
39
25
  ];
40
26
  function contentHash() {
41
27
  const hash = createHash("sha256");
@@ -51,23 +37,9 @@ function ensureExtracted(base) {
51
37
  if (existsSync(marker) && readFileSync(marker, "utf-8").trim() === currentHash) {
52
38
  return;
53
39
  }
54
- const skills = [
55
- ["zendesk-cli", SKILL_ZCLI],
56
- ["helm-watchdog", SKILL_HELM_WATCHDOG],
57
- ["source-check", SKILL_SOURCE_CHECK],
58
- ["zendesk-kg", SKILL_ZENDESK_KG],
59
- ];
60
- for (const [name, srcPath] of skills) {
61
- const dir = join(base, "skills", name);
62
- mkdirSync(dir, { recursive: true });
63
- writeFileSync(join(dir, "SKILL.md"), readFileSync(srcPath, "utf-8"));
64
- }
65
40
  const extDir = join(base, "extensions");
66
41
  mkdirSync(extDir, { recursive: true });
67
- writeFileSync(join(extDir, "custom-header.ts"), readFileSync(EXT_CUSTOM_HEADER, "utf-8"));
68
- writeFileSync(join(extDir, "source-cleanup.ts"), readFileSync(EXT_SOURCE_CLEANUP, "utf-8"));
69
- writeFileSync(join(extDir, "status.ts"), readFileSync(EXT_STATUS, "utf-8"));
70
- writeFileSync(join(base, "agents.md"), readFileSync(AGENTS_MD, "utf-8"));
42
+ writeFileSync(join(extDir, "zendy.ts"), readFileSync(EXT_ZENDY, "utf-8"));
71
43
  writeFileSync(marker, currentHash);
72
44
  }
73
45
  function findPi() {
@@ -136,23 +108,11 @@ async function main() {
136
108
  const base = cacheDir();
137
109
  ensureExtracted(base);
138
110
  const pi = findPi();
139
- const skillsDir = join(base, "skills");
140
- const agentsPath = join(base, "agents.md");
141
111
  const extensionsDir = join(base, "extensions");
142
- const skillNames = ["zendesk-cli", "helm-watchdog", "source-check", "zendesk-kg"];
143
112
  const args = [
144
- "--no-skills",
145
- "--extension",
146
- join(extensionsDir, "custom-header.ts"),
147
113
  "--extension",
148
- join(extensionsDir, "source-cleanup.ts"),
149
- "--extension",
150
- join(extensionsDir, "status.ts"),
114
+ join(extensionsDir, "zendy.ts"),
151
115
  ];
152
- for (const name of skillNames) {
153
- args.push("--skill", join(skillsDir, name));
154
- }
155
- args.push("--append-system-prompt", agentsPath);
156
116
  // Pass through all user arguments
157
117
  args.push(...filteredArgs);
158
118
  // Spawn pi, replacing this process (inherit stdio for interactive use)
package/dist/preflight.js CHANGED
@@ -3,6 +3,8 @@ import { existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
  import { createInterface } from "node:readline";
6
+ import { getConfig } from "./config/store.js";
7
+ import { migrateLegacyConfig, getLegacyZendeskConfig, getLegacyKgEnv } from "./config/migrate.js";
6
8
  // ── Helpers ────────────────────────────────────────────────────────────
7
9
  function exec(cmd, args, timeoutMs = 5000) {
8
10
  return new Promise((resolve) => {
@@ -58,55 +60,130 @@ async function checkPi() {
58
60
  }
59
61
  return { ...base, status: "ok", hint: "" };
60
62
  }
61
- async function checkZcli() {
63
+ function resolveZendeskConfig() {
64
+ // 1. zendy config (env or ~/.zendy/config.json)
65
+ const cfg = getConfig();
66
+ if (cfg.zendesk?.subdomain && cfg.zendesk?.email && cfg.zendesk?.apiToken) {
67
+ return { subdomain: cfg.zendesk.subdomain, email: cfg.zendesk.email, apiToken: cfg.zendesk.apiToken };
68
+ }
69
+ // 2. Try legacy migration
70
+ migrateLegacyConfig();
71
+ const cfg2 = getConfig();
72
+ if (cfg2.zendesk?.subdomain && cfg2.zendesk?.email && cfg2.zendesk?.apiToken) {
73
+ return { subdomain: cfg2.zendesk.subdomain, email: cfg2.zendesk.email, apiToken: cfg2.zendesk.apiToken };
74
+ }
75
+ // 3. Legacy zcli config
76
+ const legacy = getLegacyZendeskConfig();
77
+ if (legacy?.subdomain && legacy?.email && legacy?.api_token) {
78
+ return { subdomain: legacy.subdomain, email: legacy.email, apiToken: legacy.api_token };
79
+ }
80
+ return null;
81
+ }
82
+ async function checkZendesk() {
62
83
  const base = {
63
- name: "zcli",
84
+ name: "zendesk",
64
85
  label: "Zendesk access",
65
86
  level: "core",
66
87
  };
67
- // `zcli whoami` calls GET /api/v2/users/me.json — succeeds only if zcli is
68
- // installed, configured, and the Zendesk credentials are actually accepted.
69
- const result = await exec("zcli", ["whoami"], 8000);
70
- if (result.code === 127) {
88
+ const cfg = resolveZendeskConfig();
89
+ if (!cfg) {
71
90
  return {
72
91
  ...base,
73
92
  status: "missing",
74
- hint: "Install zcli: npm install -g @tplog/zendesk-cli\nThen run: zcli configure",
93
+ hint: "Zendesk credentials not configured.\nUse /zendy-config in pi, or set env: ZENDY_ZENDESK_SUBDOMAIN, ZENDY_ZENDESK_EMAIL, ZENDY_ZENDESK_API_TOKEN",
75
94
  };
76
95
  }
77
- if (result.code === 0) {
78
- return { ...base, status: "ok", hint: "" };
96
+ try {
97
+ const ctrl = new AbortController();
98
+ const timeout = setTimeout(() => ctrl.abort(), 10000);
99
+ const auth = "Basic " + Buffer.from(`${cfg.email}/token:${cfg.apiToken}`).toString("base64");
100
+ const response = await fetch(`https://${cfg.subdomain}.zendesk.com/api/v2/users/me.json`, {
101
+ headers: { Authorization: auth, Accept: "application/json", "User-Agent": "zendy-preflight" },
102
+ signal: ctrl.signal,
103
+ });
104
+ clearTimeout(timeout);
105
+ if (response.ok) {
106
+ const data = await response.json();
107
+ return { ...base, status: "ok", hint: `Authenticated as ${data.user?.email ?? "unknown"}` };
108
+ }
109
+ return {
110
+ ...base,
111
+ status: "auth_error",
112
+ hint: `Zendesk API returned ${response.status}. Check credentials in ~/.zendy/config.json or env vars.`,
113
+ };
79
114
  }
80
- // zcli prints "Not configured..." to stderr from getConfig() when creds are absent.
81
- if (result.stderr.includes("Not configured")) {
115
+ catch (e) {
116
+ const msg = e.message || String(e);
82
117
  return {
83
118
  ...base,
84
119
  status: "auth_error",
85
- hint: "zcli is installed but Zendesk credentials are not configured.\nRun: zcli configure",
120
+ hint: `Zendesk API unreachable: ${msg.slice(0, 200)}`,
86
121
  };
87
122
  }
88
- // Configured but the API call failed — likely bad creds (401) or network issue.
89
- return {
90
- ...base,
91
- status: "auth_error",
92
- hint: "zcli is configured but the Zendesk API check failed.\nRun `zcli whoami` to see the error (check credentials and network).",
93
- };
94
123
  }
95
- async function checkZendeskKg() {
124
+ function resolveKgConfig() {
125
+ // 1. zendy config
126
+ const cfg = getConfig();
127
+ if (cfg.zendeskKg?.apiKey)
128
+ return { apiUrl: cfg.zendeskKg.apiUrl, apiKey: cfg.zendeskKg.apiKey };
129
+ // 2. Try legacy migration
130
+ migrateLegacyConfig();
131
+ const cfg2 = getConfig();
132
+ if (cfg2.zendeskKg?.apiKey)
133
+ return { apiUrl: cfg2.zendeskKg.apiUrl, apiKey: cfg2.zendeskKg.apiKey };
134
+ // 3. Legacy .env
135
+ const legacy = getLegacyKgEnv();
136
+ if (legacy?.["RETRIEVER_API_KEY"]) {
137
+ return { apiUrl: legacy["RETRIEVER_API_URL"] || undefined, apiKey: legacy["RETRIEVER_API_KEY"] };
138
+ }
139
+ return null;
140
+ }
141
+ const DEFAULT_KG_API_URL = "https://zendesk-ticket-retriever.vercel.app";
142
+ async function checkZendeskKgApi() {
96
143
  const base = {
97
144
  name: "zendesk-kg",
98
145
  label: "Zendesk Knowledge Graph",
99
146
  level: "enhanced",
100
147
  };
101
- const result = await exec("zendesk-kg", ["--version"]);
102
- if (result.code === 127) {
148
+ const cfg = resolveKgConfig();
149
+ if (!cfg) {
103
150
  return {
104
151
  ...base,
105
152
  status: "missing",
106
- hint: "zendesk-kg not installed. Install with:\ncurl -fsSL https://raw.githubusercontent.com/sorphwer/zendesk-kg-cli-release/main/install.sh | sh\nThen run: zendesk-kg init",
153
+ hint: "Knowledge Graph API key not configured.\nUse /zendy-config in pi, or set env: ZENDY_KG_API_KEY",
154
+ };
155
+ }
156
+ try {
157
+ const ctrl = new AbortController();
158
+ const timeout = setTimeout(() => ctrl.abort(), 10000);
159
+ const baseUrl = cfg.apiUrl || DEFAULT_KG_API_URL;
160
+ const headers = {
161
+ Accept: "application/json",
162
+ Authorization: `Bearer ${cfg.apiKey}`,
163
+ "User-Agent": "zendy-preflight",
164
+ };
165
+ if (cfg.apiKey)
166
+ headers["x-api-key"] = cfg.apiKey;
167
+ const response = await fetch(`${baseUrl}/health`, { headers, signal: ctrl.signal });
168
+ clearTimeout(timeout);
169
+ if (response.ok) {
170
+ const data = await response.json();
171
+ return { ...base, status: "ok", hint: `Status: ${data.status ?? "ok"}` };
172
+ }
173
+ return {
174
+ ...base,
175
+ status: "auth_error",
176
+ hint: `KG API returned ${response.status}. Check credentials.`,
177
+ };
178
+ }
179
+ catch (e) {
180
+ const msg = e.message || String(e);
181
+ return {
182
+ ...base,
183
+ status: "auth_error",
184
+ hint: `KG API unreachable: ${msg.slice(0, 200)}`,
107
185
  };
108
186
  }
109
- return { ...base, status: "ok", hint: "" };
110
187
  }
111
188
  async function checkGithub() {
112
189
  const base = {
@@ -136,7 +213,7 @@ async function checkGithub() {
136
213
  }
137
214
  // ── Preflight runner ───────────────────────────────────────────────────
138
215
  export async function runPreflight() {
139
- const results = await Promise.all([checkPi(), checkZcli(), checkZendeskKg(), checkGithub()]);
216
+ const results = await Promise.all([checkPi(), checkZendesk(), checkZendeskKgApi(), checkGithub()]);
140
217
  const failed = results.filter((r) => r.status !== "ok");
141
218
  return {
142
219
  results,
@@ -0,0 +1,119 @@
1
+ // Zendy slash commands — /zendy-config, /zendy-status.
2
+ // Loaded by jiti as part of the zendy extension.
3
+
4
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
+ import { getConfig, writeConfig, configPath, configExists } from "../dist/config/store.js";
6
+ import { migrateLegacyConfig } from "../dist/config/migrate.js";
7
+ import type { ZendyConfig } from "../dist/config/schema.js";
8
+ import * as zendesk from "../dist/clients/zendesk.js";
9
+ import * as kg from "../dist/clients/zendesk-kg.js";
10
+
11
+ async function configCommand(_args: string, ctx: {
12
+ ui: {
13
+ select: (prompt: string, options: string[]) => Promise<string | undefined>;
14
+ input: (prompt: string, placeholder?: string) => Promise<string | undefined>;
15
+ notify: (msg: string, level: string) => void;
16
+ };
17
+ }): Promise<void> {
18
+ const migrated = migrateLegacyConfig();
19
+ if (migrated?.migrated) {
20
+ ctx.ui.notify(`[zendy-config] Auto-imported credentials from ${migrated.source}`, "info");
21
+ }
22
+
23
+ const action = await ctx.ui.select("Zendy Config — what would you like to configure?", [
24
+ "Zendesk credentials",
25
+ "Knowledge Graph credentials",
26
+ "View current config",
27
+ ]);
28
+ if (!action) return;
29
+
30
+ const config: ZendyConfig = getConfig();
31
+
32
+ if (action === "Zendesk credentials") {
33
+ const subdomain = await ctx.ui.input("Zendesk subdomain (e.g., dify)", config.zendesk?.subdomain ?? "dify");
34
+ const email = await ctx.ui.input("Zendesk email address", config.zendesk?.email ?? "");
35
+ const apiToken = await ctx.ui.input("Zendesk API token", "");
36
+ config.zendesk = {
37
+ ...config.zendesk,
38
+ subdomain: subdomain || config.zendesk?.subdomain,
39
+ email: email || config.zendesk?.email,
40
+ apiToken: apiToken || config.zendesk?.apiToken,
41
+ };
42
+ writeConfig(config);
43
+ ctx.ui.notify("[zendy-config] Zendesk credentials saved.", "info");
44
+ } else if (action === "Knowledge Graph credentials") {
45
+ const apiUrl = await ctx.ui.input(
46
+ "Knowledge Graph API URL (leave empty for default)",
47
+ config.zendeskKg?.apiUrl ?? "https://zendesk-ticket-retriever.vercel.app",
48
+ );
49
+ const apiKey = await ctx.ui.input("Knowledge Graph API key", "");
50
+ config.zendeskKg = {
51
+ ...config.zendeskKg,
52
+ apiUrl: apiUrl || config.zendeskKg?.apiUrl,
53
+ apiKey: apiKey || config.zendeskKg?.apiKey,
54
+ };
55
+ writeConfig(config);
56
+ ctx.ui.notify("[zendy-config] Knowledge Graph credentials saved.", "info");
57
+ } else if (action === "View current config") {
58
+ const c = getConfig();
59
+ const lines: string[] = [];
60
+ lines.push(`Config file: ${configPath()}`);
61
+ lines.push(`Exists: ${configExists() ? "yes" : "no"}`);
62
+ lines.push(
63
+ `Zendesk: ${c.zendesk?.subdomain ? `subdomain=${c.zendesk.subdomain}, email=${c.zendesk.email?.replace(/(.).+(@.+)/, "$1***$2") ?? "not set"}` : "not configured"}`,
64
+ );
65
+ lines.push(
66
+ `KG: ${c.zendeskKg?.apiKey ? `apiKey=*** (set), apiUrl=${c.zendeskKg.apiUrl || "default"}` : "not configured"}`,
67
+ );
68
+ ctx.ui.notify(`[zendy-config]\n${lines.join("\n")}`, "info");
69
+ }
70
+ }
71
+
72
+ async function statusCommand(_args: string, ctx: {
73
+ ui: { notify: (msg: string, level: "info" | "error" | "warn") => void };
74
+ }): Promise<void> {
75
+ const lines: string[] = [];
76
+ let hasIssue = false;
77
+
78
+ try {
79
+ const me = await zendesk.getMe();
80
+ lines.push(` ✓ Zendesk — authenticated as ${me.user.email}`);
81
+ } catch (e) {
82
+ lines.push(` ✗ Zendesk — ${(e as Error).message.split("\n")[0]}`);
83
+ hasIssue = true;
84
+ }
85
+
86
+ try {
87
+ const health = await kg.healthCheck();
88
+ lines.push(` ✓ Knowledge Graph — ${health.status}`);
89
+ } catch (e) {
90
+ lines.push(` ✗ Knowledge Graph — ${(e as Error).message.split("\n")[0]}`);
91
+ hasIssue = true;
92
+ }
93
+
94
+ try {
95
+ const r = await fetch("https://dify-helm-watchdog.vercel.app/api/v1/versions/latest?versionOnly=true");
96
+ lines.push(` ✓ Helm Watchdog — latest chart: ${(await r.text()).trim()}`);
97
+ } catch (e) {
98
+ lines.push(` ✗ Helm Watchdog — ${(e as Error).message.split("\n")[0]}`);
99
+ hasIssue = true;
100
+ }
101
+
102
+ const srcDir = process.env["ZENDY_SRC_DIR"];
103
+ lines.push(
104
+ srcDir ? ` ✓ Source workspace — ${srcDir}` : " - Source workspace — not active (will be created on session start)",
105
+ );
106
+
107
+ ctx.ui.notify(`Zendy Status:\n${lines.join("\n")}`, hasIssue ? "error" : "info");
108
+ }
109
+
110
+ export function registerAllCommands(pi: ExtensionAPI): void {
111
+ pi.registerCommand("zendy-config", {
112
+ description: "Configure Zendesk and Knowledge Graph credentials",
113
+ handler: configCommand,
114
+ });
115
+ pi.registerCommand("zendy-status", {
116
+ description: "Check Zendy connectivity: Zendesk, KG, Helm Watchdog",
117
+ handler: statusCommand,
118
+ });
119
+ }
@@ -0,0 +1,190 @@
1
+ // Zendy tools — LLM-callable wrappers over direct API clients.
2
+ // Loaded by jiti as part of the zendy extension.
3
+
4
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
+ import { Type } from "typebox";
6
+ import * as zendesk from "../dist/clients/zendesk.js";
7
+ import * as helm from "../dist/clients/helm-watchdog.js";
8
+ import * as kg from "../dist/clients/zendesk-kg.js";
9
+
10
+ function textResult(text: string, details: Record<string, unknown> = {}) {
11
+ return { content: [{ type: "text" as const, text }], details };
12
+ }
13
+
14
+ function registerZendeskTools(pi: ExtensionAPI): void {
15
+ pi.registerTool({
16
+ name: "zendy_ticket_get",
17
+ label: "Zendy Ticket",
18
+ description: "Fetch a Zendesk ticket with comments and user info via direct API. Prefer this over any CLI command for ticket analysis.",
19
+ promptSnippet: "Fetch Zendesk ticket, comments, and user details with zendy_ticket_get.",
20
+ promptGuidelines: [
21
+ "Use zendy_ticket_get when the user provides a Zendesk ticket ID. It returns ticket metadata, comments, requester, and assignee.",
22
+ "Base conclusions on the returned ticket and comments; do not rely on external commands.",
23
+ ],
24
+ parameters: Type.Object({
25
+ ticketId: Type.Number({ description: "Zendesk ticket ID" }),
26
+ }),
27
+ async execute(_toolCallId: string, params: { ticketId: number }, signal?: AbortSignal) {
28
+ const result = await zendesk.getTicketFull(params.ticketId, signal);
29
+ return textResult(
30
+ `Fetched Zendesk ticket #${result.ticket.id}: "${result.ticket.subject}" (${result.ticket.status}), ${result.comments?.length ?? 0} comments.`,
31
+ {
32
+ ticket: result.ticket,
33
+ comments: result.comments,
34
+ requester: result.requester,
35
+ assignee: result.assignee,
36
+ },
37
+ );
38
+ },
39
+ });
40
+
41
+ pi.registerTool({
42
+ name: "zendy_ticket_search",
43
+ label: "Zendy Search",
44
+ description: "Search Zendesk tickets via direct API. For fresh/live Zendesk lookups, not historical semantic retrieval.",
45
+ promptSnippet: "Search live Zendesk tickets with zendy_ticket_search.",
46
+ parameters: Type.Object({
47
+ query: Type.String({ description: "Zendesk search query string" }),
48
+ }),
49
+ async execute(_toolCallId: string, params: { query: string }, signal?: AbortSignal) {
50
+ const result = await zendesk.searchTickets(params.query, signal);
51
+ return textResult(`Zendesk search returned ${result.count} results.`, { results: result.results, count: result.count });
52
+ },
53
+ });
54
+ }
55
+
56
+ function registerHelmTools(pi: ExtensionAPI): void {
57
+ pi.registerTool({
58
+ name: "zendy_helm_get",
59
+ label: "Zendy Helm",
60
+ description: "Query Dify Helm Watchdog API for chart metadata, values.yaml, images, and validation. Always provide the exact chart version.",
61
+ promptSnippet: "Query Helm chart data with zendy_helm_get.",
62
+ promptGuidelines: [
63
+ "Use zendy_helm_get to query Dify Helm chart metadata. Always pass the customer's exact version.",
64
+ "Defaults change between versions — never assume a config value without checking the right version.",
65
+ ],
66
+ parameters: Type.Object({
67
+ resource: Type.Union([
68
+ Type.Literal("version"),
69
+ Type.Literal("values"),
70
+ Type.Literal("images"),
71
+ Type.Literal("validation"),
72
+ Type.Literal("latest"),
73
+ Type.Literal("versions"),
74
+ Type.Literal("cache"),
75
+ ], { description: "Resource to fetch" }),
76
+ version: Type.Optional(Type.String({ description: "Chart version. Required for: version, values, images, validation" })),
77
+ validateImages: Type.Optional(Type.Boolean({ description: "For images: include pull-status validation", default: false })),
78
+ status: Type.Optional(Type.String({ description: "For validation: filter by MISSING etc." })),
79
+ versionOnly: Type.Optional(Type.Boolean({ description: "For latest: return plain version string only", default: false })),
80
+ }),
81
+ async execute(_toolCallId: string, params: {
82
+ resource: string;
83
+ version?: string;
84
+ validateImages?: boolean;
85
+ status?: string;
86
+ versionOnly?: boolean;
87
+ }, signal?: AbortSignal) {
88
+ const needsVersion = ["version", "values", "images", "validation"].includes(params.resource);
89
+ if (needsVersion && !params.version) {
90
+ throw new Error(`version is required when resource=${params.resource}`);
91
+ }
92
+
93
+ switch (params.resource) {
94
+ case "version": {
95
+ const data = await helm.getVersion(params.version!, signal);
96
+ return textResult(`Helm Watchdog version metadata for ${params.version}.`, { version: params.version, data });
97
+ }
98
+ case "values": {
99
+ const data = await helm.getValues(params.version!, signal);
100
+ return textResult(`Helm Watchdog values.yaml for ${params.version}.`, { version: params.version, data });
101
+ }
102
+ case "images": {
103
+ const data = await helm.getImages(params.version!, params.validateImages, signal);
104
+ return textResult(`Helm Watchdog images for ${params.version} (${data.length} images).`, { version: params.version, images: data });
105
+ }
106
+ case "validation": {
107
+ const data = await helm.getValidation(params.version!, params.status, signal);
108
+ return textResult(`Helm Watchdog validation for ${params.version}.`, { version: params.version, results: data });
109
+ }
110
+ case "latest": {
111
+ const data = await helm.getLatest(params.versionOnly, signal);
112
+ return textResult("Helm Watchdog latest version.", { data });
113
+ }
114
+ case "versions": {
115
+ const data = await helm.listVersions(signal);
116
+ return textResult(`Helm Watchdog cached versions (${data.length} entries).`, { versions: data });
117
+ }
118
+ case "cache": {
119
+ const data = await helm.getCache(signal);
120
+ return textResult("Helm Watchdog cache metadata.", { data });
121
+ }
122
+ }
123
+ },
124
+ });
125
+ }
126
+
127
+ function registerKnowledgeGraphTools(pi: ExtensionAPI): void {
128
+ pi.registerTool({
129
+ name: "zendy_kg_search",
130
+ label: "Zendy KG",
131
+ description: "Search the Zendesk Knowledge Graph for historically similar tickets via direct API. Always cite ticketId values from results.",
132
+ promptSnippet: "Search historical similar tickets with zendy_kg_search.",
133
+ promptGuidelines: [
134
+ "Use zendy_kg_search for historical similar-ticket retrieval. Cite ticketId values when summarizing.",
135
+ "Empty results do not prove no similar issue exists — the KG is a snapshot, not live Zendesk.",
136
+ ],
137
+ parameters: Type.Object({
138
+ query: Type.String({ description: "Natural-language description of the issue" }),
139
+ limit: Type.Optional(Type.Number({ description: "Max results 1-20", default: 5 })),
140
+ version: Type.Optional(Type.String({ description: "Filter by Dify version mentioned in tickets" })),
141
+ priority: Type.Optional(Type.String({ description: "Filter: low, normal, high, urgent" })),
142
+ status: Type.Optional(Type.String({ description: "Filter by ticket status: open, closed, pending, etc." })),
143
+ }),
144
+ async execute(_toolCallId: string, params: {
145
+ query: string;
146
+ limit?: number;
147
+ version?: string;
148
+ priority?: string;
149
+ status?: string;
150
+ }, signal?: AbortSignal) {
151
+ const limit = Math.min(Math.max(Math.trunc(params.limit ?? 5), 1), 20);
152
+ const result = await kg.search({
153
+ query: params.query,
154
+ topK: limit,
155
+ filter: {
156
+ ...(params.version ? { versions: [params.version] } : {}),
157
+ ...(params.priority ? { priority: params.priority } : {}),
158
+ ...(params.status ? { status: params.status } : {}),
159
+ },
160
+ }, signal);
161
+ return textResult(
162
+ `KG search returned ${result.results.length} results for "${result.queryText}". Cite ticketId values from details.results.`,
163
+ { results: result.results, queryText: result.queryText },
164
+ );
165
+ },
166
+ });
167
+ }
168
+
169
+ function registerSourceTools(pi: ExtensionAPI): void {
170
+ pi.registerTool({
171
+ name: "zendy_source_status",
172
+ label: "Zendy Source Status",
173
+ description: "Report the zendy source-analysis workspace path and whether source cloning is authorized.",
174
+ promptSnippet: "Check source workspace status with zendy_source_status.",
175
+ parameters: Type.Object({}),
176
+ async execute() {
177
+ return textResult("Source workspace status.", {
178
+ workspace: process.env["ZENDY_SRC_DIR"] ?? null,
179
+ note: "Source cloning/search requires explicit user permission per zendy workflow rules. Ask before cloning private source.",
180
+ });
181
+ },
182
+ });
183
+ }
184
+
185
+ export function registerAllTools(pi: ExtensionAPI): void {
186
+ registerZendeskTools(pi);
187
+ registerHelmTools(pi);
188
+ registerKnowledgeGraphTools(pi);
189
+ registerSourceTools(pi);
190
+ }