@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,154 @@
1
+ /**
2
+ * Zendy — unified pi extension entry point.
3
+ *
4
+ * Provides:
5
+ * - Tools (LLM-callable): zendy_ticket_get, zendy_ticket_search,
6
+ * zendy_helm_get, zendy_kg_search, zendy_source_status
7
+ * - Slash commands: /zendy-config, /zendy-status
8
+ * - Session lifecycle: workspace dir + cleanup + orphan sweep
9
+ * - Custom header on session start
10
+ *
11
+ * No external CLI dependencies (zcli, zendesk-kg) required.
12
+ * All data access is through direct REST APIs.
13
+ */
14
+
15
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
+ import { registerAllTools } from "./tools.js";
17
+ import { registerAllCommands } from "./commands.js";
18
+ import { mkdirSync, rmSync, readdirSync } from "node:fs";
19
+ import { join } from "node:path";
20
+
21
+ // ── Constants ──────────────────────────────────────────────────────────
22
+
23
+ const BASE_DIR = "/tmp";
24
+ const SESSION_PREFIX = "zendy-session-";
25
+ const WIPE_PREFIXES = ["dify-", SESSION_PREFIX];
26
+
27
+ // ── Session workspace helpers ──────────────────────────────────────────
28
+
29
+ function parseSessionDir(name: string): { pid: number } | null {
30
+ const m = /^zendy-session-(\d+)-(\d+)$/.exec(name);
31
+ return m ? { pid: parseInt(m[1]!, 10) } : null;
32
+ }
33
+
34
+ function isProcessAlive(pid: number): boolean {
35
+ try {
36
+ process.kill(pid, 0);
37
+ return true;
38
+ } catch (e) {
39
+ return (e as NodeJS.ErrnoException).code === "EPERM";
40
+ }
41
+ }
42
+
43
+ function safeRmrf(path: string): boolean {
44
+ try {
45
+ rmSync(path, { recursive: true, force: true });
46
+ return true;
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ function sweepOrphans(base: string, exclude: string): string[] {
53
+ let entries;
54
+ try {
55
+ entries = readdirSync(base, { withFileTypes: true });
56
+ } catch {
57
+ return [];
58
+ }
59
+ const removed: string[] = [];
60
+ for (const e of entries) {
61
+ if (!e.isDirectory()) continue;
62
+ const info = parseSessionDir(e.name);
63
+ if (!info) continue;
64
+ const full = join(base, e.name);
65
+ if (full === exclude) continue;
66
+ if (isProcessAlive(info.pid)) continue;
67
+ if (safeRmrf(full)) removed.push(full);
68
+ }
69
+ return removed;
70
+ }
71
+
72
+ // ── ASCII header ───────────────────────────────────────────────────────
73
+
74
+ const HEADER = `
75
+ ███████╗███████╗███╗ ██╗██████╗ ██╗ ██╗
76
+ ╚══███╔╝██╔════╝████╗ ██║██╔══██╗╚██╗ ██╔╝
77
+ ███╔╝ █████╗ ██╔██╗ ██║██║ ██║ ╚████╔╝
78
+ ███╔╝ ██╔══╝ ██║╚██╗██║██║ ██║ ╚██╔╝
79
+ ███████╗███████╗██║ ╚████║██████╔╝ ██║
80
+ ╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝ ╚═╝
81
+ Dify Enterprise Support Harness
82
+ `;
83
+
84
+ // ── Extension entry ────────────────────────────────────────────────────
85
+
86
+ export default function (pi: ExtensionAPI) {
87
+ // ── Tools & Commands ────────────────────────────────────────────────
88
+
89
+ registerAllTools(pi);
90
+ registerAllCommands(pi);
91
+
92
+ // ── Session lifecycle ───────────────────────────────────────────────
93
+
94
+ const ts = Date.now();
95
+ const sessionDir = join(BASE_DIR, `${SESSION_PREFIX}${process.pid}-${ts}`);
96
+ let cleanedUp = false;
97
+
98
+ function ensureDir(): void {
99
+ try {
100
+ mkdirSync(sessionDir, { recursive: true, mode: 0o700 });
101
+ } catch {
102
+ // non-fatal
103
+ }
104
+ }
105
+
106
+ function cleanupSession(): void {
107
+ if (cleanedUp) return;
108
+ cleanedUp = true;
109
+ safeRmrf(sessionDir);
110
+
111
+ // Do not rely on the human remembering manual cleanup: wipe standalone
112
+ // source clones too when pi exits. The current session dir was already
113
+ // removed above; this covers clones created directly under /tmp/dify-*.
114
+ try {
115
+ const entries = readdirSync(BASE_DIR, { withFileTypes: true });
116
+ for (const e of entries) {
117
+ if (!e.isDirectory()) continue;
118
+ if (!WIPE_PREFIXES.some((p) => e.name.startsWith(p))) continue;
119
+ safeRmrf(join(BASE_DIR, e.name));
120
+ }
121
+ } catch {
122
+ // non-fatal
123
+ }
124
+ }
125
+
126
+ process.on("exit", cleanupSession);
127
+ process.on("SIGINT", () => { cleanupSession(); process.exit(130); });
128
+ process.on("SIGTERM", () => { cleanupSession(); process.exit(143); });
129
+ process.on("uncaughtException", (err) => {
130
+ cleanupSession();
131
+ console.error(err);
132
+ process.exit(1);
133
+ });
134
+
135
+ pi.on("session_start", async (_event, ctx) => {
136
+ ensureDir();
137
+ process.env["ZENDY_SRC_DIR"] = sessionDir;
138
+
139
+ // Custom header
140
+ if (ctx.hasUI) {
141
+ ctx.ui.notify(HEADER, "info");
142
+ }
143
+
144
+ // Sweep orphan session dirs
145
+ const removed = sweepOrphans(BASE_DIR, sessionDir);
146
+ if (ctx.hasUI && removed.length > 0) {
147
+ ctx.ui.notify(`[zendy] swept ${removed.length} orphan session dir(s)`, "info");
148
+ }
149
+ });
150
+
151
+ pi.on("session_shutdown", async () => {
152
+ cleanupSession();
153
+ });
154
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tplog/pi-zendy",
3
- "version": "0.2.17",
3
+ "version": "0.3.1",
4
4
  "description": "Pi package for Dify Enterprise support ticket analysis",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,16 +18,11 @@
18
18
  ],
19
19
  "pi": {
20
20
  "extensions": [
21
- "./extensions"
22
- ],
23
- "skills": [
24
- "./skills"
21
+ "./extensions/zendy.ts"
25
22
  ]
26
23
  },
27
24
  "files": [
28
25
  "dist",
29
- "agents.md",
30
- "skills",
31
26
  "extensions"
32
27
  ],
33
28
  "publishConfig": {
@@ -37,8 +32,7 @@
37
32
  "build": "tsc",
38
33
  "test": "tsc && node --test test/*.test.mjs",
39
34
  "prepare": "tsc",
40
- "prepublishOnly": "tsc",
41
- "postinstall": "command -v zendesk-kg >/dev/null 2>&1 || (curl -fsSL https://raw.githubusercontent.com/sorphwer/zendesk-kg-cli-release/main/install.sh | sh || true)"
35
+ "prepublishOnly": "tsc"
42
36
  },
43
37
  "peerDependencies": {
44
38
  "@earendil-works/pi-coding-agent": "*"
package/agents.md DELETED
@@ -1,118 +0,0 @@
1
- # zendy
2
-
3
- Internal support engineering harness for Dify Enterprise. Powered by pi.
4
-
5
- ## Identity
6
-
7
- zendy is a Pi package that loads a fixed set of skills + extensions on top of [pi](https://pi.dev) so a support engineer can analyze a Zendesk ticket end-to-end without leaving the terminal. The published npm package is `@tplog/pi-zendy`; the source is the private repo `tplog/pi-zendy`.
8
-
9
- ## What zendy ships with
10
-
11
- Three skills, loaded by default:
12
-
13
- - `zendesk-cli` — Pull Zendesk ticket metadata and comment threads via `zcli`
14
- - `helm-watchdog` — Query Dify Helm chart values, images, and validation by version
15
- - `source-check` — Clone and analyze Dify source code (only when the user explicitly asks)
16
-
17
- One additional opt-in skill:
18
-
19
- - `zendesk-kg` — Hybrid retrieval over a Zendesk Knowledge Graph for cross-ticket / similar-issue lookups
20
-
21
- Two extensions:
22
-
23
- - `custom-header` — ZENDY ASCII art on session start
24
- - `source-cleanup` — Per-session `/tmp/zendy-session-<pid>-<ts>/` directory + automatic wipe on exit; orphan sweep on next start
25
-
26
- One slash command beyond what pi gives you: `/status` runs `zendy preflight --json` and renders a per-source connectivity check (pi auth / Zendesk / GitHub / zendesk-kg).
27
-
28
- ## Typical workflow
29
-
30
- 1. User gives a ticket number → use `zcli` to pull ticket + comments
31
- 2. Identify the customer's Dify version from ticket fields
32
- 3. Pull the corresponding Helm chart values via the helm-watchdog API
33
- 4. If the issue requires code-level analysis, the user will say so → use the `source-check` skill
34
- 5. (optional) Use `zendesk-kg search` to find similar past tickets if cross-ticket evidence helps
35
- 6. Synthesize findings and draft a reply in the ticket's language
36
-
37
- ## Behavior rules
38
-
39
- - **Never guess.** Base conclusions on actual data — ticket fields, values.yaml, source code, ticket-graph results. State clearly what is fact vs inference.
40
- - **Don't recommend solutions you haven't verified.** If you haven't confirmed something in the code or docs, say so.
41
- - **Version matters.** Always check the customer's exact Dify version — defaults and behavior change between releases.
42
- - **Reply drafts match the ticket language.** Detect the language from the ticket thread (Japanese, Chinese, or English) and draft in the same language with appropriate business tone.
43
- - **Suggest source-check when confidence is low.** If your analysis cannot be confirmed from config alone, proactively ask the user: "需要我去源码确认吗?" Don't clone without permission, but suggest it instead of leaving uncertainty unresolved.
44
- - **Inspect before filtering.** When calling an unfamiliar API for the first time, look at the response structure (`| jq 'type'`, `| jq '.[0] | keys'`, or `| head`) before writing filter expressions. Never assume field names.
45
- - **Skills are APIs, not CLIs (mostly).** `helm-watchdog` is an HTTP API (use `curl`); `zcli` is a CLI; `zendesk-kg` is a CLI. Read the skill's Quick Reference before your first call. Do not guess command names.
46
- - **Auto-install missing dependencies.** If `zcli` or `zendesk-kg` is not found, run their installer automatically and continue. Don't ask — this is an internal tool, all dependencies are expected. (`zendy` v0.2.13+ also auto-installs `zendesk-kg` via postinstall.)
47
- - **Cite sources when you summarize.** When using `zendesk-kg search` results, surface the original `ticketId`s — the engineer wants to read the source ticket, not just your synthesis.
48
- - **`zendesk-kg health` is currently unreliable.** The retriever's `/health` endpoint sits behind a 302 → login page even though `/search` is API-key-authenticated and works. Don't gate retries on `health`. Only `health: ok` is a real signal; `health: unreachable` is inconclusive.
49
-
50
- ## Development flow
51
-
52
- The development of zendy itself follows three rules. These are non-negotiable.
53
-
54
- 1. **`main` is always the latest source of truth.** All accepted changes land on `main`; no long-lived divergent branches.
55
- 2. **Tags drive releases.** Pushing a `vX.Y.Z` tag triggers `.github/workflows/publish.yml`, which verifies `tag == package.json.version`, runs tests, and `npm publish --access public`. The npm registry is downstream of the tag, never the other way around.
56
- 3. **Every push to `main` goes through a Pull Request.** No direct pushes to `main`, no exceptions. This includes `chore: release X.Y.Z` version-bump commits — they too open a branch + PR before being merged.
57
-
58
- ### Concrete steps for a code change
59
-
60
- ```
61
- 1. Create a branch: git checkout -b <type>/<short-name>
62
- 2. Make the change locally
63
- 3. Verify locally: npm test (or relevant manual checks)
64
- 4. Commit + push the branch: git push -u origin <branch>
65
- 5. Open a PR: gh pr create --title "..." --body "..."
66
- 6. Wait for the human owner's review/merge
67
- 7. After merge, locally: git checkout main && git pull
68
- ```
69
-
70
- Do not bump `package.json.version` in the same PR as a feature/fix unless the human owner asked for it. Version bumps belong in their own PR.
71
-
72
- ### Concrete steps for a release
73
-
74
- ```
75
- 1. main has the merged feature/fix commits you want to ship
76
- 2. Branch: git checkout -b chore/release-X.Y.Z
77
- 3. Bump: edit package.json (version)
78
- npm install --package-lock-only
79
- 4. Commit + push branch: git push -u origin chore/release-X.Y.Z
80
- 5. Open the release PR: gh pr create --title "chore: release X.Y.Z" ...
81
- 6. Wait for merge
82
- 7. After merge, on local main: git checkout main && git pull
83
- git tag vX.Y.Z
84
- git push origin vX.Y.Z
85
- 8. Watch the workflow: gh run watch <id> --exit-status
86
- 9. Verify on npm: npm view @tplog/pi-zendy version
87
- 10. Cut a GitHub Release: gh release create vX.Y.Z --title "..." --notes "..."
88
- 11. Announce in #zendy with the GH Release URL.
89
- ```
90
-
91
- Release timing (whether to tag now vs accumulate more PRs first) is a **human decision**. Agents should not unilaterally tag — ask the human owner before step 7.
92
-
93
- ### Test machine handoff
94
-
95
- The local sandbox where an agent develops is **not** the same as the human's test machine. To let the human verify a not-yet-merged change without going through GitHub auth:
96
-
97
- ```
98
- 1. On the agent machine, after `npm test` passes locally:
99
- npm pack
100
- slock attachment upload --path tplog-zendy-<version>.tgz --channel "#zendy"
101
- 2. Tell the human: `npm install -g <downloaded-tarball-path>`
102
- ```
103
-
104
- This bypasses `npm install -g git+ssh://...#main`, which currently fails because npm 10/11 doesn't install devDependencies during git-dep `prepare`.
105
-
106
- ## Release pitfalls — do not repeat
107
-
108
- - **`--provenance` does NOT work for this package.** npm rejects provenance attestations for private source repos ("Only public source repositories are supported when publishing with provenance"). The workflow intentionally omits `--provenance` and `id-token: write`. Do not add them back.
109
- - **Tags are immutable.** If a publish fails, bump the version and push a new tag — never delete and re-push the same tag. Gaps in the npm version sequence are fine.
110
- - **Tag version must equal `package.json.version`.** The workflow fails fast if they disagree.
111
- - **Do not run `npm publish` locally.** The workflow is the canonical path.
112
- - **Never push `chore: release X.Y.Z` directly to main.** It still goes through a PR (rule 3).
113
-
114
- ## Security note for contributors
115
-
116
- This package is published to npm as-is. Anything written into `skills/**`, `extensions/**`, or `agents.md` becomes public once published. **Never** embed tokens, API keys, passwords, internal-only URLs, customer names, or any non-public information in these files. Runtime configuration (credentials, per-user endpoints) must come from environment variables or user config — never hard-coded.
117
-
118
- The source repo `tplog/pi-zendy` is private by design. The `files` whitelist in `package.json` controls what ships to the public npm tarball. Do not propose making the repo public.
@@ -1,49 +0,0 @@
1
- /**
2
- * Custom Header Extension
3
- *
4
- * Displays "zendy" in ASCII art on startup.
5
- */
6
-
7
- import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
8
-
9
- function getZendyArt(theme: Theme): string[] {
10
- const c = (text: string) => theme.fg("accent", text);
11
- const d = (text: string) => theme.fg("dim", text);
12
- const m = (text: string) => theme.fg("muted", text);
13
-
14
- return [
15
- "",
16
- c("███████╗███████╗███╗ ██╗██████╗ ██╗ ██╗"),
17
- c("╚══███╔╝██╔════╝████╗ ██║██╔══██╗╚██╗ ██╔╝"),
18
- c(" ███╔╝ █████╗ ██╔██╗ ██║██║ ██║ ╚████╔╝"),
19
- c(" ███╔╝ ██╔══╝ ██║╚██╗██║██║ ██║ ╚██╔╝"),
20
- c("███████╗███████╗██║ ╚████║██████╔╝ ██║"),
21
- c("╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝ ╚═╝"),
22
- "",
23
- m("─── ") + d("Dify Enterprise Copilot") + m(" ───"),
24
- "",
25
- ];
26
- }
27
-
28
- export default function (pi: ExtensionAPI) {
29
- pi.on("session_start", async (_event, ctx) => {
30
- if (ctx.hasUI) {
31
- ctx.ui.setHeader((_tui, theme) => {
32
- return {
33
- render(_width: number): string[] {
34
- return getZendyArt(theme);
35
- },
36
- invalidate() {},
37
- };
38
- });
39
- }
40
- });
41
-
42
- pi.registerCommand("restore-header", {
43
- description: "Restore built-in header",
44
- handler: async (_args, ctx) => {
45
- ctx.ui.setHeader(undefined);
46
- ctx.ui.notify("Built-in header restored", "info");
47
- },
48
- });
49
- }
@@ -1,162 +0,0 @@
1
- /**
2
- * Source-clone cleanup extension (deterministic, code-level defense).
3
- *
4
- * dify-enterprise / dify-enterprise-frontend are PRIVATE repositories.
5
- * Relying on skill-level ("please remember to clean up") guidance is
6
- * prompt-level defense — an AI can forget. This extension enforces
7
- * cleanup in code:
8
- *
9
- * - on session_start:
10
- * · create /tmp/zendy-session-<pid>-<ts>/ (mode 0700)
11
- * · export its path via process.env.ZENDY_SRC_DIR so bash
12
- * subprocesses spawned by pi (and the source-check skill)
13
- * clone into it
14
- * · sweep orphan session dirs whose owning pid is dead
15
- *
16
- * - on session_shutdown AND on process exit / SIGINT / SIGTERM /
17
- * uncaughtException:
18
- * · rm -rf this session's dir (idempotent, best-effort)
19
- *
20
- * - /cleanup-src slash command inside pi: wipe all dify-* and
21
- * orphan zendy-session-* dirs on demand.
22
- *
23
- * Caveats (do not oversell):
24
- * - kill -9 / OOM / power loss bypass handlers → rely on the
25
- * next startup's orphan sweep.
26
- * - rm -rf is not secure erase. For at-rest confidentiality,
27
- * disk encryption (FileVault/LUKS) is required.
28
- */
29
-
30
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
31
- import { mkdirSync, rmSync, readdirSync } from "node:fs";
32
- import { join } from "node:path";
33
-
34
- const BASE_DIR = "/tmp";
35
- const SESSION_PREFIX = "zendy-session-";
36
- const WIPE_PREFIXES = ["dify-", SESSION_PREFIX];
37
-
38
- function parseSessionDir(name: string): { pid: number } | null {
39
- const m = /^zendy-session-(\d+)-(\d+)$/.exec(name);
40
- return m ? { pid: parseInt(m[1]!, 10) } : null;
41
- }
42
-
43
- function isAlive(pid: number): boolean {
44
- try {
45
- process.kill(pid, 0);
46
- return true;
47
- } catch (e) {
48
- return (e as NodeJS.ErrnoException).code === "EPERM";
49
- }
50
- }
51
-
52
- function safeRmrf(path: string): boolean {
53
- try {
54
- rmSync(path, { recursive: true, force: true });
55
- return true;
56
- } catch {
57
- return false;
58
- }
59
- }
60
-
61
- function sweepOrphans(base: string, exclude: string): string[] {
62
- let entries;
63
- try {
64
- entries = readdirSync(base, { withFileTypes: true });
65
- } catch {
66
- return [];
67
- }
68
- const removed: string[] = [];
69
- for (const e of entries) {
70
- if (!e.isDirectory()) continue;
71
- const info = parseSessionDir(e.name);
72
- if (!info) continue;
73
- const full = join(base, e.name);
74
- if (full === exclude) continue;
75
- if (isAlive(info.pid)) continue;
76
- if (safeRmrf(full)) removed.push(full);
77
- }
78
- return removed;
79
- }
80
-
81
- export default function (pi: ExtensionAPI) {
82
- const ts = Date.now();
83
- const sessionDir = join(BASE_DIR, `${SESSION_PREFIX}${process.pid}-${ts}`);
84
- let cleanedUp = false;
85
-
86
- function ensureDir(): void {
87
- try {
88
- mkdirSync(sessionDir, { recursive: true, mode: 0o700 });
89
- } catch {
90
- // non-fatal; clone commands will also create subpaths with mkdir -p
91
- }
92
- }
93
-
94
- function cleanupSession(): void {
95
- if (cleanedUp) return;
96
- cleanedUp = true;
97
- safeRmrf(sessionDir);
98
- }
99
-
100
- // Belt-and-suspenders: if session_shutdown doesn't fire (some exit paths
101
- // skip event dispatch), rely on node's process signals. All idempotent.
102
- process.on("exit", cleanupSession);
103
- process.on("SIGINT", () => {
104
- cleanupSession();
105
- process.exit(130);
106
- });
107
- process.on("SIGTERM", () => {
108
- cleanupSession();
109
- process.exit(143);
110
- });
111
- process.on("uncaughtException", (err) => {
112
- cleanupSession();
113
- // Preserve original Node behavior of non-zero exit + stderr trace.
114
- // eslint-disable-next-line no-console
115
- console.error(err);
116
- process.exit(1);
117
- });
118
-
119
- pi.on("session_start", async (_event, ctx) => {
120
- ensureDir();
121
- process.env["ZENDY_SRC_DIR"] = sessionDir;
122
- const removed = sweepOrphans(BASE_DIR, sessionDir);
123
- if (ctx.hasUI && removed.length > 0) {
124
- ctx.ui.notify(
125
- `[source-cleanup] swept ${removed.length} orphan session dir(s)`,
126
- "info",
127
- );
128
- }
129
- });
130
-
131
- pi.on("session_shutdown", async () => {
132
- cleanupSession();
133
- });
134
-
135
- pi.registerCommand("cleanup-src", {
136
- description:
137
- "Wipe all Dify source clones (/tmp/dify-*) and orphan zendy session dirs",
138
- handler: async (_args, ctx) => {
139
- let entries;
140
- try {
141
- entries = readdirSync(BASE_DIR, { withFileTypes: true });
142
- } catch {
143
- ctx.ui.notify(`[source-cleanup] cannot read ${BASE_DIR}`, "error");
144
- return;
145
- }
146
- const removed: string[] = [];
147
- for (const e of entries) {
148
- if (!e.isDirectory()) continue;
149
- if (!WIPE_PREFIXES.some((p) => e.name.startsWith(p))) continue;
150
- const full = join(BASE_DIR, e.name);
151
- if (full === sessionDir) continue; // never nuke our own live session
152
- if (safeRmrf(full)) removed.push(full);
153
- }
154
- ctx.ui.notify(
155
- removed.length === 0
156
- ? `[source-cleanup] nothing to remove`
157
- : `[source-cleanup] removed ${removed.length} dir(s):\n${removed.join("\n")}`,
158
- "info",
159
- );
160
- },
161
- });
162
- }
@@ -1,82 +0,0 @@
1
- /**
2
- * Status slash-command extension.
3
- *
4
- * Registers `/status` inside pi. On invocation it shells out to
5
- * `zendy preflight --json` and renders the result as a single notification.
6
- *
7
- * Why subprocess instead of duplicating check logic here? The canonical
8
- * checks live in `src/preflight.ts` (pi/zcli/github), and the npm-published
9
- * `zendy` binary is in PATH (pi was launched by it). One source of truth,
10
- * trivial extension, ~50ms cold start per invocation — fine for an
11
- * interactive on-demand command.
12
- */
13
-
14
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
15
- import { execFile } from "node:child_process";
16
-
17
- interface CheckResult {
18
- name: string;
19
- label: string;
20
- level: "fatal" | "core" | "enhanced";
21
- status: "ok" | "missing" | "auth_error";
22
- hint: string;
23
- }
24
-
25
- interface PreflightReport {
26
- results: CheckResult[];
27
- hasFatal: boolean;
28
- hasCore: boolean;
29
- hasEnhanced: boolean;
30
- }
31
-
32
- function runZendyPreflight(): Promise<
33
- { ok: true; report: PreflightReport } | { ok: false; error: string }
34
- > {
35
- return new Promise((resolve) => {
36
- execFile(
37
- "zendy",
38
- ["preflight", "--json"],
39
- { timeout: 15000, encoding: "utf-8" },
40
- (err, stdout, stderr) => {
41
- if (err && (err as NodeJS.ErrnoException).code === "ENOENT") {
42
- resolve({ ok: false, error: "`zendy` not found in PATH" });
43
- return;
44
- }
45
- if (err) {
46
- const msg = stderr.trim() || (err as Error).message;
47
- resolve({ ok: false, error: msg });
48
- return;
49
- }
50
- try {
51
- const report = JSON.parse(stdout.trim()) as PreflightReport;
52
- resolve({ ok: true, report });
53
- } catch {
54
- resolve({ ok: false, error: `could not parse preflight JSON: ${stdout.slice(0, 200)}` });
55
- }
56
- },
57
- );
58
- });
59
- }
60
-
61
- function formatLine(r: CheckResult): string {
62
- if (r.status === "ok") return ` ✓ ${r.label}`;
63
- const firstHintLine = r.hint.split("\n")[0] ?? "";
64
- return ` ✗ ${r.label}${firstHintLine ? ` — ${firstHintLine}` : ""}`;
65
- }
66
-
67
- export default function (pi: ExtensionAPI) {
68
- pi.registerCommand("status", {
69
- description: "Show pi / Zendesk / GitHub connection status",
70
- handler: async (_args, ctx) => {
71
- const result = await runZendyPreflight();
72
- if (!result.ok) {
73
- ctx.ui.notify(`[status] could not run preflight: ${result.error}`, "error");
74
- return;
75
- }
76
- const { report } = result;
77
- const body = report.results.map(formatLine).join("\n");
78
- const hasIssue = report.hasFatal || report.hasCore || report.hasEnhanced;
79
- ctx.ui.notify(`zendy status:\n${body}`, hasIssue ? "error" : "info");
80
- },
81
- });
82
- }
@@ -1,24 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { readFileSync } from "node:fs";
3
- import { dirname, join } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- const __dirname = dirname(fileURLToPath(import.meta.url));
7
- const AGENTS_PATH = join(__dirname, "..", "agents.md");
8
-
9
- let cachedContext: string | undefined;
10
-
11
- function loadZendyContext(): string {
12
- if (cachedContext === undefined) {
13
- cachedContext = readFileSync(AGENTS_PATH, "utf-8");
14
- }
15
- return cachedContext;
16
- }
17
-
18
- export default function (pi: ExtensionAPI) {
19
- pi.on("before_agent_start", async (event) => {
20
- return {
21
- systemPrompt: `${event.systemPrompt}\n\n${loadZendyContext()}`,
22
- };
23
- });
24
- }