circal-mcp 0.2.0 → 0.2.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.
Files changed (2) hide show
  1. package/dist/circal-mcp.mjs +64 -19
  2. package/package.json +1 -1
@@ -1460,8 +1460,9 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
1460
1460
 
1461
1461
  // mcp/file.ts
1462
1462
  import { randomBytes } from "node:crypto";
1463
- import { open, readFile, rename, rm } from "node:fs/promises";
1463
+ import { open, readFile, rename, rm, stat } from "node:fs/promises";
1464
1464
  import { dirname, join } from "node:path";
1465
+ import { setTimeout as sleep } from "node:timers/promises";
1465
1466
 
1466
1467
  // src/lib/time.ts
1467
1468
  var MINUTES_PER_DAY = 1440;
@@ -3664,25 +3665,68 @@ async function loadForRead(ctx2) {
3664
3665
  const contents = await readMirror(ctx2.path);
3665
3666
  return { contents, warning: zoneWarning(contents) };
3666
3667
  }
3667
- async function casWriteOnce(ctx2, mutate) {
3668
- for (let attempt = 0; attempt < 2; attempt++) {
3669
- const before = await readMirror(ctx2.path);
3670
- assertVersionAllowsMutation(before);
3671
- assertZoneAllowsMutation(before, ctx2.env);
3672
- const { result, summary } = mutate(before);
3673
- const after = await readMirror(ctx2.path);
3674
- if (after.rev !== before.rev) {
3675
- if (attempt === 1) {
3676
- throw new Error(
3677
- `The file moved under this write: read at rev ${before.rev}, still found rev ${after.rev} after retrying once. Something else is writing it \u2014 try again.`
3678
- );
3668
+ var LOCK_STALE_MS = 1e4;
3669
+ var LOCK_WAIT_MS = 2e3;
3670
+ var LOCK_POLL_MS = 100;
3671
+ async function acquireLock(path2) {
3672
+ const lock = `${path2}.lock`;
3673
+ const deadline = Date.now() + LOCK_WAIT_MS;
3674
+ for (; ; ) {
3675
+ try {
3676
+ const file = await open(lock, "wx");
3677
+ try {
3678
+ await file.writeFile(`${process.pid} ${(/* @__PURE__ */ new Date()).toISOString()}
3679
+ `, "utf8");
3680
+ } finally {
3681
+ await file.close();
3679
3682
  }
3683
+ return lock;
3684
+ } catch (error) {
3685
+ if (error.code !== "EEXIST") throw error;
3686
+ }
3687
+ try {
3688
+ if (Date.now() - (await stat(lock)).mtimeMs > LOCK_STALE_MS) {
3689
+ await rm(lock, { force: true });
3690
+ continue;
3691
+ }
3692
+ } catch {
3680
3693
  continue;
3681
3694
  }
3682
- await atomicWrite(ctx2.path, serialiseMirror(result, after.rev + 1, after.zone || localZone()));
3683
- return summary;
3695
+ if (Date.now() >= deadline) {
3696
+ throw new Error(
3697
+ `Another writer holds ${lock}. If nothing else is writing this calendar, delete that file and retry.`
3698
+ );
3699
+ }
3700
+ await sleep(LOCK_POLL_MS);
3701
+ }
3702
+ }
3703
+ async function casWriteOnce(ctx2, mutate) {
3704
+ const lock = await acquireLock(ctx2.path);
3705
+ try {
3706
+ for (let attempt = 0; attempt < 2; attempt++) {
3707
+ const before = await readMirror(ctx2.path);
3708
+ assertVersionAllowsMutation(before);
3709
+ assertZoneAllowsMutation(before, ctx2.env);
3710
+ const { result, summary } = mutate(before);
3711
+ const after = await readMirror(ctx2.path);
3712
+ if (after.rev !== before.rev) {
3713
+ if (attempt === 1) {
3714
+ throw new Error(
3715
+ `The file moved under this write: read at rev ${before.rev}, still found rev ${after.rev} after retrying once. Something else is writing it \u2014 try again.`
3716
+ );
3717
+ }
3718
+ continue;
3719
+ }
3720
+ await atomicWrite(
3721
+ ctx2.path,
3722
+ serialiseMirror(result, after.rev + 1, after.zone || localZone())
3723
+ );
3724
+ return summary;
3725
+ }
3726
+ throw new Error("unreachable");
3727
+ } finally {
3728
+ await rm(lock, { force: true });
3684
3729
  }
3685
- throw new Error("unreachable");
3686
3730
  }
3687
3731
  var writeQueue = Promise.resolve();
3688
3732
  function casWrite(ctx2, mutate) {
@@ -3695,7 +3739,7 @@ function casWrite(ctx2, mutate) {
3695
3739
  }
3696
3740
 
3697
3741
  // mcp/find.ts
3698
- import { readFile as readFile2, readdir, stat } from "node:fs/promises";
3742
+ import { readFile as readFile2, readdir, stat as stat2 } from "node:fs/promises";
3699
3743
  import { homedir } from "node:os";
3700
3744
  import { join as join2 } from "node:path";
3701
3745
  var SKIP_DIR_NAMES = { node_modules: true, Library: true, ".git": true };
@@ -3742,7 +3786,7 @@ async function listDir(path2) {
3742
3786
  }
3743
3787
  async function asMirror(path2) {
3744
3788
  try {
3745
- const info = await stat(path2);
3789
+ const info = await stat2(path2);
3746
3790
  if (!info.isFile() || info.size > MAX_FILE_BYTES) return null;
3747
3791
  const text = await readFile2(path2, "utf8");
3748
3792
  const contents = readMirrorFile(text);
@@ -3930,7 +3974,8 @@ function resolveSpanForCreate(input, day, defaultDuration) {
3930
3974
  const at = atMinutes(day, parseTime(input.start, "start")).getTime();
3931
3975
  return { start: at, end: at, allDay: false, task: true };
3932
3976
  }
3933
- const noTimeGiven = input.start === void 0 && input.end === void 0 && input.duration === void 0;
3977
+ const endIsDay = input.end !== void 0 && DATE_KEY_RE.test(input.end);
3978
+ const noTimeGiven = input.start === void 0 && (input.end === void 0 || endIsDay) && input.duration === void 0;
3934
3979
  const allDay = input.allDay === true || input.allDay === void 0 && noTimeGiven;
3935
3980
  if (allDay) {
3936
3981
  const days = input.end ? Math.max(1, daysBetween(day, fromDayKey(input.end)) + 1) : 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circal-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "MCP server for circal — read and write your calendar through its mirror file",
6
6
  "license": "MIT",