@hasna/connectors 1.3.44 → 1.3.46

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.
Files changed (114) hide show
  1. package/README.md +25 -0
  2. package/bin/index.js +792 -130
  3. package/bin/mcp.js +438 -52
  4. package/bin/serve.js +438 -52
  5. package/connectors/cloudflare-workers/.env.example +5 -0
  6. package/connectors/cloudflare-workers/AGENTS.md +11 -0
  7. package/connectors/cloudflare-workers/CLAUDE.md +11 -0
  8. package/connectors/cloudflare-workers/README.md +45 -0
  9. package/connectors/cloudflare-workers/package.json +47 -0
  10. package/connectors/cloudflare-workers/src/api/client.test.ts +144 -0
  11. package/connectors/cloudflare-workers/src/api/client.ts +386 -0
  12. package/connectors/cloudflare-workers/src/api/index.ts +1 -0
  13. package/connectors/cloudflare-workers/src/cli/index.ts +206 -0
  14. package/connectors/cloudflare-workers/src/index.ts +16 -0
  15. package/connectors/cloudflare-workers/src/types/index.ts +53 -0
  16. package/connectors/cloudflare-workers/src/utils/config.ts +61 -0
  17. package/connectors/cloudflare-workers/src/utils/output.ts +26 -0
  18. package/connectors/cloudflare-workers/tsconfig.build.json +4 -0
  19. package/connectors/cloudflare-workers/tsconfig.json +14 -0
  20. package/connectors/tidio/.env.example +3 -2
  21. package/connectors/tidio/CLAUDE.md +21 -96
  22. package/connectors/tidio/README.md +22 -18
  23. package/connectors/tidio/package.json +4 -2
  24. package/connectors/tidio/src/api/client.ts +26 -15
  25. package/connectors/tidio/src/api/index.ts +185 -128
  26. package/connectors/tidio/src/api/tidio.test.ts +175 -44
  27. package/connectors/tidio/src/cli/index.ts +154 -388
  28. package/connectors/tidio/src/index.ts +5 -3
  29. package/connectors/tidio/src/types/index.ts +135 -82
  30. package/connectors/tidio/src/utils/config.ts +90 -44
  31. package/connectors/wandb/.env.example +7 -0
  32. package/connectors/wandb/AGENTS.md +43 -0
  33. package/connectors/wandb/CLAUDE.md +67 -0
  34. package/connectors/wandb/README.md +65 -0
  35. package/connectors/wandb/package.json +52 -0
  36. package/connectors/wandb/src/api/client.test.ts +116 -0
  37. package/connectors/wandb/src/api/client.ts +83 -0
  38. package/connectors/wandb/src/api/graphql.ts +9 -0
  39. package/connectors/wandb/src/api/index.ts +36 -0
  40. package/connectors/wandb/src/api/projects.ts +36 -0
  41. package/connectors/wandb/src/api/viewer.ts +20 -0
  42. package/connectors/wandb/src/cli/index.ts +265 -0
  43. package/connectors/wandb/src/index.ts +22 -0
  44. package/connectors/wandb/src/types/index.ts +75 -0
  45. package/connectors/wandb/src/utils/config.ts +164 -0
  46. package/connectors/wandb/src/utils/output.ts +119 -0
  47. package/connectors/wandb/tsconfig.json +16 -0
  48. package/connectors/weights-biases/.env.example +7 -0
  49. package/connectors/weights-biases/CLAUDE.md +65 -0
  50. package/connectors/weights-biases/README.md +62 -0
  51. package/connectors/weights-biases/package.json +53 -0
  52. package/connectors/weights-biases/src/api/client.test.ts +46 -0
  53. package/connectors/weights-biases/src/api/client.ts +119 -0
  54. package/connectors/weights-biases/src/api/events.ts +18 -0
  55. package/connectors/weights-biases/src/api/index.ts +56 -0
  56. package/connectors/weights-biases/src/api/runs.ts +28 -0
  57. package/connectors/weights-biases/src/api/search.ts +10 -0
  58. package/connectors/weights-biases/src/cli/index.ts +289 -0
  59. package/connectors/weights-biases/src/index.ts +19 -0
  60. package/connectors/weights-biases/src/types/index.ts +92 -0
  61. package/connectors/weights-biases/src/utils/config.ts +168 -0
  62. package/connectors/weights-biases/src/utils/output.ts +116 -0
  63. package/connectors/weights-biases/tsconfig.json +16 -0
  64. package/connectors/zoominfo/.env.example +4 -0
  65. package/connectors/zoominfo/AGENTS.md +26 -0
  66. package/connectors/zoominfo/CLAUDE.md +20 -0
  67. package/connectors/zoominfo/README.md +69 -0
  68. package/connectors/zoominfo/package.json +52 -0
  69. package/connectors/zoominfo/src/api/client.test.ts +140 -0
  70. package/connectors/zoominfo/src/api/client.ts +228 -0
  71. package/connectors/zoominfo/src/api/index.ts +1 -0
  72. package/connectors/zoominfo/src/cli/index.ts +179 -0
  73. package/connectors/zoominfo/src/index.ts +3 -0
  74. package/connectors/zoominfo/src/types/index.ts +39 -0
  75. package/connectors/zoominfo/src/utils/config.ts +61 -0
  76. package/connectors/zoominfo/src/utils/output.ts +27 -0
  77. package/connectors/zoominfo/tsconfig.json +20 -0
  78. package/connectors/zotero/.env.example +7 -0
  79. package/connectors/zotero/.npmrc.example +2 -0
  80. package/connectors/zotero/AGENTS.md +40 -0
  81. package/connectors/zotero/CLAUDE.md +69 -0
  82. package/connectors/zotero/README.md +91 -0
  83. package/connectors/zotero/package.json +43 -0
  84. package/connectors/zotero/src/api/attachments.ts +130 -0
  85. package/connectors/zotero/src/api/client.test.ts +166 -0
  86. package/connectors/zotero/src/api/client.ts +169 -0
  87. package/connectors/zotero/src/api/collections.ts +26 -0
  88. package/connectors/zotero/src/api/index.ts +45 -0
  89. package/connectors/zotero/src/api/items.ts +64 -0
  90. package/connectors/zotero/src/cli/index.ts +386 -0
  91. package/connectors/zotero/src/index.ts +28 -0
  92. package/connectors/zotero/src/types/index.ts +129 -0
  93. package/connectors/zotero/src/utils/config.ts +245 -0
  94. package/connectors/zotero/src/utils/output.ts +119 -0
  95. package/connectors/zotero/tsconfig.json +16 -0
  96. package/connectors/zymbly/.env.example +2 -0
  97. package/connectors/zymbly/AGENTS.md +11 -0
  98. package/connectors/zymbly/CLAUDE.md +5 -0
  99. package/connectors/zymbly/README.md +35 -0
  100. package/connectors/zymbly/package.json +52 -0
  101. package/connectors/zymbly/src/api/client.test.ts +110 -0
  102. package/connectors/zymbly/src/api/client.ts +166 -0
  103. package/connectors/zymbly/src/api/index.ts +1 -0
  104. package/connectors/zymbly/src/cli/index.ts +156 -0
  105. package/connectors/zymbly/src/index.ts +12 -0
  106. package/connectors/zymbly/src/types/index.ts +34 -0
  107. package/connectors/zymbly/src/utils/config.ts +52 -0
  108. package/connectors/zymbly/src/utils/output.ts +17 -0
  109. package/connectors/zymbly/tsconfig.json +13 -0
  110. package/dist/index.js +45 -2
  111. package/dist/lib/compact-output.d.ts +40 -0
  112. package/dist/mcp/tools/jobs.d.ts +32 -0
  113. package/dist/mcp/tools/jobs.test.d.ts +1 -0
  114. package/package.json +3 -2
@@ -0,0 +1,245 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ import type { CliConfig, LibraryType } from '../types';
5
+
6
+ const CONNECTOR_NAME = 'connect-zotero';
7
+ const DEFAULT_PROFILE = 'default';
8
+ const CURRENT_PROFILE_FILE = 'current_profile';
9
+ const PROFILES_DIR = 'profiles';
10
+
11
+ let profileOverride: string | undefined;
12
+
13
+ const BASE_CONFIG_DIR = join(homedir(), '.hasna', 'connectors', CONNECTOR_NAME);
14
+
15
+ export function setProfileOverride(profile: string | undefined): void {
16
+ profileOverride = profile;
17
+ }
18
+
19
+ export function getProfileOverride(): string | undefined {
20
+ return profileOverride;
21
+ }
22
+
23
+ function ensureBaseConfigDir(): void {
24
+ if (!existsSync(BASE_CONFIG_DIR)) {
25
+ mkdirSync(BASE_CONFIG_DIR, { recursive: true });
26
+ }
27
+ }
28
+
29
+ function getProfilesDir(): string {
30
+ return join(BASE_CONFIG_DIR, PROFILES_DIR);
31
+ }
32
+
33
+ function getCurrentProfileFile(): string {
34
+ return join(BASE_CONFIG_DIR, CURRENT_PROFILE_FILE);
35
+ }
36
+
37
+ export function getCurrentProfile(): string {
38
+ if (profileOverride) {
39
+ return profileOverride;
40
+ }
41
+
42
+ ensureBaseConfigDir();
43
+
44
+ const profilesDir = getProfilesDir();
45
+ if (!existsSync(profilesDir)) {
46
+ mkdirSync(profilesDir, { recursive: true });
47
+ }
48
+
49
+ const currentProfileFile = getCurrentProfileFile();
50
+ if (existsSync(currentProfileFile)) {
51
+ try {
52
+ const profile = readFileSync(currentProfileFile, 'utf-8').trim();
53
+ if (profile && profileExists(profile)) {
54
+ return profile;
55
+ }
56
+ } catch {
57
+ // Fall through to default
58
+ }
59
+ }
60
+
61
+ return DEFAULT_PROFILE;
62
+ }
63
+
64
+ export function setCurrentProfile(profile: string): void {
65
+ ensureBaseConfigDir();
66
+
67
+ if (!profileExists(profile) && profile !== DEFAULT_PROFILE) {
68
+ throw new Error(`Profile "${profile}" does not exist. Create it first with "profile create ${profile}"`);
69
+ }
70
+
71
+ writeFileSync(getCurrentProfileFile(), profile);
72
+ }
73
+
74
+ export function profileExists(profile: string): boolean {
75
+ const profileDir = join(getProfilesDir(), profile);
76
+ return existsSync(profileDir);
77
+ }
78
+
79
+ export function listProfiles(): string[] {
80
+ ensureBaseConfigDir();
81
+
82
+ const profilesDir = getProfilesDir();
83
+ if (!existsSync(profilesDir)) {
84
+ return [];
85
+ }
86
+
87
+ return readdirSync(profilesDir, { withFileTypes: true })
88
+ .filter(dirent => dirent.isDirectory())
89
+ .map(dirent => dirent.name)
90
+ .sort();
91
+ }
92
+
93
+ export function createProfile(profile: string): void {
94
+ ensureBaseConfigDir();
95
+
96
+ if (profileExists(profile)) {
97
+ throw new Error(`Profile "${profile}" already exists`);
98
+ }
99
+
100
+ if (!/^[a-zA-Z0-9_-]+$/.test(profile)) {
101
+ throw new Error('Profile name can only contain letters, numbers, hyphens, and underscores');
102
+ }
103
+
104
+ const profileDir = join(getProfilesDir(), profile);
105
+ mkdirSync(profileDir, { recursive: true });
106
+ }
107
+
108
+ export function deleteProfile(profile: string): void {
109
+ if (profile === DEFAULT_PROFILE) {
110
+ throw new Error('Cannot delete the default profile');
111
+ }
112
+
113
+ if (!profileExists(profile)) {
114
+ throw new Error(`Profile "${profile}" does not exist`);
115
+ }
116
+
117
+ const currentProfile = getCurrentProfile();
118
+ if (currentProfile === profile) {
119
+ setCurrentProfile(DEFAULT_PROFILE);
120
+ }
121
+
122
+ const profileDir = join(getProfilesDir(), profile);
123
+ rmSync(profileDir, { recursive: true });
124
+ }
125
+
126
+ function getProfileDir(): string {
127
+ ensureBaseConfigDir();
128
+
129
+ const profilesDir = getProfilesDir();
130
+ if (!existsSync(profilesDir)) {
131
+ mkdirSync(profilesDir, { recursive: true });
132
+ }
133
+
134
+ const profile = getCurrentProfile();
135
+ const profileDir = join(profilesDir, profile);
136
+
137
+ if (!existsSync(profileDir)) {
138
+ mkdirSync(profileDir, { recursive: true });
139
+ }
140
+
141
+ return profileDir;
142
+ }
143
+
144
+ export function getConfigDir(): string {
145
+ return getProfileDir();
146
+ }
147
+
148
+ export function getBaseConfigDir(): string {
149
+ return BASE_CONFIG_DIR;
150
+ }
151
+
152
+ export function ensureConfigDir(): void {
153
+ getProfileDir();
154
+ }
155
+
156
+ export function loadConfig(): CliConfig {
157
+ ensureConfigDir();
158
+ const configFile = join(getProfileDir(), 'config.json');
159
+
160
+ if (!existsSync(configFile)) {
161
+ return {};
162
+ }
163
+
164
+ try {
165
+ const content = readFileSync(configFile, 'utf-8');
166
+ return JSON.parse(content) as CliConfig;
167
+ } catch {
168
+ return {};
169
+ }
170
+ }
171
+
172
+ export function saveConfig(config: CliConfig): void {
173
+ ensureConfigDir();
174
+ const configFile = join(getProfileDir(), 'config.json');
175
+ writeFileSync(configFile, JSON.stringify(config, null, 2));
176
+ }
177
+
178
+ export function getApiKey(): string | undefined {
179
+ return process.env.ZOTERO_API_KEY || loadConfig().apiKey;
180
+ }
181
+
182
+ export function setApiKey(apiKey: string): void {
183
+ const config = loadConfig();
184
+ config.apiKey = apiKey;
185
+ saveConfig(config);
186
+ }
187
+
188
+ export function getLibraryId(): string | undefined {
189
+ return process.env.ZOTERO_LIBRARY_ID || loadConfig().libraryId;
190
+ }
191
+
192
+ export function setLibraryId(libraryId: string): void {
193
+ const config = loadConfig();
194
+ config.libraryId = libraryId;
195
+ saveConfig(config);
196
+ }
197
+
198
+ export function getLibraryType(): LibraryType {
199
+ const raw = process.env.ZOTERO_LIBRARY_TYPE || loadConfig().libraryType || 'users';
200
+ return raw === 'groups' || raw === 'group' ? 'groups' : 'users';
201
+ }
202
+
203
+ export function setLibraryType(libraryType: LibraryType | 'group'): void {
204
+ const config = loadConfig();
205
+ config.libraryType = libraryType;
206
+ saveConfig(config);
207
+ }
208
+
209
+ export function getBaseUrl(): string | undefined {
210
+ return process.env.ZOTERO_BASE_URL || loadConfig().baseUrl;
211
+ }
212
+
213
+ export function setBaseUrl(baseUrl: string): void {
214
+ const config = loadConfig();
215
+ config.baseUrl = baseUrl;
216
+ saveConfig(config);
217
+ }
218
+
219
+ export function clearConfig(): void {
220
+ saveConfig({});
221
+ }
222
+
223
+ export function isAuthenticated(): boolean {
224
+ const apiKey = getApiKey();
225
+ const libraryId = getLibraryId();
226
+ return Boolean(apiKey && libraryId);
227
+ }
228
+
229
+ export function getActiveProfileName(): string {
230
+ return getCurrentProfile();
231
+ }
232
+
233
+ export function getZoteroConfigFromEnv(): {
234
+ apiKey?: string;
235
+ libraryId?: string;
236
+ libraryType: LibraryType;
237
+ baseUrl?: string;
238
+ } {
239
+ return {
240
+ apiKey: getApiKey(),
241
+ libraryId: getLibraryId(),
242
+ libraryType: getLibraryType(),
243
+ baseUrl: getBaseUrl(),
244
+ };
245
+ }
@@ -0,0 +1,119 @@
1
+ import chalk from 'chalk';
2
+
3
+ export type OutputFormat = 'json' | 'table' | 'pretty';
4
+
5
+ export function formatOutput(data: unknown, format: OutputFormat = 'pretty'): string {
6
+ switch (format) {
7
+ case 'json':
8
+ return JSON.stringify(data, null, 2);
9
+ case 'table':
10
+ return formatAsTable(data);
11
+ case 'pretty':
12
+ default:
13
+ return formatPretty(data);
14
+ }
15
+ }
16
+
17
+ function formatAsTable(data: unknown): string {
18
+ if (!Array.isArray(data)) {
19
+ data = [data];
20
+ }
21
+
22
+ const items = data as Record<string, unknown>[];
23
+ if (items.length === 0) {
24
+ return 'No data';
25
+ }
26
+
27
+ const firstItem = items[0];
28
+ if (!firstItem || typeof firstItem !== 'object') {
29
+ return 'No data';
30
+ }
31
+
32
+ const keys = Object.keys(firstItem);
33
+ const colWidths = keys.map(key => {
34
+ const maxValue = Math.max(
35
+ key.length,
36
+ ...items.map(item => String(item[key] ?? '').length)
37
+ );
38
+ return Math.min(maxValue, 40);
39
+ });
40
+
41
+ const header = keys.map((key, i) => key.padEnd(colWidths[i] ?? 10)).join(' | ');
42
+ const separator = colWidths.map(w => '-'.repeat(w)).join('-+-');
43
+
44
+ const rows = items.map(item =>
45
+ keys.map((key, i) => {
46
+ const value = String(item[key] ?? '');
47
+ const width = colWidths[i] ?? 10;
48
+ return value.length > width
49
+ ? value.substring(0, width - 3) + '...'
50
+ : value.padEnd(width);
51
+ }).join(' | ')
52
+ );
53
+
54
+ return [header, separator, ...rows].join('\n');
55
+ }
56
+
57
+ function formatPretty(data: unknown): string {
58
+ if (Array.isArray(data)) {
59
+ return data.map((item, i) => `${chalk.cyan(`[${i + 1}]`)} ${formatPrettyItem(item)}`).join('\n\n');
60
+ }
61
+ return formatPrettyItem(data);
62
+ }
63
+
64
+ function formatPrettyItem(item: unknown, indent = 0): string {
65
+ if (item === null || item === undefined) {
66
+ return chalk.gray('null');
67
+ }
68
+
69
+ if (typeof item !== 'object') {
70
+ return String(item);
71
+ }
72
+
73
+ const spaces = ' '.repeat(indent);
74
+ const entries = Object.entries(item as Record<string, unknown>);
75
+
76
+ return entries
77
+ .map(([key, value]) => {
78
+ if (Array.isArray(value)) {
79
+ if (value.length === 0) {
80
+ return `${spaces}${chalk.blue(key)}: ${chalk.gray('[]')}`;
81
+ }
82
+ if (typeof value[0] === 'object') {
83
+ return `${spaces}${chalk.blue(key)}:\n${value.map(v => formatPrettyItem(v, indent + 1)).join('\n')}`;
84
+ }
85
+ return `${spaces}${chalk.blue(key)}: ${value.join(', ')}`;
86
+ }
87
+
88
+ if (typeof value === 'object' && value !== null) {
89
+ return `${spaces}${chalk.blue(key)}:\n${formatPrettyItem(value, indent + 1)}`;
90
+ }
91
+
92
+ return `${spaces}${chalk.blue(key)}: ${chalk.white(String(value))}`;
93
+ })
94
+ .join('\n');
95
+ }
96
+
97
+ export function success(message: string): void {
98
+ console.log(chalk.green('OK'), message);
99
+ }
100
+
101
+ export function error(message: string): void {
102
+ console.error(chalk.red('Error:'), message);
103
+ }
104
+
105
+ export function warn(message: string): void {
106
+ console.warn(chalk.yellow('Warning:'), message);
107
+ }
108
+
109
+ export function info(message: string): void {
110
+ console.log(chalk.blue('Info:'), message);
111
+ }
112
+
113
+ export function heading(message: string): void {
114
+ console.log(chalk.bold.cyan(`\n${message}\n`));
115
+ }
116
+
117
+ export function print(data: unknown, format: OutputFormat = 'pretty'): void {
118
+ console.log(formatOutput(data, format));
119
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "declaration": true,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src",
12
+ "types": ["bun-types"]
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist", "bin"]
16
+ }
@@ -0,0 +1,2 @@
1
+ ZYMBLY_API_KEY=REPLACE_ME
2
+ ZYMBLY_BASE_URL=https://api.zymbly.com/v1
@@ -0,0 +1,11 @@
1
+ # Zymbly Connector
2
+
3
+ Follow the repository root agent instructions. This connector uses the Zymbly REST API and must not add browser automation dependencies.
4
+
5
+ ## Checks
6
+
7
+ ```bash
8
+ bun run typecheck
9
+ bun test
10
+ bun run ./src/cli/index.ts --help
11
+ ```
@@ -0,0 +1,5 @@
1
+ # Zymbly Connector
2
+
3
+ This package implements a first-class Zymbly aircraft maintenance REST API connector.
4
+
5
+ Do not commit real Zymbly API keys. Use `ZYMBLY_API_KEY`.
@@ -0,0 +1,35 @@
1
+ # @hasna/connect-zymbly
2
+
3
+ Zymbly REST API connector for aircraft maintenance work orders, parts search, maintenance notes, and scoped raw API calls.
4
+
5
+ This package was rebuilt from the public Zymbly API contract and open-connectors patterns. It is not a copy of internal platform code.
6
+
7
+ ## Configure
8
+
9
+ ```bash
10
+ export ZYMBLY_API_KEY=REPLACE_ME
11
+ export ZYMBLY_BASE_URL=https://api.zymbly.com/v1
12
+ ```
13
+
14
+ ## CLI
15
+
16
+ ```bash
17
+ bun run ./src/cli/index.ts work-orders list --query '{"status":"open","limit":25}'
18
+ bun run ./src/cli/index.ts work-orders get WO-123
19
+ bun run ./src/cli/index.ts parts search --query '{"q":"brake pad"}'
20
+ bun run ./src/cli/index.ts notes create WO-123 --note "Completed 100-hour inspection"
21
+ bun run ./src/cli/index.ts raw GET /aircraft/N12345/inspections
22
+ ```
23
+
24
+ ## Programmatic Usage
25
+
26
+ ```ts
27
+ import { ZymblyClient } from "@hasna/connect-zymbly";
28
+
29
+ const client = new ZymblyClient({ apiKey: process.env.ZYMBLY_API_KEY });
30
+ await client.listWorkOrders({ status: "open" });
31
+ ```
32
+
33
+ ## Security
34
+
35
+ Do not commit real Zymbly API keys. Use placeholder values in `.env.example` only.
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@hasna/connect-zymbly",
3
+ "version": "0.0.1",
4
+ "description": "Zymbly aircraft maintenance API connector for work orders, parts search, maintenance notes, and raw API calls",
5
+ "type": "module",
6
+ "bin": {
7
+ "connect-zymbly": "./bin/index.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "scripts": {
18
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun && bun build ./src/cli/index.ts --outdir ./bin --target bun",
19
+ "dev": "bun run ./src/cli/index.ts",
20
+ "typecheck": "tsc --noEmit",
21
+ "test": "bun test",
22
+ "prepublishOnly": "bun run build"
23
+ },
24
+ "keywords": [
25
+ "zymbly",
26
+ "aircraft",
27
+ "maintenance",
28
+ "cmms",
29
+ "work-orders",
30
+ "aviation",
31
+ "connector",
32
+ "cli",
33
+ "typescript",
34
+ "bun"
35
+ ],
36
+ "author": "Hasna",
37
+ "license": "Apache-2.0",
38
+ "devDependencies": {
39
+ "@types/bun": "latest",
40
+ "typescript": "^5"
41
+ },
42
+ "dependencies": {
43
+ "commander": "^12.1.0"
44
+ },
45
+ "engines": {
46
+ "bun": ">=1.0.0"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/hasna/open-connectors.git"
51
+ }
52
+ }
@@ -0,0 +1,110 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+ import { ZymblyClient, ZymblyApiError } from "../index";
3
+
4
+ const originalFetch = globalThis.fetch;
5
+ let fetchMock: ReturnType<typeof mock>;
6
+
7
+ function mockJsonResponse(body: unknown, init: ResponseInit = {}) {
8
+ return new Response(JSON.stringify(body), {
9
+ status: init.status ?? 200,
10
+ headers: { "content-type": "application/json", ...init.headers },
11
+ });
12
+ }
13
+
14
+ function client() {
15
+ return new ZymblyClient({
16
+ apiKey: "test-api-key",
17
+ baseUrl: "https://api.zymbly.com/v1",
18
+ });
19
+ }
20
+
21
+ beforeEach(() => {
22
+ fetchMock = mock(async () => mockJsonResponse({ ok: true }));
23
+ globalThis.fetch = fetchMock as unknown as typeof fetch;
24
+ });
25
+
26
+ afterEach(() => {
27
+ globalThis.fetch = originalFetch;
28
+ });
29
+
30
+ describe("ZymblyClient", () => {
31
+ test("lists work orders with bearer auth and query params", async () => {
32
+ const api = client();
33
+
34
+ await api.listWorkOrders({ status: "open", limit: 25 });
35
+
36
+ expect(fetchMock).toHaveBeenCalledTimes(1);
37
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
38
+ expect(url).toBe("https://api.zymbly.com/v1/work-orders?status=open&limit=25");
39
+ expect(init.method).toBe("GET");
40
+ expect((init.headers as Record<string, string>).Authorization).toBe("Bearer test-api-key");
41
+ });
42
+
43
+ test("gets a work order with URL-encoded id", async () => {
44
+ const api = client();
45
+
46
+ await api.getWorkOrder("WO/2026-01");
47
+
48
+ const [url] = fetchMock.mock.calls[0] as [string, RequestInit];
49
+ expect(url).toBe("https://api.zymbly.com/v1/work-orders/WO%2F2026-01");
50
+ });
51
+
52
+ test("searches parts with query passthrough", async () => {
53
+ const api = client();
54
+
55
+ await api.searchParts({ q: "brake pad", aircraft: "N12345" });
56
+
57
+ const [url] = fetchMock.mock.calls[0] as [string, RequestInit];
58
+ expect(url).toBe("https://api.zymbly.com/v1/parts?q=brake+pad&aircraft=N12345");
59
+ });
60
+
61
+ test("creates a maintenance note with note body field", async () => {
62
+ const api = client();
63
+
64
+ await api.createMaintenanceNote("wo-42", "Replaced left main tire");
65
+
66
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
67
+ expect(url).toBe("https://api.zymbly.com/v1/work-orders/wo-42/notes");
68
+ expect(init.method).toBe("POST");
69
+ expect(init.body).toBe(JSON.stringify({ note: "Replaced left main tire" }));
70
+ });
71
+
72
+ test("supports raw relative API paths", async () => {
73
+ const api = client();
74
+
75
+ await api.rawRequest("GET", "/aircraft/N12345/inspections");
76
+
77
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
78
+ expect(url).toBe("https://api.zymbly.com/v1/aircraft/N12345/inspections");
79
+ expect(init.method).toBe("GET");
80
+ });
81
+
82
+ test("rejects absolute URLs and path traversal in raw requests", async () => {
83
+ const api = client();
84
+
85
+ expect(() => api.rawRequest("GET", "https://evil.example/v1/work-orders")).toThrow(
86
+ "Zymbly API paths must be relative",
87
+ );
88
+ expect(() => api.rawRequest("GET", "/work-orders/../secrets")).toThrow(
89
+ "parent-directory segments",
90
+ );
91
+ expect(fetchMock).not.toHaveBeenCalled();
92
+ });
93
+
94
+ test("surfaces API error messages", async () => {
95
+ fetchMock = mock(async () =>
96
+ mockJsonResponse({ message: "invalid api key" }, { status: 401 }),
97
+ );
98
+ globalThis.fetch = fetchMock as unknown as typeof fetch;
99
+
100
+ await expect(client().listWorkOrders()).rejects.toThrow(ZymblyApiError);
101
+ await expect(client().listWorkOrders()).rejects.toThrow("invalid api key");
102
+ });
103
+
104
+ test("requires API key before sending requests", async () => {
105
+ const api = new ZymblyClient({ apiKey: "" });
106
+
107
+ await expect(api.listWorkOrders()).rejects.toThrow("Zymbly API key is required");
108
+ expect(fetchMock).not.toHaveBeenCalled();
109
+ });
110
+ });