@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.
- package/README.md +51 -22
- package/dist/clients/helm-watchdog.d.ts +7 -0
- package/dist/clients/helm-watchdog.js +49 -0
- package/dist/clients/zendesk-kg.d.ts +59 -0
- package/dist/clients/zendesk-kg.js +100 -0
- package/dist/clients/zendesk.d.ts +64 -0
- package/dist/clients/zendesk.js +90 -0
- package/dist/config/migrate.d.ts +6 -0
- package/dist/config/migrate.js +78 -0
- package/dist/config/schema.d.ts +14 -0
- package/dist/config/schema.js +2 -0
- package/dist/config/store.d.ts +7 -0
- package/dist/config/store.js +81 -0
- package/dist/index.js +4 -44
- package/dist/preflight.js +101 -24
- package/extensions/commands.ts +119 -0
- package/extensions/tools.ts +190 -0
- package/extensions/zendy.ts +154 -0
- package/package.json +3 -9
- package/agents.md +0 -118
- package/extensions/custom-header.ts +0 -49
- package/extensions/source-cleanup.ts +0 -162
- package/extensions/status.ts +0 -82
- package/extensions/zendy-context.ts +0 -24
- package/skills/helm-watchdog/SKILL.md +0 -146
- package/skills/source-check/SKILL.md +0 -143
- package/skills/zendesk-cli/SKILL.md +0 -37
- package/skills/zendesk-kg/SKILL.md +0 -120
|
@@ -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
|
|
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
|
-
|
|
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, "
|
|
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, "
|
|
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
|
-
|
|
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: "
|
|
84
|
+
name: "zendesk",
|
|
64
85
|
label: "Zendesk access",
|
|
65
86
|
level: "core",
|
|
66
87
|
};
|
|
67
|
-
|
|
68
|
-
|
|
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: "
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
115
|
+
catch (e) {
|
|
116
|
+
const msg = e.message || String(e);
|
|
82
117
|
return {
|
|
83
118
|
...base,
|
|
84
119
|
status: "auth_error",
|
|
85
|
-
hint:
|
|
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
|
-
|
|
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
|
|
102
|
-
if (
|
|
148
|
+
const cfg = resolveKgConfig();
|
|
149
|
+
if (!cfg) {
|
|
103
150
|
return {
|
|
104
151
|
...base,
|
|
105
152
|
status: "missing",
|
|
106
|
-
hint: "
|
|
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(),
|
|
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
|
+
}
|