@tplog/pi-zendy 0.2.17

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 ADDED
@@ -0,0 +1,64 @@
1
+ # zendy
2
+
3
+ Pi package for Dify Enterprise support ticket analysis. Analyze Zendesk tickets with natural language — from ticket metadata to Helm chart values to source code.
4
+
5
+ Powered by [pi](https://pi.dev).
6
+
7
+ [中文](docs/README.zh.md) | [日本語](docs/README.ja.md)
8
+
9
+ ## What it does
10
+
11
+ zendy bundles support skills and extensions into a Pi package:
12
+
13
+ - **zcli** — Pull Zendesk ticket metadata and comment threads
14
+ - **helm-watchdog** — Query Dify Helm chart values, images, and validation for any version
15
+ - **source-check** — Clone and analyze Dify source code (enterprise backend/frontend, open-source core, plugin daemon, sandbox)
16
+
17
+ Typical workflow:
18
+
19
+ ```
20
+ "Analyze ticket #1959" → pull ticket + comments → identify version →
21
+ pull Helm chart values → analyze config → (if needed) clone source →
22
+ synthesize findings → draft reply
23
+ ```
24
+
25
+ ## Prerequisites
26
+
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
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pi install npm:@tplog/pi-zendy
35
+ ```
36
+
37
+ Or clone and build from source:
38
+
39
+ ```bash
40
+ git clone git@github.com:tplog/pi-zendy.git
41
+ cd zendy
42
+ npm install
43
+ npm run build
44
+ npm link
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ After installing the Pi package, start pi normally:
50
+
51
+ ```bash
52
+ pi
53
+ ```
54
+
55
+ You can also install the optional legacy launcher:
56
+
57
+ ```bash
58
+ npm install -g @tplog/pi-zendy
59
+ zendy
60
+ ```
61
+
62
+ ## How it works
63
+
64
+ As a Pi package, zendy contributes its skills and extensions through the package manifest. The legacy `zendy` launcher still exists for compatibility and starts `pi` with zendy's bundled resources while keeping user/global extensions available.
package/agents.md ADDED
@@ -0,0 +1,118 @@
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.
@@ -0,0 +1 @@
1
+ export declare function runCleanupCLI(argv: string[]): Promise<number>;
@@ -0,0 +1,217 @@
1
+ // CLI entry for `zendy cleanup-src`.
2
+ // Invoked from src/index.ts when the first user arg is "cleanup-src".
3
+ import { createInterface } from "node:readline";
4
+ import { scanCandidates, classifyCandidates, deleteCandidates, findOrphanSessionDirs, formatBytes, formatAge, } from "./source-cleanup.js";
5
+ const USAGE = `
6
+ Usage: zendy cleanup-src [options]
7
+
8
+ Wipe Dify source clones and orphan zendy session dirs from /tmp.
9
+
10
+ Defaults are aggressive by design: dify-enterprise is a private repo, and
11
+ every extra minute of on-disk residency is an extra minute of exposure.
12
+
13
+ Options:
14
+ --dry-run Show what would be deleted; delete nothing.
15
+ --yes, -y Skip the confirmation prompt in interactive mode.
16
+ --days N Only delete dirs older than N days.
17
+ (Without this flag, all matching dirs are deleted.)
18
+ --all Delete all matching dirs regardless of age (default).
19
+ --keep-last N Keep the N most-recent matching dirs. Default: 0.
20
+ --min-age-minutes N Never delete dirs modified within last N minutes.
21
+ Default: 5 (protects an actively-running clone).
22
+ --pattern P1,P2 Comma-separated prefix patterns (end with *).
23
+ Default: "dify-*,zendy-session-*".
24
+ --dir PATH Base directory to scan. Default: /tmp.
25
+ --no-sweep-orphans Skip the orphan-session sweep (orphan = session dir
26
+ whose pid is no longer alive).
27
+ -h, --help Show this help.
28
+
29
+ Examples:
30
+ # Preview what would be deleted right now:
31
+ zendy cleanup-src --dry-run
32
+
33
+ # Cron/launchd safe invocation (non-interactive, only older than 1 day):
34
+ zendy cleanup-src --days 1 --yes
35
+ `;
36
+ function parseArgs(argv) {
37
+ const opts = {
38
+ dryRun: false,
39
+ yes: false,
40
+ all: true,
41
+ keepLast: 0,
42
+ minAgeMinutes: 5,
43
+ patterns: ["dify-*", "zendy-session-*"],
44
+ dir: "/tmp",
45
+ sweepOrphans: true,
46
+ };
47
+ for (let i = 0; i < argv.length; i++) {
48
+ const a = argv[i];
49
+ switch (a) {
50
+ case "-h":
51
+ case "--help":
52
+ return { kind: "help" };
53
+ case "--dry-run":
54
+ opts.dryRun = true;
55
+ break;
56
+ case "-y":
57
+ case "--yes":
58
+ opts.yes = true;
59
+ break;
60
+ case "--all":
61
+ opts.all = true;
62
+ opts.days = undefined;
63
+ break;
64
+ case "--no-sweep-orphans":
65
+ opts.sweepOrphans = false;
66
+ break;
67
+ case "--days": {
68
+ const v = argv[++i];
69
+ const n = v !== undefined ? parseInt(v, 10) : NaN;
70
+ if (Number.isNaN(n) || n < 0)
71
+ return { kind: "error", message: `Invalid --days value: ${v}` };
72
+ opts.days = n;
73
+ opts.all = false;
74
+ break;
75
+ }
76
+ case "--keep-last": {
77
+ const v = argv[++i];
78
+ const n = v !== undefined ? parseInt(v, 10) : NaN;
79
+ if (Number.isNaN(n) || n < 0)
80
+ return { kind: "error", message: `Invalid --keep-last value: ${v}` };
81
+ opts.keepLast = n;
82
+ break;
83
+ }
84
+ case "--min-age-minutes": {
85
+ const v = argv[++i];
86
+ const n = v !== undefined ? parseInt(v, 10) : NaN;
87
+ if (Number.isNaN(n) || n < 0)
88
+ return { kind: "error", message: `Invalid --min-age-minutes value: ${v}` };
89
+ opts.minAgeMinutes = n;
90
+ break;
91
+ }
92
+ case "--pattern": {
93
+ const v = argv[++i];
94
+ if (!v)
95
+ return { kind: "error", message: "--pattern requires a value" };
96
+ opts.patterns = v.split(",").map((s) => s.trim()).filter(Boolean);
97
+ if (opts.patterns.length === 0)
98
+ return { kind: "error", message: "--pattern list cannot be empty" };
99
+ break;
100
+ }
101
+ case "--dir": {
102
+ const v = argv[++i];
103
+ if (!v)
104
+ return { kind: "error", message: "--dir requires a path" };
105
+ opts.dir = v;
106
+ break;
107
+ }
108
+ default:
109
+ return { kind: "error", message: `Unknown option: ${a}` };
110
+ }
111
+ }
112
+ return { kind: "ok", opts };
113
+ }
114
+ function promptYesNo(question) {
115
+ return new Promise((resolve) => {
116
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
117
+ rl.question(question, (answer) => {
118
+ rl.close();
119
+ resolve(/^y(es)?$/i.test(answer.trim()));
120
+ });
121
+ });
122
+ }
123
+ export async function runCleanupCLI(argv) {
124
+ const parsed = parseArgs(argv);
125
+ if (parsed.kind === "help") {
126
+ console.log(USAGE.trim());
127
+ return 0;
128
+ }
129
+ if (parsed.kind === "error") {
130
+ console.error(`Error: ${parsed.message}`);
131
+ console.error(USAGE.trim());
132
+ return 2;
133
+ }
134
+ const opts = parsed.opts;
135
+ const nowMs = Date.now();
136
+ const maxAgeMs = opts.all ? undefined : (opts.days ?? 0) * 86_400_000;
137
+ const minAgeMs = opts.minAgeMinutes * 60_000;
138
+ console.log(`Scanning ${opts.dir} for: ${opts.patterns.join(", ")}`);
139
+ const candidates = await scanCandidates(opts.dir, opts.patterns);
140
+ let orphanPaths = [];
141
+ if (opts.sweepOrphans) {
142
+ orphanPaths = await findOrphanSessionDirs(opts.dir);
143
+ }
144
+ const protectedPaths = [];
145
+ const zendySrcDir = process.env["ZENDY_SRC_DIR"];
146
+ if (zendySrcDir)
147
+ protectedPaths.push(zendySrcDir);
148
+ const classified = classifyCandidates(candidates, {
149
+ nowMs,
150
+ minAgeMs,
151
+ maxAgeMs,
152
+ keepLast: opts.keepLast,
153
+ protectedPaths,
154
+ });
155
+ // Orphans are always eligible for deletion — their owning process is gone.
156
+ // Merge them in, but never override a min-age or current-session protection.
157
+ const toDeleteByPath = new Map();
158
+ for (const c of classified.toDelete)
159
+ toDeleteByPath.set(c.path, c);
160
+ const protectedSet = new Set(classified.protected.map((c) => c.path));
161
+ for (const pp of protectedPaths)
162
+ protectedSet.add(pp);
163
+ for (const orphanPath of orphanPaths) {
164
+ if (protectedSet.has(orphanPath))
165
+ continue;
166
+ if (toDeleteByPath.has(orphanPath))
167
+ continue;
168
+ const cand = candidates.find((c) => c.path === orphanPath);
169
+ if (cand)
170
+ toDeleteByPath.set(cand.path, cand);
171
+ }
172
+ const toDelete = Array.from(toDeleteByPath.values());
173
+ console.log(``);
174
+ console.log(`Found ${candidates.length} matching director${candidates.length === 1 ? "y" : "ies"}:`);
175
+ for (const c of [...candidates].sort((a, b) => b.mtimeMs - a.mtimeMs)) {
176
+ const age = formatAge(nowMs - c.mtimeMs);
177
+ const size = formatBytes(c.sizeBytes);
178
+ let tag = "";
179
+ if (protectedPaths.includes(c.path))
180
+ tag = " [protected: current session]";
181
+ else if (classified.protected.includes(c))
182
+ tag = " [protected: within min-age]";
183
+ else if (classified.kept.includes(c))
184
+ tag = " [kept]";
185
+ else if (toDeleteByPath.has(c.path)) {
186
+ tag = orphanPaths.includes(c.path) ? " [delete: orphan session]" : " [delete]";
187
+ }
188
+ console.log(` ${c.path} ${age} ${size}${tag}`);
189
+ }
190
+ if (toDelete.length === 0) {
191
+ console.log(``);
192
+ console.log(`Nothing to delete.`);
193
+ return 0;
194
+ }
195
+ const totalBytes = toDelete.reduce((a, b) => a + b.sizeBytes, 0);
196
+ console.log(``);
197
+ console.log(`Will delete ${toDelete.length} director${toDelete.length === 1 ? "y" : "ies"}, freeing ${formatBytes(totalBytes)}.`);
198
+ if (opts.dryRun) {
199
+ console.log(``);
200
+ console.log(`[--dry-run] Nothing actually deleted. Re-run without --dry-run to delete.`);
201
+ return 0;
202
+ }
203
+ if (!opts.yes && process.stdin.isTTY) {
204
+ const ok = await promptYesNo(`Proceed? [y/N] `);
205
+ if (!ok) {
206
+ console.log(`Cancelled.`);
207
+ return 0;
208
+ }
209
+ }
210
+ const result = await deleteCandidates(toDelete);
211
+ console.log(``);
212
+ console.log(`Deleted ${result.deleted.length} director${result.deleted.length === 1 ? "y" : "ies"}, freed ${formatBytes(result.bytesFreed)}.`);
213
+ for (const e of result.errors) {
214
+ console.error(` ! ${e.path}: ${e.error}`);
215
+ }
216
+ return result.errors.length > 0 ? 1 : 0;
217
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
3
+ import { join, dirname } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { execFileSync, spawn } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+ import { createRequire } from "node:module";
8
+ import { createHash } from "node:crypto";
9
+ import { runPreflight, printPreflightReport, promptCoreMissing } from "./preflight.js";
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ const require = createRequire(import.meta.url);
13
+ const pkg = require("../package.json");
14
+ const VERSION = pkg.version;
15
+ // Resolve package root (one level up from dist/)
16
+ const PKG_ROOT = join(__dirname, "..");
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");
26
+ function cacheDir() {
27
+ return join(homedir(), ".zendy", VERSION);
28
+ }
29
+ // All source files that get extracted to cache
30
+ 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,
39
+ ];
40
+ function contentHash() {
41
+ const hash = createHash("sha256");
42
+ for (const f of SOURCE_FILES) {
43
+ hash.update(readFileSync(f, "utf-8"));
44
+ }
45
+ return hash.digest("hex").slice(0, 16);
46
+ }
47
+ function ensureExtracted(base) {
48
+ const marker = join(base, ".extracted");
49
+ const currentHash = contentHash();
50
+ // Re-extract if marker is missing or hash has changed
51
+ if (existsSync(marker) && readFileSync(marker, "utf-8").trim() === currentHash) {
52
+ return;
53
+ }
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
+ const extDir = join(base, "extensions");
66
+ 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"));
71
+ writeFileSync(marker, currentHash);
72
+ }
73
+ function findPi() {
74
+ try {
75
+ return execFileSync("which", ["pi"], { encoding: "utf-8" }).trim();
76
+ }
77
+ catch {
78
+ process.stderr.write("Error: `pi` not found in PATH.\n" +
79
+ "Install it with: npm install -g @earendil-works/pi-coding-agent\n");
80
+ process.exit(1);
81
+ }
82
+ }
83
+ async function main() {
84
+ const userArgs = process.argv.slice(2);
85
+ // Intercept --version / -V / -v before passing to pi.
86
+ // pi also recognises -v as a version flag, so without this interception
87
+ // `zendy -v` would fall through and print pi's version, not zendy's.
88
+ if (userArgs.includes("--version") ||
89
+ userArgs.includes("-V") ||
90
+ userArgs.includes("-v")) {
91
+ console.log(`zendy ${VERSION}`);
92
+ return;
93
+ }
94
+ // Intercept the cleanup-src subcommand — runs standalone, without pi.
95
+ if (userArgs[0] === "cleanup-src") {
96
+ const { runCleanupCLI } = await import("./cleanup-src.js");
97
+ const code = await runCleanupCLI(userArgs.slice(1));
98
+ process.exit(code);
99
+ }
100
+ // Intercept the preflight subcommand — runs the same checks as startup,
101
+ // but on demand. `--json` emits machine-readable output (used by the
102
+ // /status slash command extension).
103
+ if (userArgs[0] === "preflight") {
104
+ const wantJson = userArgs.includes("--json");
105
+ const report = await runPreflight();
106
+ if (wantJson) {
107
+ process.stdout.write(JSON.stringify(report) + "\n");
108
+ return;
109
+ }
110
+ printPreflightReport(report);
111
+ if (report.results.every((r) => r.status === "ok")) {
112
+ process.stdout.write("All checks passed.\n");
113
+ }
114
+ return;
115
+ }
116
+ // Skip preflight if user explicitly opts out
117
+ if (!userArgs.includes("--skip-preflight")) {
118
+ const report = await runPreflight();
119
+ printPreflightReport(report);
120
+ if (report.hasFatal) {
121
+ process.exit(1);
122
+ }
123
+ if (report.hasCore) {
124
+ // Only prompt interactively when stdin is a TTY;
125
+ // in pipes / -p mode, just warn on stderr and continue.
126
+ if (process.stdin.isTTY) {
127
+ const shouldContinue = await promptCoreMissing();
128
+ if (!shouldContinue) {
129
+ process.exit(0);
130
+ }
131
+ }
132
+ }
133
+ }
134
+ // Remove --skip-preflight before passing to pi
135
+ const filteredArgs = userArgs.filter((a) => a !== "--skip-preflight");
136
+ const base = cacheDir();
137
+ ensureExtracted(base);
138
+ const pi = findPi();
139
+ const skillsDir = join(base, "skills");
140
+ const agentsPath = join(base, "agents.md");
141
+ const extensionsDir = join(base, "extensions");
142
+ const skillNames = ["zendesk-cli", "helm-watchdog", "source-check", "zendesk-kg"];
143
+ const args = [
144
+ "--no-skills",
145
+ "--extension",
146
+ join(extensionsDir, "custom-header.ts"),
147
+ "--extension",
148
+ join(extensionsDir, "source-cleanup.ts"),
149
+ "--extension",
150
+ join(extensionsDir, "status.ts"),
151
+ ];
152
+ for (const name of skillNames) {
153
+ args.push("--skill", join(skillsDir, name));
154
+ }
155
+ args.push("--append-system-prompt", agentsPath);
156
+ // Pass through all user arguments
157
+ args.push(...filteredArgs);
158
+ // Spawn pi, replacing this process (inherit stdio for interactive use)
159
+ const child = spawn(pi, args, {
160
+ stdio: "inherit",
161
+ });
162
+ child.on("error", (err) => {
163
+ process.stderr.write(`Failed to exec pi: ${err.message}\n`);
164
+ process.exit(1);
165
+ });
166
+ child.on("exit", (code, signal) => {
167
+ if (signal) {
168
+ process.kill(process.pid, signal);
169
+ }
170
+ else {
171
+ process.exit(code ?? 1);
172
+ }
173
+ });
174
+ }
175
+ main().catch((err) => {
176
+ process.stderr.write(`zendy: ${err.message}\n`);
177
+ process.exit(1);
178
+ });
@@ -0,0 +1,22 @@
1
+ export type CheckLevel = "fatal" | "core" | "enhanced";
2
+ export type CheckStatus = "ok" | "missing" | "auth_error";
3
+ export interface CheckResult {
4
+ name: string;
5
+ label: string;
6
+ level: CheckLevel;
7
+ status: CheckStatus;
8
+ hint: string;
9
+ }
10
+ export interface PreflightReport {
11
+ results: CheckResult[];
12
+ hasFatal: boolean;
13
+ hasCore: boolean;
14
+ hasEnhanced: boolean;
15
+ }
16
+ export declare function runPreflight(): Promise<PreflightReport>;
17
+ export declare function printPreflightReport(report: PreflightReport): void;
18
+ /**
19
+ * Interactive prompt for core dependency failures.
20
+ * Returns true if the user wants to continue, false to exit.
21
+ */
22
+ export declare function promptCoreMissing(): Promise<boolean>;