letagents 0.8.1 → 0.10.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.
@@ -1,12 +1,16 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
1
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "fs";
2
+ import { randomBytes } from "crypto";
2
3
  import { homedir } from "os";
3
4
  import { dirname, join } from "path";
4
5
  const DEFAULT_STATE_PATH = join(homedir(), ".letagents", "mcp-state.json");
6
+ const STATE_LOCK_WAIT_MS = 25;
7
+ const STATE_LOCK_TIMEOUT_MS = 2_000;
8
+ const STATE_LOCK_STALE_MS = 10_000;
9
+ const STATE_LOCK_SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
5
10
  export function getLocalStatePath() {
6
11
  return process.env.LETAGENTS_STATE_PATH || DEFAULT_STATE_PATH;
7
12
  }
8
- export function readLocalState() {
9
- const statePath = getLocalStatePath();
13
+ function readLocalStateFromPath(statePath) {
10
14
  if (!existsSync(statePath)) {
11
15
  return {};
12
16
  }
@@ -19,18 +23,76 @@ export function readLocalState() {
19
23
  return {};
20
24
  }
21
25
  }
22
- export function writeLocalState(state) {
26
+ export function readLocalState() {
27
+ const statePath = getLocalStatePath();
28
+ return readLocalStateFromPath(statePath);
29
+ }
30
+ function sleepSync(ms) {
31
+ if (ms > 0) {
32
+ Atomics.wait(STATE_LOCK_SLEEP_BUFFER, 0, 0, ms);
33
+ }
34
+ }
35
+ function writeLocalStateUnlocked(statePath, state) {
36
+ const tempPath = `${statePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
37
+ try {
38
+ writeFileSync(tempPath, JSON.stringify(state, null, 2) + "\n", "utf-8");
39
+ renameSync(tempPath, statePath);
40
+ }
41
+ finally {
42
+ rmSync(tempPath, { force: true });
43
+ }
44
+ }
45
+ function withStateLock(callback) {
23
46
  const statePath = getLocalStatePath();
24
47
  mkdirSync(dirname(statePath), { recursive: true });
25
- const tempPath = `${statePath}.tmp`;
26
- writeFileSync(tempPath, JSON.stringify(state, null, 2) + "\n", "utf-8");
27
- renameSync(tempPath, statePath);
48
+ const lockPath = `${statePath}.lock`;
49
+ const startedAt = Date.now();
50
+ while (true) {
51
+ let lockFd = null;
52
+ try {
53
+ lockFd = openSync(lockPath, "wx");
54
+ return callback(statePath);
55
+ }
56
+ catch (error) {
57
+ const err = error;
58
+ if (err.code !== "EEXIST") {
59
+ throw error;
60
+ }
61
+ try {
62
+ const stats = statSync(lockPath);
63
+ if (Date.now() - stats.mtimeMs > STATE_LOCK_STALE_MS) {
64
+ rmSync(lockPath, { force: true });
65
+ continue;
66
+ }
67
+ }
68
+ catch {
69
+ continue;
70
+ }
71
+ if (Date.now() - startedAt >= STATE_LOCK_TIMEOUT_MS) {
72
+ throw new Error(`Timed out acquiring local state lock at ${lockPath}`);
73
+ }
74
+ sleepSync(STATE_LOCK_WAIT_MS);
75
+ }
76
+ finally {
77
+ if (lockFd !== null) {
78
+ closeSync(lockFd);
79
+ rmSync(lockPath, { force: true });
80
+ }
81
+ }
82
+ }
83
+ }
84
+ export function writeLocalState(state) {
85
+ withStateLock((statePath) => {
86
+ writeLocalStateUnlocked(statePath, state);
87
+ });
28
88
  }
29
89
  export function updateLocalState(updater) {
30
- const current = readLocalState();
31
- const updated = updater(current) ?? current;
32
- writeLocalState(updated);
33
- return updated;
90
+ return withStateLock((statePath) => {
91
+ const current = readLocalStateFromPath(statePath);
92
+ const updated = updater(current) ?? current;
93
+ writeLocalStateUnlocked(statePath, updated);
94
+ return updated;
95
+ });
34
96
  }
35
97
  function isExpired(expiresAt) {
36
98
  if (!expiresAt) {
@@ -88,6 +150,27 @@ export function clearPendingDeviceAuth() {
88
150
  return state;
89
151
  });
90
152
  }
153
+ export function getStoredAgentIdentity(identityKey) {
154
+ const state = readLocalState();
155
+ if (identityKey?.trim()) {
156
+ const scoped = state.agent_identities?.[identityKey.trim()];
157
+ if (scoped) {
158
+ return scoped;
159
+ }
160
+ }
161
+ return state.agent_identity ?? null;
162
+ }
163
+ export function setStoredAgentIdentity(agentIdentity, identityKey) {
164
+ updateLocalState((state) => {
165
+ state.agent_identity = agentIdentity;
166
+ if (identityKey?.trim()) {
167
+ state.agent_identities = state.agent_identities ?? {};
168
+ state.agent_identities[identityKey.trim()] = agentIdentity;
169
+ }
170
+ return state;
171
+ });
172
+ return agentIdentity;
173
+ }
91
174
  export function getStoredCurrentRoom() {
92
175
  const state = readLocalState();
93
176
  return state.current_room ?? null;
@@ -4,6 +4,9 @@ export function encodeRoomIdPath(roomId) {
4
4
  .map((segment) => encodeURIComponent(segment))
5
5
  .join("/");
6
6
  }
7
+ export function getCanonicalRoomWebPath(roomId) {
8
+ return `/in/${encodeRoomIdPath(roomId)}`;
9
+ }
7
10
  export function looksLikeInviteCode(value) {
8
11
  return /^[A-Z0-9]{4}(?:-[A-Z0-9]{4})+$/.test(value.trim().toUpperCase());
9
12
  }