@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 +64 -0
- package/agents.md +118 -0
- package/dist/cleanup-src.d.ts +1 -0
- package/dist/cleanup-src.js +217 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +178 -0
- package/dist/preflight.d.ts +22 -0
- package/dist/preflight.js +224 -0
- package/dist/source-cleanup.d.ts +43 -0
- package/dist/source-cleanup.js +188 -0
- package/extensions/custom-header.ts +49 -0
- package/extensions/source-cleanup.ts +162 -0
- package/extensions/status.ts +82 -0
- package/extensions/zendy-context.ts +24 -0
- package/package.json +55 -0
- package/skills/helm-watchdog/SKILL.md +146 -0
- package/skills/source-check/SKILL.md +143 -0
- package/skills/zendesk-cli/SKILL.md +37 -0
- package/skills/zendesk-kg/SKILL.md +120 -0
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
|
+
}
|
package/dist/index.d.ts
ADDED
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>;
|