lark-remote 0.1.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.
Files changed (201) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +162 -0
  4. package/README.md +161 -0
  5. package/dist/binder.d.ts +57 -0
  6. package/dist/binder.js +105 -0
  7. package/dist/bridge/index.d.ts +338 -0
  8. package/dist/bridge/index.js +1281 -0
  9. package/dist/bridge/queue-manager.d.ts +277 -0
  10. package/dist/bridge/queue-manager.js +729 -0
  11. package/dist/card/bash-card-session.d.ts +78 -0
  12. package/dist/card/bash-card-session.js +174 -0
  13. package/dist/card/bash-renderer.d.ts +24 -0
  14. package/dist/card/bash-renderer.js +187 -0
  15. package/dist/card/card-budget.d.ts +31 -0
  16. package/dist/card/card-budget.js +280 -0
  17. package/dist/card/card-session.d.ts +83 -0
  18. package/dist/card/card-session.js +185 -0
  19. package/dist/card/card-shared.d.ts +28 -0
  20. package/dist/card/card-shared.js +99 -0
  21. package/dist/card/collapsible.d.ts +26 -0
  22. package/dist/card/collapsible.js +87 -0
  23. package/dist/card/run-card-session.d.ts +78 -0
  24. package/dist/card/run-card-session.js +165 -0
  25. package/dist/card/run-renderer.d.ts +66 -0
  26. package/dist/card/run-renderer.js +841 -0
  27. package/dist/card/run-state.d.ts +103 -0
  28. package/dist/card/run-state.js +313 -0
  29. package/dist/card/text-truncate.d.ts +56 -0
  30. package/dist/card/text-truncate.js +184 -0
  31. package/dist/card/time.d.ts +1 -0
  32. package/dist/card/time.js +14 -0
  33. package/dist/card/tool-render.d.ts +25 -0
  34. package/dist/card/tool-render.js +176 -0
  35. package/dist/cli.d.ts +28 -0
  36. package/dist/cli.js +89 -0
  37. package/dist/common/guards.d.ts +37 -0
  38. package/dist/common/guards.js +65 -0
  39. package/dist/config/codex-config.d.ts +113 -0
  40. package/dist/config/codex-config.js +428 -0
  41. package/dist/config/dir.d.ts +17 -0
  42. package/dist/config/dir.js +81 -0
  43. package/dist/config/index.d.ts +219 -0
  44. package/dist/config/index.js +388 -0
  45. package/dist/config/kimi-config.d.ts +33 -0
  46. package/dist/config/kimi-config.js +111 -0
  47. package/dist/config/opencode-config.d.ts +20 -0
  48. package/dist/config/opencode-config.js +124 -0
  49. package/dist/config/pi-config.d.ts +49 -0
  50. package/dist/config/pi-config.js +151 -0
  51. package/dist/config/wizard.d.ts +12 -0
  52. package/dist/config/wizard.js +84 -0
  53. package/dist/connector/file-limits.d.ts +7 -0
  54. package/dist/connector/file-limits.js +8 -0
  55. package/dist/connector/index.d.ts +86 -0
  56. package/dist/connector/index.js +340 -0
  57. package/dist/error-classification.d.ts +15 -0
  58. package/dist/error-classification.js +24 -0
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.js +580 -0
  61. package/dist/instance-lock.d.ts +30 -0
  62. package/dist/instance-lock.js +184 -0
  63. package/dist/logger/index.d.ts +57 -0
  64. package/dist/logger/index.js +178 -0
  65. package/dist/order/index.d.ts +27 -0
  66. package/dist/order/index.js +90 -0
  67. package/dist/persistence/atomic-write.d.ts +10 -0
  68. package/dist/persistence/atomic-write.js +65 -0
  69. package/dist/persistence/load-json-file.d.ts +9 -0
  70. package/dist/persistence/load-json-file.js +23 -0
  71. package/dist/restart.d.ts +24 -0
  72. package/dist/restart.js +73 -0
  73. package/dist/router/card-helpers.d.ts +28 -0
  74. package/dist/router/card-helpers.js +65 -0
  75. package/dist/router/config/claude.d.ts +14 -0
  76. package/dist/router/config/claude.js +72 -0
  77. package/dist/router/config/codex.d.ts +22 -0
  78. package/dist/router/config/codex.js +100 -0
  79. package/dist/router/config/common/render.d.ts +16 -0
  80. package/dist/router/config/common/render.js +158 -0
  81. package/dist/router/config/index.d.ts +18 -0
  82. package/dist/router/config/index.js +38 -0
  83. package/dist/router/config/kimi.d.ts +21 -0
  84. package/dist/router/config/kimi.js +63 -0
  85. package/dist/router/config/opencode.d.ts +12 -0
  86. package/dist/router/config/opencode.js +44 -0
  87. package/dist/router/config/pi.d.ts +12 -0
  88. package/dist/router/config/pi.js +50 -0
  89. package/dist/router/config/types.d.ts +43 -0
  90. package/dist/router/config/types.js +8 -0
  91. package/dist/router/index.d.ts +318 -0
  92. package/dist/router/index.js +2715 -0
  93. package/dist/router/order-exec-dispatch.d.ts +30 -0
  94. package/dist/router/order-exec-dispatch.js +48 -0
  95. package/dist/router/utils.d.ts +44 -0
  96. package/dist/router/utils.js +116 -0
  97. package/dist/runner/bash/index.d.ts +4 -0
  98. package/dist/runner/bash/index.js +5 -0
  99. package/dist/runner/bash/runner.d.ts +55 -0
  100. package/dist/runner/bash/runner.js +213 -0
  101. package/dist/runner/claude/index.d.ts +4 -0
  102. package/dist/runner/claude/index.js +5 -0
  103. package/dist/runner/claude/runner.d.ts +29 -0
  104. package/dist/runner/claude/runner.js +83 -0
  105. package/dist/runner/codex/argv.d.ts +33 -0
  106. package/dist/runner/codex/argv.js +40 -0
  107. package/dist/runner/codex/index.d.ts +1 -0
  108. package/dist/runner/codex/index.js +2 -0
  109. package/dist/runner/codex/jsonl.d.ts +37 -0
  110. package/dist/runner/codex/jsonl.js +221 -0
  111. package/dist/runner/codex/runner.d.ts +65 -0
  112. package/dist/runner/codex/runner.js +124 -0
  113. package/dist/runner/common/exec-translator.d.ts +73 -0
  114. package/dist/runner/common/exec-translator.js +105 -0
  115. package/dist/runner/common/jsonl-stream.d.ts +39 -0
  116. package/dist/runner/common/jsonl-stream.js +203 -0
  117. package/dist/runner/common/process-stopper.d.ts +28 -0
  118. package/dist/runner/common/process-stopper.js +79 -0
  119. package/dist/runner/common/runner-utils.d.ts +21 -0
  120. package/dist/runner/common/runner-utils.js +65 -0
  121. package/dist/runner/common/spawn-heartbeat.d.ts +33 -0
  122. package/dist/runner/common/spawn-heartbeat.js +51 -0
  123. package/dist/runner/common/spawning-runner.d.ts +321 -0
  124. package/dist/runner/common/spawning-runner.js +634 -0
  125. package/dist/runner/common/usage.d.ts +31 -0
  126. package/dist/runner/common/usage.js +29 -0
  127. package/dist/runner/index.d.ts +19 -0
  128. package/dist/runner/index.js +24 -0
  129. package/dist/runner/kimi/index.d.ts +1 -0
  130. package/dist/runner/kimi/index.js +2 -0
  131. package/dist/runner/kimi/jsonl.d.ts +28 -0
  132. package/dist/runner/kimi/jsonl.js +143 -0
  133. package/dist/runner/kimi/runner.d.ts +50 -0
  134. package/dist/runner/kimi/runner.js +105 -0
  135. package/dist/runner/opencode/argv.d.ts +32 -0
  136. package/dist/runner/opencode/argv.js +35 -0
  137. package/dist/runner/opencode/index.d.ts +1 -0
  138. package/dist/runner/opencode/index.js +2 -0
  139. package/dist/runner/opencode/jsonl.d.ts +68 -0
  140. package/dist/runner/opencode/jsonl.js +295 -0
  141. package/dist/runner/opencode/runner.d.ts +63 -0
  142. package/dist/runner/opencode/runner.js +93 -0
  143. package/dist/runner/pi/index.d.ts +1 -0
  144. package/dist/runner/pi/index.js +2 -0
  145. package/dist/runner/pi/jsonl.d.ts +128 -0
  146. package/dist/runner/pi/jsonl.js +249 -0
  147. package/dist/runner/pi/runner.d.ts +37 -0
  148. package/dist/runner/pi/runner.js +76 -0
  149. package/dist/runner/registry.d.ts +54 -0
  150. package/dist/runner/registry.js +75 -0
  151. package/dist/runner/resolve-agent-choices.d.ts +7 -0
  152. package/dist/runner/resolve-agent-choices.js +76 -0
  153. package/dist/runner/sync-agent-choices.d.ts +6 -0
  154. package/dist/runner/sync-agent-choices.js +73 -0
  155. package/dist/runner/types.d.ts +250 -0
  156. package/dist/runner/types.js +8 -0
  157. package/dist/session/claude/index.d.ts +1 -0
  158. package/dist/session/claude/index.js +2 -0
  159. package/dist/session/claude/session-reader.d.ts +34 -0
  160. package/dist/session/claude/session-reader.js +46 -0
  161. package/dist/session/claude/sessions.d.ts +53 -0
  162. package/dist/session/claude/sessions.js +528 -0
  163. package/dist/session/codex/index.d.ts +1 -0
  164. package/dist/session/codex/index.js +2 -0
  165. package/dist/session/codex/rollout-reader.d.ts +88 -0
  166. package/dist/session/codex/rollout-reader.js +473 -0
  167. package/dist/session/codex/sessions.d.ts +21 -0
  168. package/dist/session/codex/sessions.js +40 -0
  169. package/dist/session/common/catch-up.d.ts +12 -0
  170. package/dist/session/common/catch-up.js +31 -0
  171. package/dist/session/common/constants.d.ts +11 -0
  172. package/dist/session/common/constants.js +12 -0
  173. package/dist/session/common/content-blocks.d.ts +37 -0
  174. package/dist/session/common/content-blocks.js +68 -0
  175. package/dist/session/common/jsonl.d.ts +77 -0
  176. package/dist/session/common/jsonl.js +351 -0
  177. package/dist/session/common/usage-accumulator.d.ts +58 -0
  178. package/dist/session/common/usage-accumulator.js +72 -0
  179. package/dist/session/index.d.ts +7 -0
  180. package/dist/session/index.js +11 -0
  181. package/dist/session/kimi/index.d.ts +1 -0
  182. package/dist/session/kimi/index.js +2 -0
  183. package/dist/session/kimi/sessions.d.ts +81 -0
  184. package/dist/session/kimi/sessions.js +527 -0
  185. package/dist/session/opencode/index.d.ts +1 -0
  186. package/dist/session/opencode/index.js +2 -0
  187. package/dist/session/opencode/sessions.d.ts +81 -0
  188. package/dist/session/opencode/sessions.js +388 -0
  189. package/dist/session/pi/index.d.ts +1 -0
  190. package/dist/session/pi/index.js +2 -0
  191. package/dist/session/pi/sessions.d.ts +27 -0
  192. package/dist/session/pi/sessions.js +399 -0
  193. package/dist/session/registry.d.ts +12 -0
  194. package/dist/session/registry.js +20 -0
  195. package/dist/session/session-store.d.ts +86 -0
  196. package/dist/session/session-store.js +450 -0
  197. package/dist/startup-contact.d.ts +18 -0
  198. package/dist/startup-contact.js +51 -0
  199. package/dist/workspace/index.d.ts +19 -0
  200. package/dist/workspace/index.js +53 -0
  201. package/package.json +93 -0
@@ -0,0 +1,184 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+ export class InstanceAlreadyRunningError extends Error {
5
+ pid;
6
+ lockPath;
7
+ constructor(pid, lockPath) {
8
+ super(`lark-remote is already running for this config: pid ${pid}`);
9
+ this.pid = pid;
10
+ this.lockPath = lockPath;
11
+ }
12
+ }
13
+ /**
14
+ * Probe whether a pid is alive. P2-20:
15
+ * - process.kill(pid, 0) returning normally → alive.
16
+ * - ESRCH (no such process) → dead.
17
+ * - EPERM (operation not permitted) → the process EXISTS but is owned by
18
+ * another user; we just lack permission to signal it. Returning true here
19
+ * is correct: the single instance that owns the lock is still running, so
20
+ * we must refuse to acquire. Previously EPERM was swallowed as "not
21
+ * running", allowing a second instance to overwrite the lock.
22
+ * - EINVAL / other → conservative true (do not clobber a lock we cannot
23
+ * prove is stale).
24
+ */
25
+ function isPidAlive(pid) {
26
+ try {
27
+ process.kill(pid, 0);
28
+ return true;
29
+ }
30
+ catch (err) {
31
+ const code = err.code;
32
+ if (code === 'ESRCH')
33
+ return false;
34
+ // EPERM and any unexpected error → assume alive (never clobber a lock
35
+ // we cannot disprove is held by a running instance).
36
+ return true;
37
+ }
38
+ }
39
+ export class InstanceLock {
40
+ lockPath;
41
+ /**
42
+ * P2-20: injectable process-running probe for testing, and a process-name
43
+ * verifier to defend against PID reuse (a stale pid recycled by an unrelated
44
+ * process would otherwise be misjudged as "our instance still running").
45
+ * Default implementation checks the process name via `ps` when available.
46
+ */
47
+ isProcessRunning;
48
+ constructor(lockPath, opts) {
49
+ this.lockPath = lockPath;
50
+ this.isProcessRunning =
51
+ opts?.isProcessRunning ?? ((pid, expectedComm) => defaultIsProcessRunning(pid, expectedComm));
52
+ }
53
+ acquire() {
54
+ fs.mkdirSync(path.dirname(this.lockPath), { recursive: true });
55
+ // Atomic create-with-exclusive: 'wx' fails if file already exists.
56
+ // This eliminates the TOCTOU race between readPid+isProcessRunning
57
+ // and writeFileSync in the previous implementation.
58
+ try {
59
+ const fd = fs.openSync(this.lockPath, 'wx');
60
+ fs.writeSync(fd, this.lockContents(process.pid));
61
+ fs.closeSync(fd);
62
+ }
63
+ catch (err) {
64
+ if (err.code === 'EEXIST') {
65
+ // Lock file exists — check if the owner is still alive
66
+ const { pid, comm } = this.readPidAndComm();
67
+ if (pid && this.isProcessRunning(pid, comm)) {
68
+ throw new InstanceAlreadyRunningError(pid, this.lockPath);
69
+ }
70
+ // Stale lock: remove and retry once
71
+ try {
72
+ fs.unlinkSync(this.lockPath);
73
+ }
74
+ catch { }
75
+ try {
76
+ const fd = fs.openSync(this.lockPath, 'wx');
77
+ fs.writeSync(fd, this.lockContents(process.pid));
78
+ fs.closeSync(fd);
79
+ }
80
+ catch (retryErr) {
81
+ if (retryErr.code === 'EEXIST') {
82
+ // Another process created the lock between unlink and retry.
83
+ // Bounded retry (max 1): give up rather than recurse.
84
+ const { pid, comm } = this.readPidAndComm();
85
+ if (pid && this.isProcessRunning(pid, comm)) {
86
+ throw new InstanceAlreadyRunningError(pid, this.lockPath);
87
+ }
88
+ // New lock is also stale — give up rather than recurse.
89
+ throw new InstanceAlreadyRunningError(pid ?? -1, this.lockPath);
90
+ }
91
+ throw retryErr;
92
+ }
93
+ }
94
+ else {
95
+ throw err;
96
+ }
97
+ }
98
+ }
99
+ release() {
100
+ const { pid } = this.readPidAndComm();
101
+ if (pid === process.pid) {
102
+ try {
103
+ fs.unlinkSync(this.lockPath);
104
+ }
105
+ catch { }
106
+ }
107
+ }
108
+ registerExitHandlers() {
109
+ process.on('exit', () => this.release());
110
+ process.on('SIGINT', () => {
111
+ this.release();
112
+ process.exit(130);
113
+ });
114
+ process.on('SIGTERM', () => {
115
+ this.release();
116
+ process.exit(143);
117
+ });
118
+ if (process.platform !== 'win32') {
119
+ process.on('SIGHUP', () => {
120
+ this.release();
121
+ process.exit(129);
122
+ });
123
+ }
124
+ }
125
+ /**
126
+ * Lock file stores `pid\ncomm` where comm is the owning process's command
127
+ * name (captured at acquire time). On stale-lock checks we verify the
128
+ * recorded pid is still alive AND still running a process whose comm
129
+ * matches — defending against PID reuse where an unrelated process recycled
130
+ * the stale pid.
131
+ */
132
+ lockContents(pid) {
133
+ return `${pid}\n${currentComm()}`;
134
+ }
135
+ readPidAndComm() {
136
+ if (!fs.existsSync(this.lockPath))
137
+ return { pid: undefined, comm: undefined };
138
+ const raw = fs.readFileSync(this.lockPath, 'utf-8').trim();
139
+ const [pidStr, ...commParts] = raw.split('\n');
140
+ const pid = Number(pidStr);
141
+ const comm = commParts.join('\n') || undefined;
142
+ if (Number.isInteger(pid) && pid > 0 && comm)
143
+ return { pid, comm };
144
+ // Malformed or missing comm — treat as corrupt, unlink and return undefined
145
+ try {
146
+ fs.unlinkSync(this.lockPath);
147
+ }
148
+ catch { }
149
+ return { pid: undefined, comm: undefined };
150
+ }
151
+ }
152
+ /**
153
+ * Default probe: alive by ESRCH/EPERM semantics, plus a weak comm-name match
154
+ * to reject PID reuse. If `ps` is unavailable we fall back to the pid-alive
155
+ * check alone (no false negatives, only weaker reuse protection).
156
+ */
157
+ function defaultIsProcessRunning(pid, expectedComm) {
158
+ if (!isPidAlive(pid))
159
+ return false;
160
+ if (!expectedComm)
161
+ return false;
162
+ const actualComm = readComm(pid);
163
+ if (actualComm === null)
164
+ return true; // ps unavailable → trust pid-alive
165
+ // Match on the basename so `/usr/local/bin/node` vs `node` still align.
166
+ const base = (s) => s.split('/').pop() ?? s;
167
+ return base(actualComm) === base(expectedComm) || actualComm.includes(expectedComm);
168
+ }
169
+ function currentComm() {
170
+ return readComm(process.pid) ?? 'lark-remote';
171
+ }
172
+ function readComm(pid) {
173
+ try {
174
+ const out = execFileSync('ps', ['-p', String(pid), '-o', 'comm='], {
175
+ encoding: 'utf-8',
176
+ stdio: ['ignore', 'pipe', 'ignore'],
177
+ }).trim();
178
+ return out || null;
179
+ }
180
+ catch {
181
+ return null;
182
+ }
183
+ }
184
+ //# sourceMappingURL=instance-lock.js.map
@@ -0,0 +1,57 @@
1
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+ interface LoggerOptions {
3
+ /** Log directory. Defaults to ~/.lark-remote/logs */
4
+ dir?: string;
5
+ /** Minimum level to write. Defaults to 'info'. */
6
+ level?: LogLevel;
7
+ /** File prefix. Defaults to 'lark-remote'. */
8
+ prefix?: string;
9
+ /** Override pid (for testing). */
10
+ pid?: number;
11
+ /** Override now() (for testing). */
12
+ now?: () => Date;
13
+ }
14
+ /**
15
+ * File-based logger with daily rotation.
16
+ *
17
+ * - Each day gets its own subdirectory: `<dir>/YYYY-MM-DD/`.
18
+ * - Log files are named `<prefix>-<pid>.log` inside the daily subdirectory.
19
+ * - When the calendar date changes (across midnight), a new directory and file
20
+ * are created automatically, so each file never spans two days.
21
+ * - Nothing is written to stdout/stderr; all output goes to the file.
22
+ * - Writes are synchronous (fs.appendFileSync) — log volume is low and this
23
+ * guarantees ordering without leaving dangling file handles on exit.
24
+ */
25
+ export declare class Logger {
26
+ private dir;
27
+ private level;
28
+ private prefix;
29
+ private pid;
30
+ private now;
31
+ private currentDate;
32
+ private currentFile;
33
+ constructor(opts?: LoggerOptions);
34
+ /** Get the current log file path (e.g., ~/.lark-remote/logs/2026-07-11/lark-remote-12345.log). */
35
+ getCurrentLogFile(): string;
36
+ private ensureFile;
37
+ /**
38
+ * P2-19①: remove daily subdirectories older than the retention window.
39
+ * Best-effort — any error is swallowed (logging must never throw).
40
+ */
41
+ private pruneOldDirs;
42
+ private write;
43
+ debug(...args: unknown[]): void;
44
+ info(...args: unknown[]): void;
45
+ warn(...args: unknown[]): void;
46
+ error(...args: unknown[]): void;
47
+ /** Reset internal file state so the next write rotates to a fresh daily file. */
48
+ close(): void;
49
+ }
50
+ /**
51
+ * Initialize (or reinitialize) the global logger. Must be called once at
52
+ * startup, before any logging is expected to land in the right place.
53
+ * Returns the initialized logger.
54
+ */
55
+ export declare function initLogger(opts: LoggerOptions): Logger;
56
+ export declare function getLogger(): Logger;
57
+ export {};
@@ -0,0 +1,178 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { format } from 'node:util';
5
+ const LEVEL_ORDER = {
6
+ debug: 10,
7
+ info: 20,
8
+ warn: 30,
9
+ error: 40,
10
+ };
11
+ const LEVEL_UPPER = {
12
+ debug: 'DEBUG',
13
+ info: 'INFO',
14
+ warn: 'WARN',
15
+ error: 'ERROR',
16
+ };
17
+ function todayStr(d) {
18
+ const y = d.getFullYear();
19
+ const m = String(d.getMonth() + 1).padStart(2, '0');
20
+ const day = String(d.getDate()).padStart(2, '0');
21
+ return `${y}-${m}-${day}`;
22
+ }
23
+ function timestampStr(d) {
24
+ // P2-19②: local ISO 8601 with milliseconds and explicit offset, e.g.
25
+ // 2026-08-03T01:30:00.000+08:00. Previously this used toISOString() (UTC)
26
+ // while the daily directory boundary (todayStr) used local time — after
27
+ // local midnight in a +08:00 zone the new day's directory held lines
28
+ // timestamped with the previous UTC day. Local offset keeps the timestamp's
29
+ // calendar date consistent with the directory date across all timezones.
30
+ const pad = (n) => String(n).padStart(2, '0');
31
+ const y = d.getFullYear();
32
+ const mo = pad(d.getMonth() + 1);
33
+ const da = pad(d.getDate());
34
+ const h = pad(d.getHours());
35
+ const mi = pad(d.getMinutes());
36
+ const s = pad(d.getSeconds());
37
+ const ms = String(d.getMilliseconds()).padStart(3, '0');
38
+ const offsetMin = -d.getTimezoneOffset();
39
+ const sign = offsetMin >= 0 ? '+' : '-';
40
+ const absOff = Math.abs(offsetMin);
41
+ const offH = pad(Math.floor(absOff / 60));
42
+ const offM = pad(absOff % 60);
43
+ return `${y}-${mo}-${da}T${h}:${mi}:${s}.${ms}${sign}${offH}:${offM}`;
44
+ }
45
+ /**
46
+ * File-based logger with daily rotation.
47
+ *
48
+ * - Each day gets its own subdirectory: `<dir>/YYYY-MM-DD/`.
49
+ * - Log files are named `<prefix>-<pid>.log` inside the daily subdirectory.
50
+ * - When the calendar date changes (across midnight), a new directory and file
51
+ * are created automatically, so each file never spans two days.
52
+ * - Nothing is written to stdout/stderr; all output goes to the file.
53
+ * - Writes are synchronous (fs.appendFileSync) — log volume is low and this
54
+ * guarantees ordering without leaving dangling file handles on exit.
55
+ */
56
+ export class Logger {
57
+ dir;
58
+ level;
59
+ prefix;
60
+ pid;
61
+ now;
62
+ currentDate = '';
63
+ currentFile = '';
64
+ constructor(opts = {}) {
65
+ this.dir = opts.dir ?? path.join(os.homedir(), '.lark-remote', 'logs');
66
+ this.level = LEVEL_ORDER[opts.level ?? 'info'];
67
+ this.prefix = opts.prefix ?? 'lark-remote';
68
+ this.pid = opts.pid ?? process.pid;
69
+ this.now = opts.now ?? (() => new Date());
70
+ }
71
+ /** Get the current log file path (e.g., ~/.lark-remote/logs/2026-07-11/lark-remote-12345.log). */
72
+ getCurrentLogFile() {
73
+ // Ensure the file is up-to-date before returning
74
+ this.ensureFile(this.now());
75
+ return this.currentFile;
76
+ }
77
+ ensureFile(date) {
78
+ const dateStr = todayStr(date);
79
+ if (this.currentFile && this.currentDate === dateStr)
80
+ return this.currentFile;
81
+ // Date changed (or first call): rotate to a new daily subdirectory
82
+ const dayDir = path.join(this.dir, dateStr);
83
+ fs.mkdirSync(dayDir, { recursive: true });
84
+ this.currentFile = path.join(dayDir, `${this.prefix}-${this.pid}.log`);
85
+ this.currentDate = dateStr;
86
+ // P2-19①: prune daily log directories older than the retention window so
87
+ // the logs directory does not grow without bound (single-day size cap is
88
+ // out of scope; bounded retention by day is the requested guard). Wrapped
89
+ // defensively so cleanup failure never blocks logging.
90
+ this.pruneOldDirs(dateStr);
91
+ return this.currentFile;
92
+ }
93
+ /**
94
+ * P2-19①: remove daily subdirectories older than the retention window.
95
+ * Best-effort — any error is swallowed (logging must never throw).
96
+ */
97
+ pruneOldDirs(currentDateStr) {
98
+ const RETENTION_DAYS = 30;
99
+ try {
100
+ const entries = fs.readdirSync(this.dir, { withFileTypes: true });
101
+ const cutoff = new Date(`${currentDateStr}T00:00:00`).getTime() - RETENTION_DAYS * 86400000;
102
+ for (const entry of entries) {
103
+ if (!entry.isDirectory())
104
+ continue;
105
+ // Only prune directories that look like YYYY-MM-DD daily dirs.
106
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(entry.name))
107
+ continue;
108
+ const t = new Date(`${entry.name}T00:00:00`).getTime();
109
+ if (Number.isNaN(t))
110
+ continue;
111
+ if (t < cutoff) {
112
+ fs.rmSync(path.join(this.dir, entry.name), { recursive: true, force: true });
113
+ }
114
+ }
115
+ }
116
+ catch {
117
+ /* best-effort cleanup; never throw */
118
+ }
119
+ }
120
+ write(level, args) {
121
+ if (LEVEL_ORDER[level] < this.level)
122
+ return;
123
+ const date = this.now();
124
+ const ts = timestampStr(date);
125
+ const message = format(...args);
126
+ const line = `${ts} [${LEVEL_UPPER[level]}] ${message}\n`;
127
+ const file = this.ensureFile(date);
128
+ // P2-19③: never let a disk error (EACCES / ENOSPC / EROFS) escape write().
129
+ // If logging itself throws — especially inside an uncaughtException handler
130
+ // that calls logger.error(...) — a re-throw would abort Node and skip
131
+ // instanceLock.release(), leaking the singleton lock. Logging must never
132
+ // kill the process. Fall back to a single stderr line for diagnosis.
133
+ try {
134
+ fs.appendFileSync(file, line);
135
+ }
136
+ catch {
137
+ try {
138
+ process.stderr.write(`[logger] failed to write log: ${message}\n`);
139
+ }
140
+ catch {
141
+ /* nothing left to do; never throw */
142
+ }
143
+ }
144
+ }
145
+ debug(...args) {
146
+ this.write('debug', args);
147
+ }
148
+ info(...args) {
149
+ this.write('info', args);
150
+ }
151
+ warn(...args) {
152
+ this.write('warn', args);
153
+ }
154
+ error(...args) {
155
+ this.write('error', args);
156
+ }
157
+ /** Reset internal file state so the next write rotates to a fresh daily file. */
158
+ close() {
159
+ this.currentFile = '';
160
+ this.currentDate = '';
161
+ }
162
+ }
163
+ // --- Singleton ---
164
+ let defaultLogger = new Logger();
165
+ /**
166
+ * Initialize (or reinitialize) the global logger. Must be called once at
167
+ * startup, before any logging is expected to land in the right place.
168
+ * Returns the initialized logger.
169
+ */
170
+ export function initLogger(opts) {
171
+ defaultLogger.close();
172
+ defaultLogger = new Logger(opts);
173
+ return defaultLogger;
174
+ }
175
+ export function getLogger() {
176
+ return defaultLogger;
177
+ }
178
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,27 @@
1
+ interface OrderEntry {
2
+ id: string;
3
+ text: string;
4
+ createdAt: string;
5
+ usedAt?: string;
6
+ }
7
+ /**
8
+ * Global persistent order store.
9
+ *
10
+ * Atomic write (tmp + rename) and corrupt-file recovery,
11
+ * following the same pattern as WorkspaceStore.
12
+ */
13
+ export declare class OrderStore {
14
+ private filePath;
15
+ private data;
16
+ constructor(filePath?: string);
17
+ /** Reload data from disk (useful when another process may have written) */
18
+ reload(): void;
19
+ private load;
20
+ private persist;
21
+ save(text: string): OrderEntry;
22
+ updateUsedAt(id: string): void;
23
+ get(): OrderEntry[];
24
+ has(id: string): boolean;
25
+ remove(id: string): void;
26
+ }
27
+ export {};
@@ -0,0 +1,90 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import { atomicWriteJson } from '../persistence/atomic-write.js';
4
+ import { loadJsonFile } from '../persistence/load-json-file.js';
5
+ const MAX_TEXT_LENGTH = 200;
6
+ const MAX_ORDERS = 50;
7
+ /**
8
+ * Global persistent order store.
9
+ *
10
+ * Atomic write (tmp + rename) and corrupt-file recovery,
11
+ * following the same pattern as WorkspaceStore.
12
+ */
13
+ export class OrderStore {
14
+ filePath;
15
+ data;
16
+ constructor(filePath) {
17
+ this.filePath = filePath ?? path.join(os.homedir(), '.lark-remote', 'orders.json');
18
+ this.data = [];
19
+ this.load();
20
+ }
21
+ /** Reload data from disk (useful when another process may have written) */
22
+ reload() {
23
+ this.load();
24
+ }
25
+ load() {
26
+ const parsed = loadJsonFile(this.filePath, undefined);
27
+ if (!parsed) {
28
+ this.data = [];
29
+ return;
30
+ }
31
+ if (!Array.isArray(parsed)) {
32
+ this.data = [];
33
+ return;
34
+ }
35
+ const valid = [];
36
+ for (const e of parsed) {
37
+ if (e &&
38
+ typeof e.id === 'string' &&
39
+ typeof e.text === 'string' &&
40
+ typeof e.createdAt === 'string') {
41
+ const entry = {
42
+ id: e.id,
43
+ text: e.text.slice(0, MAX_TEXT_LENGTH),
44
+ createdAt: e.createdAt,
45
+ };
46
+ if (typeof e.usedAt === 'string')
47
+ entry.usedAt = e.usedAt;
48
+ valid.push(entry);
49
+ }
50
+ }
51
+ this.data = valid;
52
+ }
53
+ persist() {
54
+ atomicWriteJson(this.filePath, this.data);
55
+ }
56
+ save(text) {
57
+ if (text.length > MAX_TEXT_LENGTH) {
58
+ throw new Error(`指令文本超过 ${MAX_TEXT_LENGTH} 字符限制`);
59
+ }
60
+ if (this.data.length >= MAX_ORDERS) {
61
+ throw new Error(`指令数量已达上限 ${MAX_ORDERS}`);
62
+ }
63
+ const entry = {
64
+ id: crypto.randomUUID(),
65
+ text,
66
+ createdAt: new Date().toISOString(),
67
+ };
68
+ this.data.push(entry);
69
+ this.persist();
70
+ return entry;
71
+ }
72
+ updateUsedAt(id) {
73
+ const entry = this.data.find((e) => e.id === id);
74
+ if (entry) {
75
+ entry.usedAt = new Date().toISOString();
76
+ this.persist();
77
+ }
78
+ }
79
+ get() {
80
+ return [...this.data];
81
+ }
82
+ has(id) {
83
+ return this.data.some((e) => e.id === id);
84
+ }
85
+ remove(id) {
86
+ this.data = this.data.filter((e) => e.id !== id);
87
+ this.persist();
88
+ }
89
+ }
90
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Atomically write string content to a file via tmp + rename.
3
+ * Falls back to copy+unlink when rename fails with EXDEV (cross-device).
4
+ */
5
+ export declare function atomicWrite(filePath: string, content: string, encoding?: BufferEncoding): void;
6
+ /**
7
+ * Atomically write JSON data to a file via tmp + rename.
8
+ * Ensures a crash mid-write cannot leave a truncated file.
9
+ */
10
+ export declare function atomicWriteJson(filePath: string, data: unknown): void;
@@ -0,0 +1,65 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ /**
4
+ * Atomically write string content to a file via tmp + rename.
5
+ * Falls back to copy+unlink when rename fails with EXDEV (cross-device).
6
+ */
7
+ export function atomicWrite(filePath, content, encoding = 'utf-8') {
8
+ const dir = path.dirname(filePath);
9
+ fs.mkdirSync(dir, { recursive: true });
10
+ const tmpPath = filePath + '.tmp';
11
+ fs.writeFileSync(tmpPath, content, encoding);
12
+ // fsync the tmp file before rename so its data is durable on disk;
13
+ // rename itself is only a metadata change, so without fsync a crash after
14
+ // rename could leave the target as a 0-byte file. Best-effort: some
15
+ // filesystems (tmpfs) may not support fsync — ignore those errors.
16
+ let fd;
17
+ try {
18
+ fd = fs.openSync(tmpPath, 'r');
19
+ fs.fsyncSync(fd);
20
+ }
21
+ catch {
22
+ /* fsync unavailable on this fs — best-effort durability */
23
+ }
24
+ finally {
25
+ if (fd !== undefined) {
26
+ try {
27
+ fs.closeSync(fd);
28
+ }
29
+ catch {
30
+ /* ignore close failure */
31
+ }
32
+ }
33
+ }
34
+ try {
35
+ fs.renameSync(tmpPath, filePath);
36
+ }
37
+ catch (err) {
38
+ if (err.code === 'EXDEV') {
39
+ // Cross-device rename: copy then unlink (tmp cleanup handled by finally)
40
+ fs.copyFileSync(tmpPath, filePath);
41
+ }
42
+ else {
43
+ throw err;
44
+ }
45
+ }
46
+ finally {
47
+ // Best-effort cleanup: on success the tmp was renamed away (ENOENT here,
48
+ // silently ignored); on EXDEV it was copied and now needs removal; on other
49
+ // errors we must not leak the tmp file.
50
+ try {
51
+ fs.unlinkSync(tmpPath);
52
+ }
53
+ catch {
54
+ /* already moved or never created */
55
+ }
56
+ }
57
+ }
58
+ /**
59
+ * Atomically write JSON data to a file via tmp + rename.
60
+ * Ensures a crash mid-write cannot leave a truncated file.
61
+ */
62
+ export function atomicWriteJson(filePath, data) {
63
+ atomicWrite(filePath, JSON.stringify(data, null, 2));
64
+ }
65
+ //# sourceMappingURL=atomic-write.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Load and parse a JSON file with fallback on corruption.
3
+ * Returns the parsed data or the fallback value if loading fails.
4
+ *
5
+ * @param filePath - Path to the JSON file
6
+ * @param fallback - Value to return if file doesn't exist or is corrupted
7
+ * @param parseFn - Optional custom parser (defaults to JSON.parse)
8
+ */
9
+ export declare function loadJsonFile<T>(filePath: string, fallback: T, parseFn?: (raw: string) => T): T;
@@ -0,0 +1,23 @@
1
+ import fs from 'node:fs';
2
+ import { getLogger } from '../logger/index.js';
3
+ /**
4
+ * Load and parse a JSON file with fallback on corruption.
5
+ * Returns the parsed data or the fallback value if loading fails.
6
+ *
7
+ * @param filePath - Path to the JSON file
8
+ * @param fallback - Value to return if file doesn't exist or is corrupted
9
+ * @param parseFn - Optional custom parser (defaults to JSON.parse)
10
+ */
11
+ export function loadJsonFile(filePath, fallback, parseFn = JSON.parse) {
12
+ try {
13
+ if (!fs.existsSync(filePath))
14
+ return fallback;
15
+ const raw = fs.readFileSync(filePath, 'utf-8');
16
+ return parseFn(raw);
17
+ }
18
+ catch (err) {
19
+ getLogger().warn(`[persistence] failed to load ${filePath}, using fallback:`, err);
20
+ return fallback;
21
+ }
22
+ }
23
+ //# sourceMappingURL=load-json-file.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Self-restart without an external watchdog/cron: the only process that can
3
+ * reliably start the successor is the dying bridge itself. The old process
4
+ * spawns a detached replacement (same executable + argv, hence same
5
+ * --config-dir), then exits cleanly. The child learns the old pid via env
6
+ * and waits for it to die before acquiring the instance lock, so the lock
7
+ * file stays the single source of truth for the handoff.
8
+ */
9
+ /** Env var telling a replacement child which parent pid to wait for before acquiring the instance lock. */
10
+ export declare const RESTART_WAIT_PID_ENV = "LARK_REMOTE_RESTART_WAIT_PID";
11
+ /**
12
+ * Spawn a detached replacement bridge with the same executable and argv
13
+ * (including --config-dir) and return its pid. The caller is expected to
14
+ * exit right after. stdio is redirected to restart-child.log so early
15
+ * startup failures (before the file logger initializes) are not lost.
16
+ */
17
+ export declare function spawnReplacementBridge(logsDir: string): number;
18
+ /**
19
+ * On startup, if this process was spawned as a restart replacement, wait for
20
+ * the previous bridge process to exit (and release the instance lock) before
21
+ * continuing. No-op for normal starts. On timeout, proceed anyway — lock
22
+ * acquisition remains the authority.
23
+ */
24
+ export declare function waitForPreviousInstance(): Promise<void>;