@yawlabs/mcp 0.58.0 → 0.58.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,381 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/team-sync.ts
4
+ import { existsSync } from "fs";
5
+ import { chmod, readFile, unlink as unlink2 } from "fs/promises";
6
+ import { homedir as homedir2 } from "os";
7
+ import { join } from "path";
8
+
9
+ // src/atomic-write.ts
10
+ import { mkdir, rename, unlink, writeFile } from "fs/promises";
11
+ import path from "path";
12
+ async function atomicWriteFile(filePath, contents, encoding = "utf8") {
13
+ const dir = path.dirname(filePath);
14
+ const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
15
+ await mkdir(dir, { recursive: true });
16
+ try {
17
+ await writeFile(tmp, contents, encoding);
18
+ await rename(tmp, filePath);
19
+ } catch (err) {
20
+ await unlink(tmp).catch(() => void 0);
21
+ throw err;
22
+ }
23
+ }
24
+
25
+ // src/logger.ts
26
+ var LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
27
+ var minLevel = LOG_LEVELS[process.env.LOG_LEVEL?.toLowerCase()] ?? LOG_LEVELS.info;
28
+ function log(level, msg, data) {
29
+ if (LOG_LEVELS[level] < minLevel) return;
30
+ const entry = JSON.stringify({ level, msg, ts: (/* @__PURE__ */ new Date()).toISOString(), ...data });
31
+ process.stderr.write(`${entry}
32
+ `);
33
+ }
34
+
35
+ // src/paths.ts
36
+ import { access } from "fs/promises";
37
+ import { homedir } from "os";
38
+ import path2 from "path";
39
+ function cacheDir() {
40
+ if (process.platform === "win32") {
41
+ const localAppData = process.env.LOCALAPPDATA;
42
+ const base = localAppData && localAppData.length > 0 ? localAppData : path2.join(homedir(), "AppData", "Local");
43
+ return path2.join(base, "yaw-mcp", "Cache");
44
+ }
45
+ if (process.platform === "darwin") {
46
+ return path2.join(homedir(), "Library", "Caches", "yaw-mcp");
47
+ }
48
+ const xdg = process.env.XDG_CACHE_HOME;
49
+ return path2.join(xdg && xdg.length > 0 ? xdg : path2.join(homedir(), ".cache"), "yaw-mcp");
50
+ }
51
+ var CONFIG_DIRNAME = ".yaw-mcp";
52
+ function userConfigDir(home = homedir()) {
53
+ return path2.join(home, CONFIG_DIRNAME);
54
+ }
55
+ async function findProjectConfigDir(start, home = homedir()) {
56
+ const homeResolved = path2.resolve(home);
57
+ let dir = path2.resolve(start);
58
+ let prev = "";
59
+ while (dir !== prev) {
60
+ if (dir === homeResolved) return null;
61
+ const candidate = path2.join(dir, CONFIG_DIRNAME);
62
+ try {
63
+ await access(candidate);
64
+ return candidate;
65
+ } catch {
66
+ }
67
+ prev = dir;
68
+ dir = path2.dirname(dir);
69
+ }
70
+ return null;
71
+ }
72
+ var GUIDE_FILENAME = "YAW-MCP.md";
73
+ function guidePath(configDir) {
74
+ return path2.join(configDir, GUIDE_FILENAME);
75
+ }
76
+
77
+ // src/team-sync.ts
78
+ var BASE_URL_DEFAULT = "https://yaw.sh";
79
+ var COOKIE_NAME = "yaw_team";
80
+ var REQUEST_TIMEOUT_MS = 15e3;
81
+ var SESSION_STATE_FILENAME = "team-session.json";
82
+ var TeamSyncAuthError = class extends Error {
83
+ constructor(message = "Not signed in.") {
84
+ super(message);
85
+ this.name = "TeamSyncAuthError";
86
+ }
87
+ };
88
+ var TeamSyncForbiddenError = class extends Error {
89
+ constructor(message = "You do not have permission to edit this resource.") {
90
+ super(message);
91
+ this.name = "TeamSyncForbiddenError";
92
+ }
93
+ };
94
+ var TeamSyncStaleVersionError = class extends Error {
95
+ currentVersion;
96
+ constructor(currentVersion) {
97
+ super("Your copy is out of date. Pull the latest version and retry.");
98
+ this.name = "TeamSyncStaleVersionError";
99
+ this.currentVersion = currentVersion;
100
+ }
101
+ };
102
+ function sessionStatePath(home = homedir2()) {
103
+ return join(home, CONFIG_DIRNAME, SESSION_STATE_FILENAME);
104
+ }
105
+ function resolveBaseUrl(env = process.env) {
106
+ const fromEnv = env.YAW_MCP_TEAM_BASE_URL;
107
+ return fromEnv && fromEnv.length > 0 ? fromEnv.replace(/\/$/, "") : BASE_URL_DEFAULT;
108
+ }
109
+ function expMs(session) {
110
+ const e = session.exp;
111
+ return e > 0 && e < 1e12 ? e * 1e3 : e;
112
+ }
113
+ var cachedState = null;
114
+ function invalidateState() {
115
+ cachedState = null;
116
+ }
117
+ async function loadStoredState(filePath) {
118
+ if (cachedState) {
119
+ const s = cachedState.state;
120
+ if (s && expMs(s.session) < Date.now()) {
121
+ cachedState = { state: null };
122
+ return null;
123
+ }
124
+ return s;
125
+ }
126
+ let parsed;
127
+ try {
128
+ const raw = await readFile(filePath, "utf8");
129
+ const obj = JSON.parse(raw);
130
+ if (!obj || typeof obj !== "object") parsed = null;
131
+ else if (typeof obj.cookie !== "string" || !obj.cookie) parsed = null;
132
+ else if (!obj.session || typeof obj.session !== "object") parsed = null;
133
+ else parsed = obj;
134
+ } catch {
135
+ parsed = null;
136
+ }
137
+ if (parsed && expMs(parsed.session) < Date.now()) {
138
+ cachedState = { state: null };
139
+ return null;
140
+ }
141
+ cachedState = { state: parsed };
142
+ return parsed;
143
+ }
144
+ async function saveStoredState(filePath, state) {
145
+ cachedState = { state };
146
+ try {
147
+ await atomicWriteFile(filePath, JSON.stringify(state, null, 2));
148
+ if (process.platform !== "win32") {
149
+ try {
150
+ await chmod(filePath, 384);
151
+ } catch {
152
+ }
153
+ }
154
+ } catch (err) {
155
+ log("warn", "Failed to persist team session", {
156
+ error: err instanceof Error ? err.message : String(err)
157
+ });
158
+ }
159
+ }
160
+ async function clearStoredState(filePath) {
161
+ invalidateState();
162
+ try {
163
+ if (existsSync(filePath)) await unlink2(filePath);
164
+ } catch {
165
+ }
166
+ }
167
+ function parseOneSetCookie(line) {
168
+ const semi = line.indexOf(";");
169
+ const pair = semi >= 0 ? line.slice(0, semi) : line;
170
+ const eq = pair.indexOf("=");
171
+ if (eq < 1) return null;
172
+ return { name: pair.slice(0, eq).trim(), value: pair.slice(eq + 1).trim() };
173
+ }
174
+ function parseSetCookie(headers) {
175
+ const getAll = headers.getSetCookie;
176
+ if (typeof getAll === "function") {
177
+ for (const line of getAll.call(headers)) {
178
+ const parsed2 = parseOneSetCookie(line);
179
+ if (parsed2 && parsed2.name === COOKIE_NAME) return parsed2.value;
180
+ }
181
+ return null;
182
+ }
183
+ const headerValue = headers.get("set-cookie");
184
+ if (!headerValue) return null;
185
+ const parsed = parseOneSetCookie(headerValue);
186
+ return parsed && parsed.name === COOKIE_NAME ? parsed.value : null;
187
+ }
188
+ async function httpJson(opts) {
189
+ const baseUrl = opts.baseUrl ?? resolveBaseUrl();
190
+ const headers = { "Content-Type": "application/json" };
191
+ if (opts.cookie) headers.Cookie = `${COOKIE_NAME}=${opts.cookie}`;
192
+ const res = await fetch(`${baseUrl}${opts.path}`, {
193
+ method: opts.method,
194
+ headers,
195
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
196
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
197
+ });
198
+ let body;
199
+ try {
200
+ body = await res.json();
201
+ } catch {
202
+ body = {};
203
+ }
204
+ const cookie = parseSetCookie(res.headers);
205
+ return { status: res.status, body, cookie };
206
+ }
207
+ async function signIn(key, opts = {}) {
208
+ const trimmed = key.trim();
209
+ if (!trimmed) throw new Error("License key is required.");
210
+ const baseUrl = opts.baseUrl;
211
+ const post = await httpJson({
212
+ method: "POST",
213
+ path: "/api/team/session",
214
+ body: { key: trimmed },
215
+ baseUrl
216
+ });
217
+ if (post.status !== 200 || !post.cookie || !post.body.email || !post.body.role || !post.body.order_id) {
218
+ if (post.body.error) log("warn", "team sign-in failed", { status: post.status, error: post.body.error });
219
+ throw new TeamSyncAuthError("Sign in failed. Check your license key and try again.");
220
+ }
221
+ const get = await httpJson({
222
+ method: "GET",
223
+ path: "/api/team/session",
224
+ cookie: post.cookie,
225
+ baseUrl
226
+ });
227
+ if (get.status !== 200 || !get.body.email || !get.body.role || !get.body.order_id || typeof get.body.exp !== "number") {
228
+ await httpJson({ method: "POST", path: "/api/team/session/logout", cookie: post.cookie, baseUrl }).catch(
229
+ () => void 0
230
+ );
231
+ throw new TeamSyncAuthError("Sign in succeeded but session check failed.");
232
+ }
233
+ const session = {
234
+ email: get.body.email,
235
+ role: get.body.role,
236
+ order_id: get.body.order_id,
237
+ exp: get.body.exp,
238
+ can_edit: get.body.can_edit
239
+ };
240
+ const filePath = opts.filePath ?? sessionStatePath(opts.home);
241
+ await saveStoredState(filePath, { cookie: post.cookie, session });
242
+ return session;
243
+ }
244
+ async function signOut(opts = {}) {
245
+ const filePath = opts.filePath ?? sessionStatePath(opts.home);
246
+ const state = await loadStoredState(filePath);
247
+ if (state) {
248
+ try {
249
+ await httpJson({
250
+ method: "POST",
251
+ path: "/api/team/session/logout",
252
+ cookie: state.cookie,
253
+ baseUrl: opts.baseUrl
254
+ });
255
+ } catch {
256
+ }
257
+ }
258
+ await clearStoredState(filePath);
259
+ }
260
+ async function getSession(opts = {}) {
261
+ const filePath = opts.filePath ?? sessionStatePath(opts.home);
262
+ const state = await loadStoredState(filePath);
263
+ return state?.session ?? null;
264
+ }
265
+ async function getResource(name, opts = {}) {
266
+ const filePath = opts.filePath ?? sessionStatePath(opts.home);
267
+ const state = await loadStoredState(filePath);
268
+ if (!state) throw new TeamSyncAuthError();
269
+ const res = await httpJson({
270
+ method: "GET",
271
+ path: `/api/team/resource/${encodeURIComponent(name)}`,
272
+ cookie: state.cookie,
273
+ baseUrl: opts.baseUrl
274
+ });
275
+ if (res.status === 401) {
276
+ await clearStoredState(filePath);
277
+ throw new TeamSyncAuthError();
278
+ }
279
+ if (res.status === 403) throw new TeamSyncForbiddenError();
280
+ if (res.status !== 200) {
281
+ if (res.body.error) log("warn", "team fetch failed", { name, status: res.status, error: res.body.error });
282
+ throw new Error(`Team fetch failed (${res.status}).`);
283
+ }
284
+ return {
285
+ version: res.body.version,
286
+ data: res.body.data,
287
+ updated_at: res.body.updated_at,
288
+ updated_by: res.body.updated_by
289
+ };
290
+ }
291
+ async function putResource(name, version, data, opts = {}) {
292
+ const filePath = opts.filePath ?? sessionStatePath(opts.home);
293
+ const state = await loadStoredState(filePath);
294
+ if (!state) throw new TeamSyncAuthError();
295
+ const res = await httpJson({
296
+ method: "PUT",
297
+ path: `/api/team/resource/${encodeURIComponent(name)}`,
298
+ body: { version, data },
299
+ cookie: state.cookie,
300
+ baseUrl: opts.baseUrl
301
+ });
302
+ if (res.status === 401) {
303
+ await clearStoredState(filePath);
304
+ throw new TeamSyncAuthError();
305
+ }
306
+ if (res.status === 403) throw new TeamSyncForbiddenError();
307
+ if (res.status === 409) throw new TeamSyncStaleVersionError(res.body.current_version ?? 0);
308
+ if (res.status !== 200) {
309
+ if (res.body.error) log("warn", "team write failed", { name, status: res.status, error: res.body.error });
310
+ throw new Error(`Team write failed (${res.status}).`);
311
+ }
312
+ return {
313
+ version: res.body.version,
314
+ data: res.body.data,
315
+ updated_at: res.body.updated_at,
316
+ updated_by: res.body.updated_by
317
+ };
318
+ }
319
+ async function postAnalyticsEvent(event, opts = {}) {
320
+ const filePath = opts.filePath ?? sessionStatePath(opts.home);
321
+ const state = await loadStoredState(filePath);
322
+ if (!state) return { ok: false };
323
+ const res = await httpJson({
324
+ method: "POST",
325
+ path: "/api/team/analytics/event",
326
+ body: event,
327
+ cookie: state.cookie,
328
+ baseUrl: opts.baseUrl
329
+ });
330
+ if (res.status === 401) {
331
+ await clearStoredState(filePath);
332
+ return { ok: false };
333
+ }
334
+ return { ok: res.status === 200 };
335
+ }
336
+ async function listAnalyticsEvents(opts = {}) {
337
+ const filePath = opts.filePath ?? sessionStatePath(opts.home);
338
+ const state = await loadStoredState(filePath);
339
+ if (!state) throw new TeamSyncAuthError();
340
+ const res = await httpJson({
341
+ method: "GET",
342
+ path: "/api/team/analytics",
343
+ cookie: state.cookie,
344
+ baseUrl: opts.baseUrl
345
+ });
346
+ if (res.status === 401) {
347
+ await clearStoredState(filePath);
348
+ throw new TeamSyncAuthError();
349
+ }
350
+ if (res.status !== 200) {
351
+ if (res.body.error) log("warn", "team analytics list failed", { status: res.status, error: res.body.error });
352
+ throw new Error(`Team analytics fetch failed (${res.status}).`);
353
+ }
354
+ return { events: res.body.events ?? [], cap: res.body.cap ?? 0, order_id: res.body.order_id ?? "" };
355
+ }
356
+ function _resetForTests() {
357
+ invalidateState();
358
+ }
359
+
360
+ export {
361
+ log,
362
+ cacheDir,
363
+ CONFIG_DIRNAME,
364
+ userConfigDir,
365
+ findProjectConfigDir,
366
+ guidePath,
367
+ atomicWriteFile,
368
+ SESSION_STATE_FILENAME,
369
+ TeamSyncAuthError,
370
+ TeamSyncForbiddenError,
371
+ TeamSyncStaleVersionError,
372
+ sessionStatePath,
373
+ signIn,
374
+ signOut,
375
+ getSession,
376
+ getResource,
377
+ putResource,
378
+ postAnalyticsEvent,
379
+ listAnalyticsEvents,
380
+ _resetForTests
381
+ };