@stratusagent/cli 0.2.2 → 0.3.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.
package/dist/logs.d.ts ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * One line of `~/.stratus/logs/stratusd.jsonl`. Structured rather than
3
+ * pretty-printed because the interesting questions are filters — what did
4
+ * this agent do, what happened in this session — and a whole roster
5
+ * answering at once makes an unattributed stream unreadable.
6
+ */
7
+ export interface LogRecord {
8
+ /** ISO-8601, always UTC. */
9
+ ts: string;
10
+ level: 'info' | 'warn' | 'event';
11
+ /** Event type for level 'event'; absent for daemon lines. */
12
+ event?: string;
13
+ /** Human-readable line for level 'info' and 'warn'. */
14
+ msg?: string;
15
+ agentId?: string;
16
+ sessionId?: string;
17
+ /** Event-specific fields: tool name, status, part count, error text. */
18
+ detail?: Record<string, unknown>;
19
+ }
20
+ export declare const LOG_FILENAME = "stratusd.jsonl";
21
+ /** Rotate past this size. Small enough to stay greppable, large enough to hold a busy day. */
22
+ export declare const LOG_MAX_BYTES: number;
23
+ /** Rotated generations kept beside the live file. */
24
+ export declare const LOG_KEEP = 3;
25
+ export interface LogWriter {
26
+ write(record: LogRecord): Promise<void>;
27
+ /**
28
+ * Resolves once every queued write has reached disk. Callers on the hot
29
+ * path drop the promise `write` returns, so a process that exits without
30
+ * flushing loses whatever is still queued — which is exactly the last
31
+ * few lines, the ones saying why it stopped.
32
+ */
33
+ flush(): Promise<void>;
34
+ /** Absolute path of the live log file. */
35
+ readonly path: string;
36
+ }
37
+ /**
38
+ * Files a service manager redirects the daemon's stdout/stderr into. They
39
+ * are plain files it holds open with no rotation of its own, so they grow
40
+ * for the life of the daemon — and everything in them is already in this
41
+ * log, which does rotate. Truncated alongside a rotation rather than
42
+ * deleted: launchd and systemd keep writing to the same open descriptor.
43
+ */
44
+ export declare const REDIRECT_LOG_FILENAMES: string[];
45
+ /**
46
+ * Truncate the manager's redirect files if they have grown past `maxBytes`.
47
+ *
48
+ * Called on rotation AND on its own schedule, because the two can come
49
+ * apart: a daemon crash-looping at startup, or one whose structured
50
+ * logging is itself failing, produces launchd diagnostics without ever
51
+ * producing enough records to rotate the JSONL. Tying this to rotation
52
+ * alone would leave exactly that case unbounded.
53
+ */
54
+ export declare const truncateRedirectLogs: (dir: string, maxBytes?: number) => Promise<void>;
55
+ export interface LogWriterOptions {
56
+ dir: string;
57
+ maxBytes?: number;
58
+ keep?: number;
59
+ /** Reports a logging failure without taking the daemon down with it. */
60
+ onError?: (error: unknown) => void;
61
+ }
62
+ /**
63
+ * Appends JSONL, rotating by size. Writes are serialized through a promise
64
+ * chain: concurrent turns emit events from independent async contexts, and
65
+ * interleaved appends would split lines down the middle.
66
+ */
67
+ export declare const createLogWriter: (options: LogWriterOptions) => LogWriter;
68
+ export interface LogFilter {
69
+ agentId?: string;
70
+ sessionId?: string;
71
+ }
72
+ export declare const matchesFilter: (record: LogRecord, filter: LogFilter) => boolean;
73
+ /** Malformed lines are skipped: a torn final line must not end the read. */
74
+ export declare const parseLogLines: (chunk: string) => LogRecord[];
75
+ /**
76
+ * The most recent `limit` records, oldest first, reaching back through
77
+ * rotated generations when the live file is short — a rotation must not
78
+ * make `stratus logs` look like the daemon just started.
79
+ */
80
+ export declare const readRecentRecords: (dir: string, limit: number, filter?: LogFilter,
81
+ /**
82
+ * Stop where a follower will resume. The bound is an offset INTO A
83
+ * SPECIFIC FILE: if the log rotated between capturing it and reading
84
+ * here, applying the offset to the replacement would truncate the wrong
85
+ * file and re-read the whole of the old one, duplicating everything past
86
+ * the cutoff. Matching by inode puts the bound back on the right file.
87
+ */
88
+ until?: LogPosition) => Promise<LogRecord[]>;
89
+ /**
90
+ * Where a follower should resume from: a byte offset AND the identity of
91
+ * the file it refers to. The offset alone is meaningless after a rotation,
92
+ * because the bytes it counted are in a different file by then.
93
+ */
94
+ export interface LogPosition {
95
+ offset: number;
96
+ /** inode of the live file; undefined when there is no file yet. */
97
+ ino?: number;
98
+ }
99
+ export declare const currentLogPosition: (dir: string) => Promise<LogPosition>;
100
+ export interface TailOptions {
101
+ dir: string;
102
+ filter?: LogFilter;
103
+ /** Poll interval; the daemon writes from another process, so there is nothing to await. */
104
+ intervalMs?: number;
105
+ signal?: AbortSignal;
106
+ /**
107
+ * Where to resume. Callers that print a backlog first must capture this
108
+ * BEFORE reading it (`currentLogPosition`), or records written in between
109
+ * are in neither the backlog nor the stream — and the file identity has
110
+ * to travel with the offset, or a rotation in that same window makes the
111
+ * offset point into the wrong file.
112
+ */
113
+ startPosition?: LogPosition;
114
+ onRecord: (record: LogRecord) => void;
115
+ }
116
+ /**
117
+ * Follows the live log from its current end. Polls rather than watching:
118
+ * fs.watch semantics differ across platforms and miss rotation, and a
119
+ * rotation mid-tail is exactly when you are reading the log.
120
+ */
121
+ export declare const tailLog: (options: TailOptions) => Promise<void>;
122
+ export declare const formatLogRecord: (record: LogRecord) => string;
123
+ //# sourceMappingURL=logs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logs.d.ts","sourceRoot":"","sources":["../src/logs.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,4BAA4B;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IACjC,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,eAAO,MAAM,YAAY,mBAAmB,CAAC;AAC7C,8FAA8F;AAC9F,eAAO,MAAM,aAAa,QAAkB,CAAC;AAC7C,qDAAqD;AACrD,eAAO,MAAM,QAAQ,IAAI,CAAC;AAE1B,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC;;;;;OAKG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,UAA2C,CAAC;AAE/E;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAAU,KAAK,MAAM,EAAE,iBAAwB,KAAG,OAAO,CAAC,IAAI,CAW9F,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAID;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,SAAS,gBAAgB,KAAG,SA6D3D,CAAC;AAEF,MAAM,WAAW,SAAS;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,aAAa,GAAI,QAAQ,SAAS,EAAE,QAAQ,SAAS,KAAG,OAQpE,CAAC;AAEF,4EAA4E;AAC5E,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,SAAS,EAiBtD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAC5B,KAAK,MAAM,EACX,OAAO,MAAM,EACb,SAAQ,SAAc;AACtB;;;;;;GAMG;AACH,QAAQ,WAAW,KAClB,OAAO,CAAC,SAAS,EAAE,CAkErB,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,kBAAkB,GAAU,KAAK,MAAM,KAAG,OAAO,CAAC,WAAW,CAGzC,CAAC;AAuBlC,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,2FAA2F;IAC3F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,WAAW,CAAC;IAC5B,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC;CACvC;AAED;;;;GAIG;AACH,eAAO,MAAM,OAAO,GAAU,SAAS,WAAW,KAAG,OAAO,CAAC,IAAI,CAwFhE,CAAC;AAaF,eAAO,MAAM,eAAe,GAAI,QAAQ,SAAS,KAAG,MAmBnD,CAAC"}
package/dist/logs.js ADDED
@@ -0,0 +1,358 @@
1
+ import { appendFile, chmod, mkdir, open, readdir, rename, stat, unlink } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ export const LOG_FILENAME = 'stratusd.jsonl';
4
+ /** Rotate past this size. Small enough to stay greppable, large enough to hold a busy day. */
5
+ export const LOG_MAX_BYTES = 8 * 1024 * 1024;
6
+ /** Rotated generations kept beside the live file. */
7
+ export const LOG_KEEP = 3;
8
+ /**
9
+ * Files a service manager redirects the daemon's stdout/stderr into. They
10
+ * are plain files it holds open with no rotation of its own, so they grow
11
+ * for the life of the daemon — and everything in them is already in this
12
+ * log, which does rotate. Truncated alongside a rotation rather than
13
+ * deleted: launchd and systemd keep writing to the same open descriptor.
14
+ */
15
+ export const REDIRECT_LOG_FILENAMES = ['stratusd.out.log', 'stratusd.err.log'];
16
+ /**
17
+ * Truncate the manager's redirect files if they have grown past `maxBytes`.
18
+ *
19
+ * Called on rotation AND on its own schedule, because the two can come
20
+ * apart: a daemon crash-looping at startup, or one whose structured
21
+ * logging is itself failing, produces launchd diagnostics without ever
22
+ * producing enough records to rotate the JSONL. Tying this to rotation
23
+ * alone would leave exactly that case unbounded.
24
+ */
25
+ export const truncateRedirectLogs = async (dir, maxBytes = LOG_MAX_BYTES) => {
26
+ const { truncate } = await import('node:fs/promises');
27
+ for (const name of REDIRECT_LOG_FILENAMES) {
28
+ const target = path.join(dir, name);
29
+ const size = await stat(target).then((info) => info.size).catch(() => 0);
30
+ if (size >= maxBytes) {
31
+ // O_APPEND writers resume at the new end, so the daemon keeps
32
+ // logging; only the duplicate history goes.
33
+ await truncate(target, 0).catch(() => undefined);
34
+ }
35
+ }
36
+ };
37
+ const rotatedName = (index) => `${LOG_FILENAME}.${index}`;
38
+ /**
39
+ * Appends JSONL, rotating by size. Writes are serialized through a promise
40
+ * chain: concurrent turns emit events from independent async contexts, and
41
+ * interleaved appends would split lines down the middle.
42
+ */
43
+ export const createLogWriter = (options) => {
44
+ const maxBytes = options.maxBytes ?? LOG_MAX_BYTES;
45
+ const keep = options.keep ?? LOG_KEEP;
46
+ const logPath = path.join(options.dir, LOG_FILENAME);
47
+ let queue = Promise.resolve();
48
+ let ensured = false;
49
+ const rotate = async () => {
50
+ await truncateRedirectLogs(options.dir, maxBytes);
51
+ // Oldest first, so each generation moves into a free slot.
52
+ for (let index = keep; index >= 1; index -= 1) {
53
+ const from = path.join(options.dir, index === 1 ? LOG_FILENAME : rotatedName(index - 1));
54
+ const to = path.join(options.dir, rotatedName(index));
55
+ if (index === keep) {
56
+ await unlink(to).catch(() => undefined);
57
+ }
58
+ await rename(from, to).catch((error) => {
59
+ if (error.code !== 'ENOENT') {
60
+ throw error;
61
+ }
62
+ });
63
+ }
64
+ };
65
+ const append = async (record) => {
66
+ if (!ensured) {
67
+ await mkdir(options.dir, { recursive: true, mode: 0o700 });
68
+ ensured = true;
69
+ }
70
+ const size = await stat(logPath).then((info) => info.size).catch(() => 0);
71
+ if (size >= maxBytes) {
72
+ await rotate();
73
+ }
74
+ // Sessions carry prompts and tool output, so the log is as sensitive
75
+ // as the transcript it describes. appendFile's mode applies only when
76
+ // it CREATES the file, so a log that already exists — from an older
77
+ // build, a restored backup, a different umask — would keep whatever
78
+ // permissions it had while the daemon kept writing to it.
79
+ await chmod(logPath, 0o600).catch((error) => {
80
+ if (error.code !== 'ENOENT') {
81
+ throw error;
82
+ }
83
+ });
84
+ await appendFile(logPath, `${JSON.stringify(record)}\n`, { mode: 0o600 });
85
+ };
86
+ return {
87
+ path: logPath,
88
+ write(record) {
89
+ // A logging failure must never fail the turn that produced the line.
90
+ queue = queue.then(() => append(record)).catch((error) => {
91
+ options.onError?.(error);
92
+ });
93
+ return queue;
94
+ },
95
+ flush() {
96
+ // The chain already swallows its own errors through onError, so this
97
+ // settles rather than rejecting into a shutdown path.
98
+ return queue;
99
+ },
100
+ };
101
+ };
102
+ export const matchesFilter = (record, filter) => {
103
+ if (filter.agentId && record.agentId !== filter.agentId) {
104
+ return false;
105
+ }
106
+ if (filter.sessionId && record.sessionId !== filter.sessionId) {
107
+ return false;
108
+ }
109
+ return true;
110
+ };
111
+ /** Malformed lines are skipped: a torn final line must not end the read. */
112
+ export const parseLogLines = (chunk) => {
113
+ const records = [];
114
+ for (const line of chunk.split('\n')) {
115
+ const trimmed = line.trim();
116
+ if (trimmed.length === 0) {
117
+ continue;
118
+ }
119
+ try {
120
+ const parsed = JSON.parse(trimmed);
121
+ if (typeof parsed?.ts === 'string' && typeof parsed?.level === 'string') {
122
+ records.push(parsed);
123
+ }
124
+ }
125
+ catch {
126
+ continue;
127
+ }
128
+ }
129
+ return records;
130
+ };
131
+ /**
132
+ * The most recent `limit` records, oldest first, reaching back through
133
+ * rotated generations when the live file is short — a rotation must not
134
+ * make `stratus logs` look like the daemon just started.
135
+ */
136
+ export const readRecentRecords = async (dir, limit, filter = {},
137
+ /**
138
+ * Stop where a follower will resume. The bound is an offset INTO A
139
+ * SPECIFIC FILE: if the log rotated between capturing it and reading
140
+ * here, applying the offset to the replacement would truncate the wrong
141
+ * file and re-read the whole of the old one, duplicating everything past
142
+ * the cutoff. Matching by inode puts the bound back on the right file.
143
+ */
144
+ until) => {
145
+ let names;
146
+ try {
147
+ names = await readdir(dir);
148
+ }
149
+ catch (error) {
150
+ if (error.code === 'ENOENT') {
151
+ return [];
152
+ }
153
+ throw error;
154
+ }
155
+ // Newest first: the live file, then .1, .2, …
156
+ const ordered = [
157
+ ...(names.includes(LOG_FILENAME) ? [LOG_FILENAME] : []),
158
+ ...names
159
+ .filter((name) => name.startsWith(`${LOG_FILENAME}.`))
160
+ .sort((left, right) => Number(left.split('.').pop()) - Number(right.split('.').pop())),
161
+ ];
162
+ // slice(-0) is slice(0), which would return everything — the opposite of
163
+ // what `-n 0` asks for, and `-n 0 -f` is the ordinary way to follow
164
+ // without replaying the file.
165
+ if (limit === 0) {
166
+ return [];
167
+ }
168
+ const collected = [];
169
+ const { readFile } = await import('node:fs/promises');
170
+ // Which file the cutoff belongs to now — the live one until a rotation
171
+ // moved it, and nothing at all once it has been evicted.
172
+ let boundedName;
173
+ if (until !== undefined) {
174
+ if (until.ino === undefined) {
175
+ boundedName = LOG_FILENAME;
176
+ }
177
+ else {
178
+ const liveIno = await stat(path.join(dir, LOG_FILENAME)).then((info) => info.ino).catch(() => undefined);
179
+ if (liveIno === until.ino) {
180
+ boundedName = LOG_FILENAME;
181
+ }
182
+ else {
183
+ const rotated = (await rotatedGenerations(dir)).find((entry) => entry.ino === until.ino);
184
+ boundedName = rotated ? path.basename(rotated.path) : undefined;
185
+ }
186
+ }
187
+ }
188
+ // Everything newer than the file the cutoff lives in is, by definition,
189
+ // after the cutoff — it belongs to the follower, not the backlog.
190
+ const scanned = until === undefined
191
+ ? ordered
192
+ : boundedName === undefined
193
+ // The cutoff's file has been evicted, so every retained generation
194
+ // was written after it.
195
+ ? []
196
+ : ordered.slice(ordered.indexOf(boundedName));
197
+ for (const name of scanned) {
198
+ const raw = await readFile(path.join(dir, name)).catch(() => Buffer.alloc(0));
199
+ // Sliced as bytes, not characters: the offset a follower resumes from
200
+ // is a byte count, and a multi-byte character would shift the seam.
201
+ const contents = (until !== undefined && name === boundedName
202
+ ? raw.subarray(0, until.offset)
203
+ : raw).toString('utf8');
204
+ const matching = parseLogLines(contents).filter((record) => matchesFilter(record, filter));
205
+ collected.unshift(...matching);
206
+ if (collected.length >= limit) {
207
+ break;
208
+ }
209
+ }
210
+ return collected.slice(-limit);
211
+ };
212
+ export const currentLogPosition = async (dir) => stat(path.join(dir, LOG_FILENAME))
213
+ .then((info) => ({ offset: info.size, ino: info.ino }))
214
+ .catch(() => ({ offset: 0 }));
215
+ /** Rotated generations present on disk, newest first, with their inodes. */
216
+ const rotatedGenerations = async (dir) => {
217
+ const names = await readdir(dir).catch(() => []);
218
+ const entries = [];
219
+ for (const name of names) {
220
+ if (!name.startsWith(`${LOG_FILENAME}.`)) {
221
+ continue;
222
+ }
223
+ const index = Number(name.slice(LOG_FILENAME.length + 1));
224
+ if (!Number.isInteger(index) || index < 1) {
225
+ continue;
226
+ }
227
+ const full = path.join(dir, name);
228
+ const ino = await stat(full).then((info) => info.ino).catch(() => undefined);
229
+ if (ino !== undefined) {
230
+ entries.push({ index, path: full, ino });
231
+ }
232
+ }
233
+ return entries.sort((left, right) => left.index - right.index);
234
+ };
235
+ /**
236
+ * Follows the live log from its current end. Polls rather than watching:
237
+ * fs.watch semantics differ across platforms and miss rotation, and a
238
+ * rotation mid-tail is exactly when you are reading the log.
239
+ */
240
+ export const tailLog = async (options) => {
241
+ const intervalMs = options.intervalMs ?? 400;
242
+ const filter = options.filter ?? {};
243
+ const logPath = path.join(options.dir, LOG_FILENAME);
244
+ const start = options.startPosition ?? await currentLogPosition(options.dir);
245
+ let offset = start.offset;
246
+ let carry = '';
247
+ // Rotation is a file-identity change, not a size change. A burst can
248
+ // rotate and grow the replacement past the old offset before the next
249
+ // poll, and a size comparison reads that as "the file just grew" —
250
+ // skipping the replacement's prefix and never draining what moved to .1.
251
+ let liveIno = start.ino;
252
+ /** Reads from `from` to EOF, emitting whole lines. Returns the new offset. */
253
+ const drain = async (target, from) => {
254
+ const handle = await open(target, 'r').catch(() => undefined);
255
+ if (!handle) {
256
+ return from;
257
+ }
258
+ try {
259
+ const size = await handle.stat().then((info) => info.size);
260
+ if (size <= from) {
261
+ return from;
262
+ }
263
+ const length = size - from;
264
+ const buffer = Buffer.alloc(length);
265
+ const { bytesRead } = await handle.read(buffer, 0, length, from);
266
+ const text = carry + buffer.subarray(0, bytesRead).toString('utf8');
267
+ // A partial trailing line is held back until its newline lands.
268
+ const lastBreak = text.lastIndexOf('\n');
269
+ carry = lastBreak >= 0 ? text.slice(lastBreak + 1) : text;
270
+ for (const record of parseLogLines(lastBreak >= 0 ? text.slice(0, lastBreak) : '')) {
271
+ if (matchesFilter(record, filter)) {
272
+ options.onRecord(record);
273
+ }
274
+ }
275
+ return from + bytesRead;
276
+ }
277
+ finally {
278
+ await handle.close();
279
+ }
280
+ };
281
+ while (!options.signal?.aborted) {
282
+ const info = await stat(logPath).catch(() => undefined);
283
+ if (info) {
284
+ if (liveIno !== undefined && info.ino !== liveIno) {
285
+ // The file we were reading has been rotated away. It is not
286
+ // necessarily .1: several rotations can happen between polls under
287
+ // a burst, in which case it is .2 or later and every generation
288
+ // newer than it is a complete file we never saw at all. Find it by
289
+ // inode, drain the rest of it, then walk forward through the
290
+ // generations it was pushed past — anything less loses whole
291
+ // files, and draining by position instead of identity would replay
292
+ // records rather than recover them.
293
+ const generations = await rotatedGenerations(options.dir);
294
+ const previous = generations.find((entry) => entry.ino === liveIno);
295
+ if (previous) {
296
+ await drain(previous.path, offset).catch(() => offset);
297
+ carry = '';
298
+ for (const entry of generations.filter((candidate) => candidate.index < previous.index).reverse()) {
299
+ await drain(entry.path, 0).catch(() => 0);
300
+ carry = '';
301
+ }
302
+ }
303
+ offset = 0;
304
+ carry = '';
305
+ }
306
+ liveIno = info.ino;
307
+ offset = await drain(logPath, offset);
308
+ }
309
+ if (options.signal?.aborted) {
310
+ return;
311
+ }
312
+ await new Promise((resolve) => {
313
+ // Both paths detach the listener: {once:true} only fires on abort, so
314
+ // a poll loop running for days would otherwise retain one closure per
315
+ // tick — roughly 216,000 a day at the default interval.
316
+ const onAbort = () => {
317
+ clearTimeout(timer);
318
+ resolve();
319
+ };
320
+ const timer = setTimeout(() => {
321
+ options.signal?.removeEventListener('abort', onAbort);
322
+ resolve();
323
+ }, intervalMs);
324
+ options.signal?.addEventListener('abort', onAbort, { once: true });
325
+ });
326
+ }
327
+ };
328
+ /** `14:32:07 ava tool.called memory.remember` — time, who, what. */
329
+ const formatDetail = (detail) => {
330
+ if (detail === undefined || detail === null) {
331
+ return '';
332
+ }
333
+ if (typeof detail !== 'object') {
334
+ return String(detail);
335
+ }
336
+ return Object.entries(detail).map(([key, value]) => `${key}=${String(value)}`).join(' ');
337
+ };
338
+ export const formatLogRecord = (record) => {
339
+ const time = record.ts.slice(11, 19);
340
+ const who = (record.agentId ?? '—').padEnd(12);
341
+ if (record.level === 'event') {
342
+ // Records are parsed from a file, not handed over in-process: a
343
+ // hand-edited line or one written by another version can carry a
344
+ // string where this version writes an object, and spreading that
345
+ // into entries renders a word one character per pair.
346
+ const detail = formatDetail(record.detail);
347
+ // The whole session id, never a prefix of it. A channel id is
348
+ // `channel:agent:team:conversation[:thread]`, so eight characters of
349
+ // one is `slack:av` — the same string for every Slack conversation on
350
+ // the machine. This is also the column you copy into `stratus logs
351
+ // --session`, which matches on equality, so a prefix finds nothing.
352
+ const session = record.sessionId ? ` [${record.sessionId}]` : '';
353
+ return `${time} ${who}${record.event ?? 'event'}${detail ? ` ${detail}` : ''}${session}`;
354
+ }
355
+ const prefix = record.level === 'warn' ? 'warning: ' : '';
356
+ return `${time} ${who}${prefix}${record.msg ?? ''}`;
357
+ };
358
+ //# sourceMappingURL=logs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logs.js","sourceRoot":"","sources":["../src/logs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Running `stratus serve` as a managed service.
3
+ *
4
+ * `serve` itself stays a plain foreground process — debuggable, drains on
5
+ * SIGTERM, composes with any supervisor. Everything about surviving logout,
6
+ * crashes, and reboots belongs here, in the platform's own service manager,
7
+ * rather than in a hand-rolled daemonizer.
8
+ */
9
+ export type ServicePlatform = 'launchd' | 'systemd';
10
+ /** Reverse-DNS on macOS, a plain unit name on Linux. */
11
+ export declare const SERVICE_LABEL = "com.stratusagent.stratusd";
12
+ export declare const SYSTEMD_UNIT = "stratusd.service";
13
+ export interface ServiceCommandResult {
14
+ code: number;
15
+ stdout: string;
16
+ stderr: string;
17
+ }
18
+ /** Runs a service-manager command. Injectable so tests never touch launchctl. */
19
+ export type ServiceRunner = (command: string, args: string[]) => Promise<ServiceCommandResult>;
20
+ export interface ServiceEnvironment {
21
+ platform?: NodeJS.Platform;
22
+ homeDir?: string;
23
+ /** Directory the install ran from; becomes the unit's working directory. */
24
+ cwd?: string;
25
+ /** Absolute path of the node binary that should run the daemon. */
26
+ execPath?: string;
27
+ /** Absolute path of the CLI entrypoint (bin.js). */
28
+ scriptPath?: string;
29
+ /** Node flags the entrypoint needs. Defaults to this process's own. */
30
+ execArgv?: string[];
31
+ /** User id, for launchctl's `gui/<uid>` domain. */
32
+ uid?: number;
33
+ run?: ServiceRunner;
34
+ }
35
+ export declare const servicePlatform: (env?: ServiceEnvironment) => ServicePlatform | undefined;
36
+ export declare const serviceUnitPath: (env?: ServiceEnvironment) => string;
37
+ export interface ServiceDefinition {
38
+ /** node, then the CLI entrypoint, then the subcommand. */
39
+ argv: string[];
40
+ logDir: string;
41
+ workingDirectory: string;
42
+ /** Start automatically when the user logs in. */
43
+ runAtLogin: boolean;
44
+ }
45
+ export interface ServiceInstallOptions {
46
+ runAtLogin?: boolean;
47
+ /**
48
+ * Config the daemon should load, absolute. The unit runs from the home
49
+ * directory, so a relative path — or none, when setup wrote somewhere
50
+ * other than the discovered location — would start the daemon on a
51
+ * different config than the one just saved, with a different roster.
52
+ */
53
+ configPath?: string;
54
+ }
55
+ export declare const launchdPlist: (definition: ServiceDefinition) => string;
56
+ export declare const systemdUnit: (definition: ServiceDefinition) => string;
57
+ export declare const serviceDefinition: (env: ServiceEnvironment, runAtLogin: boolean, configPath?: string) => ServiceDefinition;
58
+ /** A service command that has not answered by now is not going to. */
59
+ export declare const SERVICE_COMMAND_TIMEOUT_MS = 15000;
60
+ export interface ServiceStatus {
61
+ platform: ServicePlatform;
62
+ /** A unit file exists on disk. */
63
+ installed: boolean;
64
+ /** The service manager reports it running. */
65
+ running: boolean;
66
+ /** Starts automatically at login; undefined when the manager could not say. */
67
+ runAtLogin: boolean | undefined;
68
+ unitPath: string;
69
+ /** Present when the platform's status command failed outright. */
70
+ detail?: string;
71
+ }
72
+ export declare const readServiceStatus: (env?: ServiceEnvironment) => Promise<ServiceStatus | undefined>;
73
+ export interface ServiceActionResult {
74
+ ok: boolean;
75
+ /** Lines to show the user, in order. */
76
+ messages: string[];
77
+ }
78
+ export declare const installService: (env?: ServiceEnvironment, options?: ServiceInstallOptions) => Promise<ServiceActionResult>;
79
+ export declare const uninstallService: (env?: ServiceEnvironment) => Promise<ServiceActionResult>;
80
+ export declare const startService: (env?: ServiceEnvironment) => Promise<ServiceActionResult>;
81
+ export declare const stopService: (env?: ServiceEnvironment) => Promise<ServiceActionResult>;
82
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}