@operatorkit/hs 0.1.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 +135 -0
- package/bin/hs.js +61 -0
- package/bin/install.js +168 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @operatorkit/hs
|
|
2
|
+
|
|
3
|
+
A command-line interface for the [HelpScout](https://www.helpscout.com/) API. Manage mailboxes, conversations, customers, tags, users, workflows, webhooks, and knowledge base content from the terminal.
|
|
4
|
+
|
|
5
|
+
Ships with an embedded [MCP](https://modelcontextprotocol.io/) server for AI-assisted workflows.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Run directly (no install)
|
|
11
|
+
npx -y @operatorkit/hs version
|
|
12
|
+
|
|
13
|
+
# Global install
|
|
14
|
+
npm i -g @operatorkit/hs
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
On install, the matching platform binary is downloaded from GitHub Releases. Supported: `linux`, `darwin`, `windows` on `amd64`/`arm64`.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Authenticate (Inbox API — OAuth2 app credentials)
|
|
23
|
+
npx -y @operatorkit/hs inbox auth login
|
|
24
|
+
|
|
25
|
+
# Authenticate (Docs API — API key)
|
|
26
|
+
npx -y @operatorkit/hs docs auth login
|
|
27
|
+
|
|
28
|
+
# List mailboxes
|
|
29
|
+
npx -y @operatorkit/hs inbox mailboxes list
|
|
30
|
+
|
|
31
|
+
# List conversations
|
|
32
|
+
npx -y @operatorkit/hs inbox conversations list --status active
|
|
33
|
+
|
|
34
|
+
# Search articles
|
|
35
|
+
npx -y @operatorkit/hs docs articles search --query "getting started"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## API coverage
|
|
39
|
+
|
|
40
|
+
### Inbox API (`hs inbox ...`)
|
|
41
|
+
|
|
42
|
+
Full CRUD for all Inbox API resources:
|
|
43
|
+
|
|
44
|
+
- **Conversations** — list, get, create, update, delete, tags, custom fields, snooze
|
|
45
|
+
- **Threads** — list, reply, note, create (chat/customer/phone), update, source, source-rfc822
|
|
46
|
+
- **Customers** — list, get, create, update, overwrite, delete
|
|
47
|
+
- **Mailboxes** — list, get, folders, custom fields, routing
|
|
48
|
+
- **Tags** — list, get
|
|
49
|
+
- **Users** — list, get, me, delete, status
|
|
50
|
+
- **Teams** — list, members
|
|
51
|
+
- **Organizations** — list, get, create, update, delete, conversations, customers, properties
|
|
52
|
+
- **Workflows** — list, run, update-status
|
|
53
|
+
- **Webhooks** — list, get, create, update, delete
|
|
54
|
+
- **Saved Replies** — list, get, create, update, delete
|
|
55
|
+
- **Reports** — chats, company, conversations, customers, docs, email, productivity, ratings, users
|
|
56
|
+
- **Properties** — customer properties, conversation properties
|
|
57
|
+
- **Ratings** — get
|
|
58
|
+
- **Attachments** — upload, list, get, delete
|
|
59
|
+
|
|
60
|
+
### Docs API (`hs docs ...`)
|
|
61
|
+
|
|
62
|
+
Full CRUD for all Docs API resources:
|
|
63
|
+
|
|
64
|
+
- **Articles** — list, search, get, create, update, delete, upload, revisions, drafts, related, view count
|
|
65
|
+
- **Categories** — list, get, create, update, reorder, delete
|
|
66
|
+
- **Collections** — list, get, create, update, delete
|
|
67
|
+
- **Sites** — list, get, create, update, delete, restrictions
|
|
68
|
+
- **Redirects** — list, get, find, create, update, delete
|
|
69
|
+
- **Assets** — article upload, settings upload
|
|
70
|
+
|
|
71
|
+
## MCP server
|
|
72
|
+
|
|
73
|
+
Start the embedded MCP server for AI-assisted workflows:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx -y @operatorkit/hs mcp -t stdio
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
124 MCP tools auto-discovered from the command tree (85 inbox + 39 docs). Management commands (`auth`, `config`, `permissions`) are excluded.
|
|
80
|
+
|
|
81
|
+
### MCP client config
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"mcpServers": {
|
|
86
|
+
"helpscout": {
|
|
87
|
+
"command": "npx",
|
|
88
|
+
"args": ["-y", "@operatorkit/hs", "mcp", "-t", "stdio"],
|
|
89
|
+
"env": {
|
|
90
|
+
"HS_INBOX_APP_ID": "your-app-id",
|
|
91
|
+
"HS_INBOX_APP_SECRET": "your-app-secret",
|
|
92
|
+
"HS_DOCS_API_KEY": "your-docs-api-key",
|
|
93
|
+
"HS_INBOX_PERMISSIONS": "*:read"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Output formats
|
|
101
|
+
|
|
102
|
+
| Format | Flag | Description |
|
|
103
|
+
|--------|------|-------------|
|
|
104
|
+
| Table | `--format table` | Human-readable table (default) |
|
|
105
|
+
| JSON | `--format json` | Clean JSON — HAL noise stripped, HTML→markdown, fields normalized |
|
|
106
|
+
| JSON-full | `--format json-full` | Raw API response, pretty-printed |
|
|
107
|
+
| CSV | `--format csv` | RFC 4180 CSV with headers |
|
|
108
|
+
|
|
109
|
+
## Safety features
|
|
110
|
+
|
|
111
|
+
- **PII redaction** — deterministic, layered pipeline (structured fields + free-text + source payloads). Modes: `off`, `customers`, `all`.
|
|
112
|
+
- **Permissions** — allowlist-based `resource:operation` pairs restrict which actions are permitted. Set via `HS_INBOX_PERMISSIONS` / `HS_DOCS_PERMISSIONS`.
|
|
113
|
+
- **Rate limiting** — built-in rate limiters respect API quotas (Inbox: 200/min, Docs: 2000/10min).
|
|
114
|
+
|
|
115
|
+
## Environment variables
|
|
116
|
+
|
|
117
|
+
| Variable | Description |
|
|
118
|
+
|----------|-------------|
|
|
119
|
+
| `HS_INBOX_APP_ID` | Inbox API App ID |
|
|
120
|
+
| `HS_INBOX_APP_SECRET` | Inbox API App Secret |
|
|
121
|
+
| `HS_DOCS_API_KEY` | Docs API key |
|
|
122
|
+
| `HS_FORMAT` | Default output format |
|
|
123
|
+
| `HS_INBOX_PII_MODE` | PII redaction mode: `off`, `customers`, `all` |
|
|
124
|
+
| `HS_INBOX_PERMISSIONS` | Inbox permission policy |
|
|
125
|
+
| `HS_DOCS_PERMISSIONS` | Docs permission policy |
|
|
126
|
+
|
|
127
|
+
## Links
|
|
128
|
+
|
|
129
|
+
- [GitHub](https://github.com/operator-kit/hs-cli)
|
|
130
|
+
- [Issues](https://github.com/operator-kit/hs-cli/issues)
|
|
131
|
+
- [HelpScout API docs](https://developer.helpscout.com/)
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
[MIT](https://github.com/operator-kit/hs-cli/blob/main/LICENSE)
|
package/bin/hs.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { spawn } = require("node:child_process");
|
|
6
|
+
|
|
7
|
+
function resolveTarget() {
|
|
8
|
+
const platformMap = {
|
|
9
|
+
linux: "linux",
|
|
10
|
+
darwin: "darwin",
|
|
11
|
+
win32: "windows"
|
|
12
|
+
};
|
|
13
|
+
const archMap = {
|
|
14
|
+
x64: "amd64",
|
|
15
|
+
arm64: "arm64"
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const os = platformMap[process.platform];
|
|
19
|
+
const arch = archMap[process.arch];
|
|
20
|
+
if (!os || !arch) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Unsupported platform/arch: ${process.platform}/${process.arch}`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return { os, arch };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveBinaryPath() {
|
|
29
|
+
const target = resolveTarget();
|
|
30
|
+
const exe = process.platform === "win32" ? "hs.exe" : "hs";
|
|
31
|
+
return path.join(__dirname, "..", "dist", `${target.os}-${target.arch}`, exe);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function main() {
|
|
35
|
+
const binaryPath = resolveBinaryPath();
|
|
36
|
+
if (!fs.existsSync(binaryPath)) {
|
|
37
|
+
console.error(
|
|
38
|
+
"hs binary is not installed. Reinstall package: npm i -g @operatorkit/hs"
|
|
39
|
+
);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
44
|
+
stdio: "inherit"
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
child.on("error", (err) => {
|
|
48
|
+
console.error(`Failed to start hs: ${err.message}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
child.on("exit", (code, signal) => {
|
|
53
|
+
if (signal) {
|
|
54
|
+
process.kill(process.pid, signal);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
process.exit(code ?? 1);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
main();
|
package/bin/install.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const os = require("node:os");
|
|
6
|
+
const https = require("node:https");
|
|
7
|
+
const { pipeline } = require("node:stream/promises");
|
|
8
|
+
|
|
9
|
+
const AdmZip = require("adm-zip");
|
|
10
|
+
const tar = require("tar");
|
|
11
|
+
|
|
12
|
+
const REPO_OWNER = "operator-kit";
|
|
13
|
+
const REPO_NAME = "hs-cli";
|
|
14
|
+
|
|
15
|
+
function resolveTarget() {
|
|
16
|
+
const platformMap = {
|
|
17
|
+
linux: "linux",
|
|
18
|
+
darwin: "darwin",
|
|
19
|
+
win32: "windows"
|
|
20
|
+
};
|
|
21
|
+
const archMap = {
|
|
22
|
+
x64: "amd64",
|
|
23
|
+
arm64: "arm64"
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const osName = platformMap[process.platform];
|
|
27
|
+
const archName = archMap[process.arch];
|
|
28
|
+
if (!osName || !archName) {
|
|
29
|
+
throw new Error(`Unsupported platform/arch: ${process.platform}/${process.arch}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
os: osName,
|
|
34
|
+
arch: archName,
|
|
35
|
+
archiveExt: osName === "windows" ? ".zip" : ".tar.gz",
|
|
36
|
+
binaryName: osName === "windows" ? "hs.exe" : "hs"
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function packageVersion() {
|
|
41
|
+
const pkgPath = path.join(__dirname, "..", "package.json");
|
|
42
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
43
|
+
return String(pkg.version || "").trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isDevVersion(version) {
|
|
47
|
+
return (
|
|
48
|
+
!version ||
|
|
49
|
+
version === "0.0.0-development" ||
|
|
50
|
+
version === "0.0.0" ||
|
|
51
|
+
version.includes("dev")
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function releaseAssetURL(version, target) {
|
|
56
|
+
const tag = `v${version}`;
|
|
57
|
+
const asset = `hs_${tag}_${target.os}_${target.arch}${target.archiveExt}`;
|
|
58
|
+
return {
|
|
59
|
+
tag,
|
|
60
|
+
asset,
|
|
61
|
+
url: `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${tag}/${asset}`
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function downloadFile(url, destination, redirects = 5) {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
https.get(url, (response) => {
|
|
68
|
+
if (
|
|
69
|
+
response.statusCode &&
|
|
70
|
+
response.statusCode >= 300 &&
|
|
71
|
+
response.statusCode < 400 &&
|
|
72
|
+
response.headers.location
|
|
73
|
+
) {
|
|
74
|
+
if (redirects <= 0) {
|
|
75
|
+
reject(new Error(`Too many redirects while downloading ${url}`));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
response.resume();
|
|
79
|
+
resolve(downloadFile(response.headers.location, destination, redirects - 1));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (response.statusCode !== 200) {
|
|
84
|
+
const code = response.statusCode || "unknown";
|
|
85
|
+
response.resume();
|
|
86
|
+
reject(new Error(`Download failed (${code}): ${url}`));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const file = fs.createWriteStream(destination);
|
|
91
|
+
pipeline(response, file)
|
|
92
|
+
.then(() => resolve())
|
|
93
|
+
.catch(reject);
|
|
94
|
+
}).on("error", reject);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function extractArchive(archivePath, target, distDir) {
|
|
99
|
+
if (target.archiveExt === ".zip") {
|
|
100
|
+
const zip = new AdmZip(archivePath);
|
|
101
|
+
zip.extractAllTo(distDir, true);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
await tar.x({
|
|
105
|
+
file: archivePath,
|
|
106
|
+
cwd: distDir
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function findFile(rootDir, fileName) {
|
|
111
|
+
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
const entryPath = path.join(rootDir, entry.name);
|
|
114
|
+
if (entry.isFile() && entry.name === fileName) {
|
|
115
|
+
return entryPath;
|
|
116
|
+
}
|
|
117
|
+
if (entry.isDirectory()) {
|
|
118
|
+
const nested = findFile(entryPath, fileName);
|
|
119
|
+
if (nested) {
|
|
120
|
+
return nested;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return "";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function install() {
|
|
128
|
+
const version = packageVersion();
|
|
129
|
+
if (isDevVersion(version)) {
|
|
130
|
+
console.log("Skipping hs binary download for development package version.");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const target = resolveTarget();
|
|
135
|
+
const release = releaseAssetURL(version, target);
|
|
136
|
+
const distDir = path.join(__dirname, "..", "dist", `${target.os}-${target.arch}`);
|
|
137
|
+
await fs.promises.mkdir(distDir, { recursive: true });
|
|
138
|
+
|
|
139
|
+
const tempArchivePath = path.join(
|
|
140
|
+
os.tmpdir(),
|
|
141
|
+
`hs-${target.os}-${target.arch}-${Date.now()}${target.archiveExt}`
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
console.log(`Downloading hs ${release.tag} (${target.os}/${target.arch})...`);
|
|
145
|
+
await downloadFile(release.url, tempArchivePath);
|
|
146
|
+
await extractArchive(tempArchivePath, target, distDir);
|
|
147
|
+
fs.unlinkSync(tempArchivePath);
|
|
148
|
+
|
|
149
|
+
const binaryPath = path.join(distDir, target.binaryName);
|
|
150
|
+
if (!fs.existsSync(binaryPath)) {
|
|
151
|
+
const discovered = findFile(distDir, target.binaryName);
|
|
152
|
+
if (!discovered) {
|
|
153
|
+
throw new Error(`Binary ${target.binaryName} not found in downloaded archive.`);
|
|
154
|
+
}
|
|
155
|
+
fs.copyFileSync(discovered, binaryPath);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (target.binaryName === "hs") {
|
|
159
|
+
await fs.promises.chmod(binaryPath, 0o755);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(`Installed hs binary to ${binaryPath}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
install().catch((err) => {
|
|
166
|
+
console.error(`Failed to install hs binary: ${err.message}`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@operatorkit/hs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "HelpScout CLI with embedded MCP server",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/operator-kit/hs-cli",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/operator-kit/hs-cli.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/operator-kit/hs-cli/issues"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"hs": "bin/hs.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"bin",
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"postinstall": "node ./bin/install.js"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"adm-zip": "^0.5.16",
|
|
30
|
+
"tar": "^7.4.3"
|
|
31
|
+
}
|
|
32
|
+
}
|