@tplog/pi-zendy 0.2.17 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -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 +164 -0
- package/extensions/tools.ts +190 -0
- package/extensions/zendy.ts +140 -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
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# zendy
|
|
2
2
|
|
|
3
|
-
Pi
|
|
3
|
+
Pi extension for Dify Enterprise support ticket analysis. Analyze Zendesk tickets with natural language — from ticket metadata to Helm chart values to source code.
|
|
4
4
|
|
|
5
5
|
Powered by [pi](https://pi.dev).
|
|
6
6
|
|
|
@@ -8,25 +8,23 @@ Powered by [pi](https://pi.dev).
|
|
|
8
8
|
|
|
9
9
|
## What it does
|
|
10
10
|
|
|
11
|
-
zendy
|
|
11
|
+
zendy is a single pi extension that provides:
|
|
12
12
|
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
13
|
+
- **LLM Tools** — Direct API access to Zendesk, Helm Watchdog, and Knowledge Graph. No external CLI dependencies.
|
|
14
|
+
- **Slash Commands** — `/zendy-config` to set up credentials, `/zendy-status` to check connectivity, `/zendy-cleanup` to wipe source clones.
|
|
15
|
+
- **Session Safety** — Automatic workspace isolation and cleanup for source code analysis.
|
|
16
16
|
|
|
17
17
|
Typical workflow:
|
|
18
18
|
|
|
19
19
|
```
|
|
20
|
-
"Analyze ticket #1959" →
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
pi → "Analyze ticket #1959" → agent calls zendy_ticket_get →
|
|
21
|
+
identifies version → agent calls zendy_helm_get →
|
|
22
|
+
synthesizes findings → drafts reply
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
## Prerequisites
|
|
26
26
|
|
|
27
27
|
- [pi](https://pi.dev) installed globally: `npm install -g @earendil-works/pi-coding-agent`
|
|
28
|
-
- Zendesk credentials configured: `zcli configure` or env vars (`ZENDESK_SUBDOMAIN`, `ZENDESK_EMAIL`, `ZENDESK_API_TOKEN`)
|
|
29
|
-
- Git SSH access to private repos (for source-check skill)
|
|
30
28
|
|
|
31
29
|
## Install
|
|
32
30
|
|
|
@@ -34,31 +32,63 @@ synthesize findings → draft reply
|
|
|
34
32
|
pi install npm:@tplog/pi-zendy
|
|
35
33
|
```
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
## Configure
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
npm run build
|
|
44
|
-
npm link
|
|
37
|
+
Start pi and run:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
/zendy-config
|
|
45
41
|
```
|
|
46
42
|
|
|
47
|
-
|
|
43
|
+
This interactively collects Zendesk credentials (subdomain, email, API token) and Knowledge Graph API key.
|
|
48
44
|
|
|
49
|
-
|
|
45
|
+
Alternatively, set environment variables:
|
|
50
46
|
|
|
51
47
|
```bash
|
|
52
|
-
|
|
48
|
+
export ZENDY_ZENDESK_SUBDOMAIN=dify
|
|
49
|
+
export ZENDY_ZENDESK_EMAIL=you@example.com
|
|
50
|
+
export ZENDY_ZENDESK_API_TOKEN=your_token
|
|
51
|
+
export ZENDY_KG_API_KEY=your_kg_key
|
|
53
52
|
```
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
Credentials are stored in `~/.zendy/config.json` (mode 0600). On first run, zendy auto-imports
|
|
55
|
+
from legacy `zcli` and `zendesk-kg` config files if they exist.
|
|
56
|
+
|
|
57
|
+
## Commands
|
|
58
|
+
|
|
59
|
+
| Command | Purpose |
|
|
60
|
+
|---------|---------|
|
|
61
|
+
| `/zendy-config` | Configure Zendesk and KG credentials |
|
|
62
|
+
| `/zendy-status` | Check connectivity to all services |
|
|
63
|
+
| `/zendy-cleanup` | Wipe source clones and orphan session dirs |
|
|
64
|
+
|
|
65
|
+
## Tools
|
|
66
|
+
|
|
67
|
+
The agent can call these tools directly:
|
|
68
|
+
|
|
69
|
+
| Tool | Description |
|
|
70
|
+
|------|-------------|
|
|
71
|
+
| `zendy_ticket_get` | Fetch ticket metadata, comments, and user info |
|
|
72
|
+
| `zendy_ticket_search` | Search live Zendesk tickets |
|
|
73
|
+
| `zendy_helm_get` | Query Helm chart values, images, validation by version |
|
|
74
|
+
| `zendy_kg_search` | Semantic search over historical tickets |
|
|
75
|
+
| `zendy_source_status` | Check source analysis workspace |
|
|
76
|
+
|
|
77
|
+
## Legacy Launcher
|
|
78
|
+
|
|
79
|
+
For users of the old `zendy` CLI:
|
|
56
80
|
|
|
57
81
|
```bash
|
|
58
82
|
npm install -g @tplog/pi-zendy
|
|
59
83
|
zendy
|
|
60
84
|
```
|
|
61
85
|
|
|
86
|
+
The legacy launcher starts pi with the zendy extension and system prompt. It still supports
|
|
87
|
+
`zendy preflight` and `zendy cleanup-src` as standalone subcommands.
|
|
88
|
+
|
|
62
89
|
## How it works
|
|
63
90
|
|
|
64
|
-
|
|
91
|
+
zendy registers as a pi extension package. The extension provides tools (callable by the LLM),
|
|
92
|
+
slash commands (for human engineers), and session lifecycle hooks (workspace creation, cleanup).
|
|
93
|
+
All data access goes through direct REST APIs — no `zcli`, `zendesk-kg`, or other CLI tools
|
|
94
|
+
are required at runtime.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function getVersion(version: string, signal?: AbortSignal): Promise<Record<string, unknown>>;
|
|
2
|
+
export declare function getValues(version: string, signal?: AbortSignal): Promise<string>;
|
|
3
|
+
export declare function getImages(version: string, validate?: boolean, signal?: AbortSignal): Promise<Record<string, unknown>[]>;
|
|
4
|
+
export declare function getValidation(version: string, status?: string, signal?: AbortSignal): Promise<Record<string, unknown>[]>;
|
|
5
|
+
export declare function getLatest(versionOnly?: boolean, signal?: AbortSignal): Promise<string | Record<string, unknown>>;
|
|
6
|
+
export declare function listVersions(signal?: AbortSignal): Promise<Record<string, unknown>[]>;
|
|
7
|
+
export declare function getCache(signal?: AbortSignal): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Direct Helm Watchdog API client. No curl/skill needed.
|
|
2
|
+
const BASE_URL = "https://dify-helm-watchdog.vercel.app/api/v1";
|
|
3
|
+
async function fetchJson(url, signal) {
|
|
4
|
+
const response = await fetch(url, {
|
|
5
|
+
headers: { Accept: "application/json", "User-Agent": "zendy/1.0" },
|
|
6
|
+
signal,
|
|
7
|
+
});
|
|
8
|
+
if (!response.ok) {
|
|
9
|
+
const body = await response.text().catch(() => "");
|
|
10
|
+
throw new Error(`Helm Watchdog error ${response.status}: ${body.slice(0, 500)}`);
|
|
11
|
+
}
|
|
12
|
+
const ct = response.headers.get("content-type") ?? "";
|
|
13
|
+
if (ct.includes("application/json")) {
|
|
14
|
+
return response.json();
|
|
15
|
+
}
|
|
16
|
+
return response.text();
|
|
17
|
+
}
|
|
18
|
+
export async function getVersion(version, signal) {
|
|
19
|
+
return fetchJson(`${BASE_URL}/versions/${encodeURIComponent(version)}`, signal);
|
|
20
|
+
}
|
|
21
|
+
export async function getValues(version, signal) {
|
|
22
|
+
const response = await fetch(`${BASE_URL}/versions/${encodeURIComponent(version)}/values`, {
|
|
23
|
+
headers: { Accept: "text/yaml, text/plain, */*", "User-Agent": "zendy/1.0" },
|
|
24
|
+
signal,
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const body = await response.text().catch(() => "");
|
|
28
|
+
throw new Error(`Helm Watchdog error ${response.status}: ${body.slice(0, 500)}`);
|
|
29
|
+
}
|
|
30
|
+
return response.text();
|
|
31
|
+
}
|
|
32
|
+
export async function getImages(version, validate = false, signal) {
|
|
33
|
+
const params = validate ? "?validate=true" : "";
|
|
34
|
+
return fetchJson(`${BASE_URL}/versions/${encodeURIComponent(version)}/images${params}`, signal);
|
|
35
|
+
}
|
|
36
|
+
export async function getValidation(version, status, signal) {
|
|
37
|
+
const params = status ? `?status=${encodeURIComponent(status)}` : "";
|
|
38
|
+
return fetchJson(`${BASE_URL}/versions/${encodeURIComponent(version)}/validation${params}`, signal);
|
|
39
|
+
}
|
|
40
|
+
export async function getLatest(versionOnly = false, signal) {
|
|
41
|
+
const params = versionOnly ? "?versionOnly=true" : "";
|
|
42
|
+
return fetchJson(`${BASE_URL}/versions/latest${params}`, signal);
|
|
43
|
+
}
|
|
44
|
+
export async function listVersions(signal) {
|
|
45
|
+
return fetchJson(`${BASE_URL}/versions`, signal);
|
|
46
|
+
}
|
|
47
|
+
export async function getCache(signal) {
|
|
48
|
+
return fetchJson(`${BASE_URL}/cache`, signal);
|
|
49
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface KgSearchResult {
|
|
2
|
+
ticketId: string;
|
|
3
|
+
subject: string;
|
|
4
|
+
status: string;
|
|
5
|
+
priority: string;
|
|
6
|
+
quickSummary: string;
|
|
7
|
+
issueSummary: string;
|
|
8
|
+
solutionSummary: string;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
handledBy: string[];
|
|
11
|
+
versions: string[];
|
|
12
|
+
keywords: string[];
|
|
13
|
+
referenceUrls: string[];
|
|
14
|
+
channels: Record<string, unknown>;
|
|
15
|
+
rrfScore: number;
|
|
16
|
+
}
|
|
17
|
+
export interface KgSearchResponse {
|
|
18
|
+
results: KgSearchResult[];
|
|
19
|
+
queryText: string;
|
|
20
|
+
queryVectorDimension: number;
|
|
21
|
+
constraintsApplied: Record<string, unknown>;
|
|
22
|
+
fallbackSuggestion: string | null;
|
|
23
|
+
recommendedLinks: unknown[];
|
|
24
|
+
}
|
|
25
|
+
export interface KgStats {
|
|
26
|
+
tickets: number;
|
|
27
|
+
versions: number;
|
|
28
|
+
keywords: number;
|
|
29
|
+
links: number;
|
|
30
|
+
people: number;
|
|
31
|
+
}
|
|
32
|
+
export interface KgHealth {
|
|
33
|
+
status: string;
|
|
34
|
+
}
|
|
35
|
+
export interface KgSearchFilter {
|
|
36
|
+
versions?: string[];
|
|
37
|
+
priority?: string;
|
|
38
|
+
status?: string;
|
|
39
|
+
handledBy?: string;
|
|
40
|
+
keywords?: string[];
|
|
41
|
+
createdAfter?: string;
|
|
42
|
+
createdBefore?: string;
|
|
43
|
+
channels?: string[];
|
|
44
|
+
}
|
|
45
|
+
export interface KgSearchOptions {
|
|
46
|
+
query: string;
|
|
47
|
+
topK?: number;
|
|
48
|
+
vectorK?: number;
|
|
49
|
+
fulltextK?: number;
|
|
50
|
+
rrfK?: number;
|
|
51
|
+
orderBy?: "rrfScore" | "createdAt" | "priority";
|
|
52
|
+
allowVersionFamilyFallback?: boolean;
|
|
53
|
+
recommendedLinkK?: number;
|
|
54
|
+
useLlmClassification?: boolean;
|
|
55
|
+
filter?: KgSearchFilter;
|
|
56
|
+
}
|
|
57
|
+
export declare function search(options: KgSearchOptions, signal?: AbortSignal): Promise<KgSearchResponse>;
|
|
58
|
+
export declare function getStats(signal?: AbortSignal): Promise<KgStats>;
|
|
59
|
+
export declare function healthCheck(signal?: AbortSignal): Promise<KgHealth>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Direct Zendesk Knowledge Graph API client. No zendesk-kg CLI dependency.
|
|
2
|
+
import { getConfig } from "../config/store.js";
|
|
3
|
+
import { migrateLegacyConfig, getLegacyKgEnv } from "../config/migrate.js";
|
|
4
|
+
import { DEFAULT_KG_API_URL } from "../config/schema.js";
|
|
5
|
+
// ── Resolve config ─────────────────────────────────────────────────────
|
|
6
|
+
function resolveConfig() {
|
|
7
|
+
// Try zendy config first (env or ~/.zendy/config.json)
|
|
8
|
+
const { zendeskKg } = getConfig();
|
|
9
|
+
if (zendeskKg?.apiKey)
|
|
10
|
+
return zendeskKg;
|
|
11
|
+
// Try legacy migration
|
|
12
|
+
migrateLegacyConfig();
|
|
13
|
+
const cfg2 = getConfig();
|
|
14
|
+
if (cfg2.zendeskKg?.apiKey)
|
|
15
|
+
return cfg2.zendeskKg;
|
|
16
|
+
// Try legacy .env directly
|
|
17
|
+
const legacy = getLegacyKgEnv();
|
|
18
|
+
if (legacy?.["RETRIEVER_API_KEY"]) {
|
|
19
|
+
return {
|
|
20
|
+
apiUrl: legacy["RETRIEVER_API_URL"] || undefined,
|
|
21
|
+
apiKey: legacy["RETRIEVER_API_KEY"],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
throw new Error("Zendesk KG not configured. Use /zendy-config to set up credentials.\n" +
|
|
25
|
+
"Or set env: ZENDY_KG_API_KEY");
|
|
26
|
+
}
|
|
27
|
+
// ── HTTP helpers ───────────────────────────────────────────────────────
|
|
28
|
+
async function kgPost(path, body, signal) {
|
|
29
|
+
const cfg = resolveConfig();
|
|
30
|
+
const base = cfg.apiUrl || DEFAULT_KG_API_URL;
|
|
31
|
+
const data = JSON.stringify(body);
|
|
32
|
+
const headers = {
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
Accept: "application/json",
|
|
35
|
+
Authorization: `Bearer ${cfg.apiKey}`,
|
|
36
|
+
"Content-Length": String(Buffer.byteLength(data)),
|
|
37
|
+
"User-Agent": "zendy/1.0",
|
|
38
|
+
};
|
|
39
|
+
if (cfg.apiKey)
|
|
40
|
+
headers["x-api-key"] = cfg.apiKey;
|
|
41
|
+
const response = await fetch(`${base}${path}`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers,
|
|
44
|
+
body: data,
|
|
45
|
+
signal,
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const respBody = await response.text().catch(() => "");
|
|
49
|
+
throw new Error(`KG API error ${response.status}: ${respBody.slice(0, 500)}`);
|
|
50
|
+
}
|
|
51
|
+
return response.json();
|
|
52
|
+
}
|
|
53
|
+
async function kgGet(path, signal) {
|
|
54
|
+
const cfg = resolveConfig();
|
|
55
|
+
const base = cfg.apiUrl || DEFAULT_KG_API_URL;
|
|
56
|
+
const headers = {
|
|
57
|
+
Accept: "application/json",
|
|
58
|
+
Authorization: `Bearer ${cfg.apiKey}`,
|
|
59
|
+
"User-Agent": "zendy/1.0",
|
|
60
|
+
};
|
|
61
|
+
if (cfg.apiKey)
|
|
62
|
+
headers["x-api-key"] = cfg.apiKey;
|
|
63
|
+
const response = await fetch(`${base}${path}`, {
|
|
64
|
+
headers,
|
|
65
|
+
signal,
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
const body = await response.text().catch(() => "");
|
|
69
|
+
throw new Error(`KG API error ${response.status}: ${body.slice(0, 500)}`);
|
|
70
|
+
}
|
|
71
|
+
return response.json();
|
|
72
|
+
}
|
|
73
|
+
// ── Public API ─────────────────────────────────────────────────────────
|
|
74
|
+
export async function search(options, signal) {
|
|
75
|
+
const body = {
|
|
76
|
+
query: options.query,
|
|
77
|
+
topK: options.topK ?? 5,
|
|
78
|
+
vectorK: options.vectorK ?? 10,
|
|
79
|
+
fulltextK: options.fulltextK ?? 10,
|
|
80
|
+
rrfK: options.rrfK ?? 60,
|
|
81
|
+
allowVersionFamilyFallback: options.allowVersionFamilyFallback ?? false,
|
|
82
|
+
recommendedLinkK: options.recommendedLinkK ?? 3,
|
|
83
|
+
useLlmClassification: options.useLlmClassification ?? false,
|
|
84
|
+
};
|
|
85
|
+
if (options.filter && Object.keys(options.filter).length > 0) {
|
|
86
|
+
body.filter = options.filter;
|
|
87
|
+
}
|
|
88
|
+
return kgPost("/v1/retrieval", body, signal);
|
|
89
|
+
}
|
|
90
|
+
export async function getStats(signal) {
|
|
91
|
+
return kgGet("/stats", signal);
|
|
92
|
+
}
|
|
93
|
+
export async function healthCheck(signal) {
|
|
94
|
+
try {
|
|
95
|
+
return await kgGet("/health", signal);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return { status: "unreachable" };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export interface Ticket {
|
|
2
|
+
id: number;
|
|
3
|
+
url: string;
|
|
4
|
+
subject: string;
|
|
5
|
+
description: string;
|
|
6
|
+
status: string;
|
|
7
|
+
priority: string | null;
|
|
8
|
+
created_at: string;
|
|
9
|
+
updated_at: string;
|
|
10
|
+
requester_id: number;
|
|
11
|
+
assignee_id: number | null;
|
|
12
|
+
organization_id: number | null;
|
|
13
|
+
group_id: number | null;
|
|
14
|
+
tags: string[];
|
|
15
|
+
custom_fields: Array<{
|
|
16
|
+
id: number;
|
|
17
|
+
value: string | number | boolean | null;
|
|
18
|
+
}>;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
export interface SlimComment {
|
|
22
|
+
id: number;
|
|
23
|
+
type: string;
|
|
24
|
+
author_id: number;
|
|
25
|
+
body: string;
|
|
26
|
+
html_body: string;
|
|
27
|
+
plain_body: string;
|
|
28
|
+
public: boolean;
|
|
29
|
+
created_at: string;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
export interface User {
|
|
33
|
+
id: number;
|
|
34
|
+
name: string;
|
|
35
|
+
email: string;
|
|
36
|
+
role: string;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
export interface SearchResults {
|
|
40
|
+
results: Array<Record<string, unknown>>;
|
|
41
|
+
count: number;
|
|
42
|
+
next_page: string | null;
|
|
43
|
+
previous_page: string | null;
|
|
44
|
+
}
|
|
45
|
+
export interface TicketResult {
|
|
46
|
+
ticket: Ticket;
|
|
47
|
+
comments?: SlimComment[];
|
|
48
|
+
requester?: User;
|
|
49
|
+
assignee?: User;
|
|
50
|
+
}
|
|
51
|
+
export declare function getTicket(ticketId: number, signal?: AbortSignal): Promise<{
|
|
52
|
+
ticket: Ticket;
|
|
53
|
+
}>;
|
|
54
|
+
export declare function getTicketComments(ticketId: number, signal?: AbortSignal): Promise<{
|
|
55
|
+
comments: SlimComment[];
|
|
56
|
+
}>;
|
|
57
|
+
export declare function getUser(userId: number, signal?: AbortSignal): Promise<{
|
|
58
|
+
user: User;
|
|
59
|
+
}>;
|
|
60
|
+
export declare function searchTickets(query: string, signal?: AbortSignal): Promise<SearchResults>;
|
|
61
|
+
export declare function getMe(signal?: AbortSignal): Promise<{
|
|
62
|
+
user: User;
|
|
63
|
+
}>;
|
|
64
|
+
export declare function getTicketFull(ticketId: number, signal?: AbortSignal): Promise<TicketResult>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Direct Zendesk REST API client. No zcli dependency.
|
|
2
|
+
import { getConfig } from "../config/store.js";
|
|
3
|
+
import { migrateLegacyConfig, getLegacyZendeskConfig } from "../config/migrate.js";
|
|
4
|
+
// ── Resolve config ─────────────────────────────────────────────────────
|
|
5
|
+
function resolveConfig() {
|
|
6
|
+
// Try zendy config first (env or ~/.zendy/config.json)
|
|
7
|
+
const { zendesk } = getConfig();
|
|
8
|
+
if (zendesk?.subdomain && zendesk?.email && zendesk?.apiToken) {
|
|
9
|
+
return zendesk;
|
|
10
|
+
}
|
|
11
|
+
// Try legacy migration
|
|
12
|
+
migrateLegacyConfig();
|
|
13
|
+
const cfg2 = getConfig();
|
|
14
|
+
if (cfg2.zendesk?.subdomain && cfg2.zendesk?.email && cfg2.zendesk?.apiToken) {
|
|
15
|
+
return cfg2.zendesk;
|
|
16
|
+
}
|
|
17
|
+
// Try legacy zcli config directly
|
|
18
|
+
const legacy = getLegacyZendeskConfig();
|
|
19
|
+
if (legacy?.subdomain && legacy?.email && legacy?.api_token) {
|
|
20
|
+
return { subdomain: legacy.subdomain, email: legacy.email, apiToken: legacy.api_token };
|
|
21
|
+
}
|
|
22
|
+
throw new Error("Zendesk not configured. Use /zendy-config to set up credentials.\n" +
|
|
23
|
+
"Or set env: ZENDY_ZENDESK_SUBDOMAIN, ZENDY_ZENDESK_EMAIL, ZENDY_ZENDESK_API_TOKEN");
|
|
24
|
+
}
|
|
25
|
+
// ── HTTP helpers ───────────────────────────────────────────────────────
|
|
26
|
+
function authHeader(cfg) {
|
|
27
|
+
return "Basic " + Buffer.from(`${cfg.email}/token:${cfg.apiToken}`).toString("base64");
|
|
28
|
+
}
|
|
29
|
+
async function zendeskGet(path, signal) {
|
|
30
|
+
const cfg = resolveConfig();
|
|
31
|
+
const url = `https://${cfg.subdomain}.zendesk.com/api/v2${path}`;
|
|
32
|
+
const response = await fetch(url, {
|
|
33
|
+
headers: {
|
|
34
|
+
Authorization: authHeader(cfg),
|
|
35
|
+
Accept: "application/json",
|
|
36
|
+
"User-Agent": "zendy/1.0",
|
|
37
|
+
},
|
|
38
|
+
signal,
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const body = await response.text().catch(() => "");
|
|
42
|
+
throw new Error(`Zendesk API error ${response.status} for ${path}: ${body.slice(0, 500)}`);
|
|
43
|
+
}
|
|
44
|
+
return response.json();
|
|
45
|
+
}
|
|
46
|
+
// ── Public API ─────────────────────────────────────────────────────────
|
|
47
|
+
export async function getTicket(ticketId, signal) {
|
|
48
|
+
return zendeskGet(`/tickets/${ticketId}.json`, signal);
|
|
49
|
+
}
|
|
50
|
+
export async function getTicketComments(ticketId, signal) {
|
|
51
|
+
return zendeskGet(`/tickets/${ticketId}/comments.json`, signal);
|
|
52
|
+
}
|
|
53
|
+
export async function getUser(userId, signal) {
|
|
54
|
+
return zendeskGet(`/users/${userId}.json`, signal);
|
|
55
|
+
}
|
|
56
|
+
export async function searchTickets(query, signal) {
|
|
57
|
+
return zendeskGet(`/search.json?query=${encodeURIComponent(query)}`, signal);
|
|
58
|
+
}
|
|
59
|
+
export async function getMe(signal) {
|
|
60
|
+
return zendeskGet("/users/me.json", signal);
|
|
61
|
+
}
|
|
62
|
+
export async function getTicketFull(ticketId, signal) {
|
|
63
|
+
const { ticket } = await getTicket(ticketId, signal);
|
|
64
|
+
let comments;
|
|
65
|
+
let requester;
|
|
66
|
+
let assignee;
|
|
67
|
+
try {
|
|
68
|
+
({ comments } = await getTicketComments(ticketId, signal));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// comments optional
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
if (ticket.requester_id) {
|
|
75
|
+
({ user: requester } = await getUser(ticket.requester_id, signal));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// users optional
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
if (ticket.assignee_id) {
|
|
83
|
+
({ user: assignee } = await getUser(ticket.assignee_id, signal));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// users optional
|
|
88
|
+
}
|
|
89
|
+
return { ticket, comments, requester, assignee };
|
|
90
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Migrate legacy zcli / zendesk-kg config into ~/.zendy/config.json.
|
|
2
|
+
// Non-destructive: writes to zendy config only if it doesn't already exist.
|
|
3
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { writeConfig, configExists } from "./store.js";
|
|
7
|
+
const LEGACY_ZCLI_PATH = join(homedir(), ".zendesk-cli", "config.json");
|
|
8
|
+
const LEGACY_KG_ENV_PATH = join(homedir(), ".zendesk-kg", ".env");
|
|
9
|
+
function parseLegacyKgEnv() {
|
|
10
|
+
const out = {};
|
|
11
|
+
try {
|
|
12
|
+
if (!existsSync(LEGACY_KG_ENV_PATH))
|
|
13
|
+
return out;
|
|
14
|
+
for (const line of readFileSync(LEGACY_KG_ENV_PATH, "utf-8").split(/\r?\n/)) {
|
|
15
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/);
|
|
16
|
+
if (m)
|
|
17
|
+
out[m[1]] = m[2].replace(/^['"]|['"]$/g, "");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch { /* ignore */ }
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
function parseLegacyZcliConfig() {
|
|
24
|
+
try {
|
|
25
|
+
if (!existsSync(LEGACY_ZCLI_PATH))
|
|
26
|
+
return {};
|
|
27
|
+
const raw = readFileSync(LEGACY_ZCLI_PATH, "utf-8");
|
|
28
|
+
const j = JSON.parse(raw);
|
|
29
|
+
if (j && typeof j === "object")
|
|
30
|
+
return j;
|
|
31
|
+
}
|
|
32
|
+
catch { /* ignore */ }
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
export function migrateLegacyConfig() {
|
|
36
|
+
// Don't overwrite existing zendy config
|
|
37
|
+
if (configExists())
|
|
38
|
+
return null;
|
|
39
|
+
const zcli = parseLegacyZcliConfig();
|
|
40
|
+
const kg = parseLegacyKgEnv();
|
|
41
|
+
const hasZcli = zcli.subdomain && zcli.email && zcli.api_token;
|
|
42
|
+
const hasKg = !!kg["RETRIEVER_API_KEY"];
|
|
43
|
+
if (!hasZcli && !hasKg)
|
|
44
|
+
return null;
|
|
45
|
+
const config = {};
|
|
46
|
+
if (hasZcli) {
|
|
47
|
+
config.zendesk = {
|
|
48
|
+
subdomain: zcli.subdomain,
|
|
49
|
+
email: zcli.email,
|
|
50
|
+
apiToken: zcli.api_token,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (hasKg) {
|
|
54
|
+
config.zendeskKg = {
|
|
55
|
+
apiUrl: kg["RETRIEVER_API_URL"] || undefined,
|
|
56
|
+
apiKey: kg["RETRIEVER_API_KEY"],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
writeConfig(config);
|
|
60
|
+
const sources = [];
|
|
61
|
+
if (hasZcli)
|
|
62
|
+
sources.push("zcli (~/.zendesk-cli/config.json)");
|
|
63
|
+
if (hasKg)
|
|
64
|
+
sources.push("zendesk-kg (~/.zendesk-kg/.env)");
|
|
65
|
+
return { migrated: true, source: sources.join(", ") };
|
|
66
|
+
}
|
|
67
|
+
export function getLegacyZendeskConfig() {
|
|
68
|
+
const zcli = parseLegacyZcliConfig();
|
|
69
|
+
if (zcli.subdomain && zcli.email && zcli.api_token)
|
|
70
|
+
return zcli;
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
export function getLegacyKgEnv() {
|
|
74
|
+
const env = parseLegacyKgEnv();
|
|
75
|
+
if (env["RETRIEVER_API_KEY"])
|
|
76
|
+
return env;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ZendyConfig {
|
|
2
|
+
zendesk?: ZendeskConfig;
|
|
3
|
+
zendeskKg?: ZendeskKgConfig;
|
|
4
|
+
}
|
|
5
|
+
export interface ZendeskConfig {
|
|
6
|
+
subdomain?: string;
|
|
7
|
+
email?: string;
|
|
8
|
+
apiToken?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ZendeskKgConfig {
|
|
11
|
+
apiUrl?: string;
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const DEFAULT_KG_API_URL = "https://zendesk-ticket-retriever.vercel.app";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ZendyConfig, ZendeskConfig, ZendeskKgConfig } from "./schema.js";
|
|
2
|
+
export declare function getConfig(): ZendyConfig;
|
|
3
|
+
export declare function getZendeskConfig(): ZendeskConfig | undefined;
|
|
4
|
+
export declare function getZendeskKgConfig(): ZendeskKgConfig | undefined;
|
|
5
|
+
export declare function writeConfig(config: ZendyConfig): void;
|
|
6
|
+
export declare function configPath(): string;
|
|
7
|
+
export declare function configExists(): boolean;
|