@yawlabs/mcp 0.63.1 → 0.64.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  All notable changes to `@yawlabs/mcp` (formerly `@yawlabs/mcph`) are documented here. This project uses [semantic versioning](https://semver.org) and a script-gated release flow: `./release.sh <version>` runs lint + tests + build, bumps, tags, publishes to npm, and creates the GitHub release.
4
4
 
5
+ ## 0.63.2 -- release pipeline: publish npm from CI
6
+
7
+ No changes to the package runtime or CLI -- this release exists to exercise the
8
+ new CI-on-tag-push publish flow end to end. The published artifact is identical
9
+ to 0.63.1 aside from the version bump.
10
+
11
+ - **npm is now published from CI, not the workstation.** A new `publish-npm` job
12
+ in `release.yml` publishes `@yawlabs/mcp` on every `v*` tag using the org
13
+ `NPM_TOKEN` + `--provenance` (the repo and package are public), gated on the
14
+ binary build so npm and the GitHub Release stay in lockstep. It is idempotent:
15
+ a version already live is a clean skip, and an `EPUBLISHCONFLICT` from
16
+ registry read-replica lag is treated as success. `publish-registry` now
17
+ `needs: publish-npm`, so the MCP-registry verify can no longer race ahead of
18
+ the npm publish. `release.sh`'s hand-off detection was tightened to key on a
19
+ real `npm publish` / `NODE_AUTH_TOKEN` signal instead of the registry job's
20
+ `id-token: write` (the false positive that wedged the 0.63.0/0.63.1 runs).
21
+ - **Registry job hardening.** `mcp-publisher` is pinned to a tagged release and
22
+ verified against its published sha256 before execution (was an unpinned
23
+ `curl .../latest | tar`), and the job pins its Node toolchain via `setup-node`.
24
+
5
25
  ## 0.63.1 -- CLI follow-ups: wire dead --dry-run/--stdin flags, fix completion drift, dedup probes
6
26
 
7
27
  Patch-level follow-ups on the 0.63.0 CLI hardening pass. All fixes; no behavior changes for callers who weren't already hitting the dead-flag bugs.
@@ -2,17 +2,17 @@
2
2
 
3
3
  // src/team-sync.ts
4
4
  import { existsSync } from "fs";
5
- import { chmod, readFile, unlink as unlink2 } from "fs/promises";
5
+ import { chmod as chmod2, readFile, unlink as unlink2 } from "fs/promises";
6
6
  import { homedir as homedir2 } from "os";
7
7
  import { join } from "path";
8
8
 
9
9
  // src/atomic-write.ts
10
- import { mkdir, rename, unlink, writeFile } from "fs/promises";
10
+ import { chmod, mkdir, rename, stat, unlink, writeFile } from "fs/promises";
11
11
  import path from "path";
12
- async function atomicWriteFile(filePath, contents, encoding = "utf8", mode) {
12
+ async function atomicWriteFile(filePath, contents, encoding = "utf8", mode, dirMode) {
13
13
  const dir = path.dirname(filePath);
14
14
  const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
15
- await mkdir(dir, { recursive: true });
15
+ await mkdirpWithMode(dir, dirMode);
16
16
  try {
17
17
  await writeFile(tmp, contents, mode === void 0 ? { encoding } : { encoding, mode });
18
18
  await rename(tmp, filePath);
@@ -21,6 +21,36 @@ async function atomicWriteFile(filePath, contents, encoding = "utf8", mode) {
21
21
  throw err;
22
22
  }
23
23
  }
24
+ async function mkdirpWithMode(dir, dirMode) {
25
+ if (dirMode === void 0 || process.platform === "win32") {
26
+ await mkdir(dir, { recursive: true });
27
+ return;
28
+ }
29
+ const resolved = path.resolve(dir);
30
+ const toCreate = [];
31
+ let cursor = resolved;
32
+ while (true) {
33
+ let exists = true;
34
+ try {
35
+ await stat(cursor);
36
+ } catch {
37
+ exists = false;
38
+ }
39
+ if (exists) break;
40
+ toCreate.unshift(cursor);
41
+ const parent = path.dirname(cursor);
42
+ if (parent === cursor) break;
43
+ cursor = parent;
44
+ }
45
+ if (toCreate.length === 0) return;
46
+ await mkdir(resolved, { recursive: true });
47
+ for (const created of toCreate) {
48
+ try {
49
+ await chmod(created, dirMode);
50
+ } catch {
51
+ }
52
+ }
53
+ }
24
54
 
25
55
  // src/logger.ts
26
56
  var LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
@@ -52,12 +82,24 @@ var CONFIG_DIRNAME = ".yaw-mcp";
52
82
  function userConfigDir(home = homedir()) {
53
83
  return path2.join(home, CONFIG_DIRNAME);
54
84
  }
85
+ function normalizeForCompare(p) {
86
+ return process.platform === "win32" ? p.toLowerCase() : p;
87
+ }
88
+ function isUnderHome(dir, homeResolved) {
89
+ const dirKey = normalizeForCompare(dir);
90
+ const homeKey = normalizeForCompare(homeResolved);
91
+ if (dirKey === homeKey) return false;
92
+ const rel = path2.relative(homeResolved, dir);
93
+ const relNorm = normalizeForCompare(rel);
94
+ return relNorm !== "" && !relNorm.startsWith("..") && !path2.isAbsolute(rel);
95
+ }
55
96
  async function findProjectConfigDir(start, home = homedir()) {
56
- const homeResolved = path2.resolve(home);
97
+ const homeFallback = home && home.length > 0 ? home : process.env.USERPROFILE || homedir();
98
+ const homeResolved = path2.resolve(homeFallback);
57
99
  let dir = path2.resolve(start);
58
100
  let prev = "";
59
101
  while (dir !== prev) {
60
- if (dir === homeResolved) return null;
102
+ if (!isUnderHome(dir, homeResolved)) return null;
61
103
  const candidate = path2.join(dir, CONFIG_DIRNAME);
62
104
  try {
63
105
  await access(candidate);
@@ -108,7 +150,8 @@ function resolveBaseUrl(env = process.env) {
108
150
  }
109
151
  function expMs(session) {
110
152
  const e = session.exp;
111
- return e > 0 && e < 1e12 ? e * 1e3 : e;
153
+ if (typeof e !== "number" || !Number.isFinite(e) || e <= 0) return 0;
154
+ return e < 1e12 ? e * 1e3 : e;
112
155
  }
113
156
  var cachedState = null;
114
157
  function invalidateState() {
@@ -117,7 +160,7 @@ function invalidateState() {
117
160
  async function loadStoredState(filePath) {
118
161
  if (cachedState && cachedState.filePath === filePath) {
119
162
  const s = cachedState.state;
120
- if (s && expMs(s.session) < Date.now()) {
163
+ if (s && expMs(s.session) <= Date.now()) {
121
164
  cachedState = { filePath, state: null };
122
165
  return null;
123
166
  }
@@ -130,11 +173,12 @@ async function loadStoredState(filePath) {
130
173
  if (!obj || typeof obj !== "object") parsed = null;
131
174
  else if (typeof obj.cookie !== "string" || !obj.cookie) parsed = null;
132
175
  else if (!obj.session || typeof obj.session !== "object") parsed = null;
176
+ else if (typeof obj.session.exp !== "number" || !Number.isFinite(obj.session.exp)) parsed = null;
133
177
  else parsed = obj;
134
178
  } catch {
135
179
  parsed = null;
136
180
  }
137
- if (parsed && expMs(parsed.session) < Date.now()) {
181
+ if (parsed && expMs(parsed.session) <= Date.now()) {
138
182
  cachedState = { filePath, state: null };
139
183
  return null;
140
184
  }
@@ -144,10 +188,10 @@ async function loadStoredState(filePath) {
144
188
  async function saveStoredState(filePath, state) {
145
189
  cachedState = { filePath, state };
146
190
  try {
147
- await atomicWriteFile(filePath, JSON.stringify(state, null, 2), "utf8", 384);
191
+ await atomicWriteFile(filePath, JSON.stringify(state, null, 2), "utf8", 384, 448);
148
192
  if (process.platform !== "win32") {
149
193
  try {
150
- await chmod(filePath, 384);
194
+ await chmod2(filePath, 384);
151
195
  } catch {
152
196
  }
153
197
  }
@@ -208,12 +252,18 @@ async function signIn(key, opts = {}) {
208
252
  const trimmed = key.trim();
209
253
  if (!trimmed) throw new Error("License key is required.");
210
254
  const baseUrl = opts.baseUrl;
211
- const post = await httpJson({
212
- method: "POST",
213
- path: "/api/team/session",
214
- body: { key: trimmed },
215
- baseUrl
216
- });
255
+ let post;
256
+ try {
257
+ post = await httpJson({
258
+ method: "POST",
259
+ path: "/api/team/session",
260
+ body: { key: trimmed },
261
+ baseUrl
262
+ });
263
+ } catch (err) {
264
+ const msg = err instanceof Error ? err.message : String(err);
265
+ throw new TeamSyncAuthError(`Sign in request failed: ${msg.replace(trimmed, "[redacted]")}`);
266
+ }
217
267
  if (post.status !== 200 || !post.cookie || !post.body.email || !post.body.role || !post.body.order_id) {
218
268
  if (post.body.error) log("warn", "team sign-in failed", { status: post.status, error: post.body.error });
219
269
  throw new TeamSyncAuthError("Sign in failed. Check your license key and try again.");