@guenbankhai/tools 0.0.0-stage → 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.
- package/README.md +53 -2
- package/README.vi.md +54 -0
- package/agentmoni/lib/agents/claude/billing.js +13 -0
- package/agentmoni/lib/agents/claude/home.js +4 -0
- package/agentmoni/lib/agents/claude/index.js +181 -0
- package/agentmoni/lib/agents/claude/sources/oauth-usage.js +74 -0
- package/agentmoni/lib/agents/claude/timeline.js +26 -0
- package/agentmoni/lib/agents/codex/billing.js +9 -0
- package/agentmoni/lib/agents/codex/index.js +280 -0
- package/agentmoni/lib/agents/codex/sources/rollout.js +26 -0
- package/agentmoni/lib/agents/codex/timeline.js +22 -0
- package/agentmoni/lib/agents/index.js +13 -0
- package/agentmoni/lib/catalog.js +96 -0
- package/agentmoni/lib/catalog.json +493 -0
- package/agentmoni/lib/collect.js +11 -0
- package/agentmoni/lib/colors.js +35 -0
- package/agentmoni/lib/cost.js +87 -0
- package/agentmoni/lib/format.js +18 -0
- package/agentmoni/lib/graph.js +106 -0
- package/agentmoni/lib/history.js +50 -0
- package/agentmoni/lib/json-fields.js +58 -0
- package/agentmoni/lib/jsonl.js +43 -0
- package/agentmoni/lib/launch.js +118 -0
- package/agentmoni/lib/limits.js +11 -0
- package/agentmoni/lib/links.js +139 -0
- package/agentmoni/lib/list.js +71 -0
- package/agentmoni/lib/log.js +226 -0
- package/agentmoni/lib/monitor.js +58 -0
- package/agentmoni/lib/page.html +591 -0
- package/agentmoni/lib/present.js +184 -0
- package/agentmoni/lib/procs.js +22 -0
- package/agentmoni/lib/quota.js +34 -0
- package/agentmoni/lib/repokey.js +44 -0
- package/agentmoni/lib/share.js +106 -0
- package/agentmoni/lib/state.js +62 -0
- package/agentmoni/lib/terminal.js +143 -0
- package/agentmoni/lib/timeline.js +40 -0
- package/agentmoni/lib/web.js +75 -0
- package/agentmoni/monitor.js +98 -0
- package/package.json +22 -4
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// Durable log, one writer at a time (lock file):
|
|
2
|
+
// _machine/quota-YYYY-MM-DD.jsonl account quota, appended only when a number changes
|
|
3
|
+
// <repo-key>/sessions/<provider>-<id>.json session summary, written atomically when the session changes
|
|
4
|
+
// <repo-key>/events-YYYY-MM-DD.jsonl lifecycle marks only: start, end, resume, link, orphan
|
|
5
|
+
// Events are start, end, resume, link and orphan only (running/waiting flips are not logged). No tool calls or per-turn usage are copied; transcripts stay the source for detail. A bad file or line is skipped with a warning.
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { execFileSync } = require('node:child_process');
|
|
9
|
+
|
|
10
|
+
const DAY_MS = 86400e3;
|
|
11
|
+
const KEEP_DAYS = 30;
|
|
12
|
+
const TICK_MS = 30 * 1000; // minimum gap between rewrites of a summary that only gained tokens or activity
|
|
13
|
+
const safe = s => String(s).replace(/[^\w.-]/g, '_');
|
|
14
|
+
const day = ms => new Date(ms).toISOString().slice(0, 10);
|
|
15
|
+
const fileDay = name => /(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(name)?.[1];
|
|
16
|
+
|
|
17
|
+
function pidAlive(pid) {
|
|
18
|
+
try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Start time of a process (ms), or null when it cannot be read; used to tell a reused pid from the lock owner.
|
|
22
|
+
function processStart(pid) {
|
|
23
|
+
try {
|
|
24
|
+
const out = execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', `(Get-CimInstance Win32_Process -Filter 'ProcessId=${Number(pid)}').CreationDate.ToUniversalTime().ToString('o')`], { encoding: 'utf8', timeout: 15000, windowsHide: true });
|
|
25
|
+
return Date.parse(out.trim()) || null;
|
|
26
|
+
} catch { return null; }
|
|
27
|
+
}
|
|
28
|
+
const LOCK_SLACK_MS = 5000;
|
|
29
|
+
const STALE_TMP_MS = 3600e3;
|
|
30
|
+
|
|
31
|
+
function atomicWrite(file, text) {
|
|
32
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
33
|
+
fs.writeFileSync(temp, text);
|
|
34
|
+
fs.renameSync(temp, file);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createLog({ dir, isAlive = pidAlive, startOf = processStart } = {}) {
|
|
38
|
+
const warnings = [];
|
|
39
|
+
const warn = m => { if (!warnings.includes(m)) warnings.push(m); };
|
|
40
|
+
const lockFile = path.join(dir, '.lock');
|
|
41
|
+
const webFile = path.join(dir, 'web.json');
|
|
42
|
+
const known = new Map(); // "provider:id" -> { summary, material, writtenAt }
|
|
43
|
+
const lastQuota = new Map(); // provider -> JSON of the last logged numbers
|
|
44
|
+
let owner = false;
|
|
45
|
+
|
|
46
|
+
// -> true when this process may write. Another live monitor holding the lock makes us read-only.
|
|
47
|
+
function acquire() {
|
|
48
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
49
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
50
|
+
try { fs.writeFileSync(lockFile, JSON.stringify({ pid: process.pid, startMs: Math.round(Date.now() - process.uptime() * 1000) }), { flag: 'wx' }); owner = true;
|
|
51
|
+
try { fs.unlinkSync(webFile); } catch { /* no previous URL */ }
|
|
52
|
+
return true; }
|
|
53
|
+
catch (e) {
|
|
54
|
+
if (e.code !== 'EEXIST') { warn(`log lock failed: ${e.message}`); return false; }
|
|
55
|
+
// An empty or unreadable lock may be a starter mid-write: treat it as held, never delete it.
|
|
56
|
+
let lock = null;
|
|
57
|
+
try { lock = JSON.parse(fs.readFileSync(lockFile, 'utf8')); } catch { /* handled below */ }
|
|
58
|
+
const pid = typeof lock === 'number' ? lock : lock?.pid;
|
|
59
|
+
if (!Number.isSafeInteger(pid)) { warn('log lock is unreadable: treated as held, read-only'); return false; }
|
|
60
|
+
const real = pid !== process.pid && isAlive(pid) ? startOf(pid) : undefined; // undefined: not running
|
|
61
|
+
const sameProcess = real !== undefined && (real == null || lock.startMs == null || Math.abs(real - lock.startMs) <= LOCK_SLACK_MS);
|
|
62
|
+
if (sameProcess) { warn(`another monitor (pid ${pid}) owns the log: read-only`); return false; }
|
|
63
|
+
try { fs.unlinkSync(lockFile); } catch { /* raced with another starter */ }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
function release() {
|
|
69
|
+
if (!owner) return;
|
|
70
|
+
owner = false;
|
|
71
|
+
try { fs.unlinkSync(webFile); } catch { /* no web */ }
|
|
72
|
+
try { fs.unlinkSync(lockFile); } catch { /* already gone */ }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const repos = () => { try { return fs.readdirSync(dir, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name); } catch { return []; } };
|
|
76
|
+
|
|
77
|
+
// Removes day files and ended summaries older than 30 days.
|
|
78
|
+
function cleanup(now) {
|
|
79
|
+
const cutoff = now - KEEP_DAYS * DAY_MS;
|
|
80
|
+
let removed = 0;
|
|
81
|
+
const drop = file => { try { fs.unlinkSync(file); removed++; } catch (e) { warn(`cannot remove ${file}: ${e.message}`); } };
|
|
82
|
+
// Half-written temp files of a crashed writer (an hour old, so a live writer is never raced).
|
|
83
|
+
for (const d of [dir, ...repos().flatMap(r => [path.join(dir, r), path.join(dir, r, 'sessions')])]) {
|
|
84
|
+
let names = [];
|
|
85
|
+
try { names = fs.readdirSync(d); } catch { continue; }
|
|
86
|
+
for (const n of names.filter(x => x.endsWith('.tmp'))) {
|
|
87
|
+
try { if (fs.statSync(path.join(d, n)).mtimeMs < now - STALE_TMP_MS) drop(path.join(d, n)); } catch { /* gone */ }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const repo of repos()) {
|
|
91
|
+
let names = [];
|
|
92
|
+
try { names = fs.readdirSync(path.join(dir, repo)); } catch { /* unreadable folder */ }
|
|
93
|
+
for (const n of names) {
|
|
94
|
+
const d = fileDay(n);
|
|
95
|
+
if (d && Date.parse(`${d}T23:59:59Z`) < cutoff) drop(path.join(dir, repo, n));
|
|
96
|
+
}
|
|
97
|
+
const sdir = path.join(dir, repo, 'sessions');
|
|
98
|
+
try { names = fs.readdirSync(sdir); } catch { continue; }
|
|
99
|
+
for (const n of names.filter(x => x.endsWith('.json'))) {
|
|
100
|
+
const s = readSummary(path.join(sdir, n));
|
|
101
|
+
if (s && s.state === 'ended' && s.lastActivityAt < cutoff) drop(path.join(sdir, n));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return removed;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readSummary(file) {
|
|
108
|
+
try {
|
|
109
|
+
const s = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
110
|
+
if (typeof s?.provider !== 'string' || typeof s.id !== 'string' || !Number.isFinite(s.startedAt) || !Number.isFinite(s.lastActivityAt)) throw new Error('missing fields');
|
|
111
|
+
return s;
|
|
112
|
+
} catch (e) { warn(`skipped ${file}: ${e.message}`); return null; }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Every summary on disk, newest activity first; remembered so later writes can tell what changed.
|
|
116
|
+
function load() {
|
|
117
|
+
const all = [];
|
|
118
|
+
for (const repo of repos()) {
|
|
119
|
+
const sdir = path.join(dir, repo, 'sessions');
|
|
120
|
+
let names = [];
|
|
121
|
+
try { names = fs.readdirSync(sdir); } catch { continue; }
|
|
122
|
+
for (const n of names.filter(x => x.endsWith('.json'))) {
|
|
123
|
+
const s = readSummary(path.join(sdir, n));
|
|
124
|
+
if (s) { all.push(s); known.set(`${s.provider}:${s.id}`, { summary: s, material: materialOf(s), writtenAt: 0 }); }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const q of loadQuota()) lastQuota.set(q.provider, quotaKey(q));
|
|
128
|
+
return all.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The newest logged numbers per provider, so a restart does not log them again.
|
|
132
|
+
function loadQuota() {
|
|
133
|
+
const m = path.join(dir, '_machine');
|
|
134
|
+
let names = [];
|
|
135
|
+
try { names = fs.readdirSync(m).filter(n => n.startsWith('quota-') && fileDay(n)).sort(); } catch { return []; }
|
|
136
|
+
const latest = new Map();
|
|
137
|
+
for (const n of names.slice(-2)) {
|
|
138
|
+
let bad = 0;
|
|
139
|
+
for (const line of fs.readFileSync(path.join(m, n), 'utf8').split('\n')) {
|
|
140
|
+
if (!line.trim()) continue;
|
|
141
|
+
try { const q = JSON.parse(line); if (q?.provider) latest.set(q.provider, q); } catch { bad++; }
|
|
142
|
+
}
|
|
143
|
+
if (bad) warn(`skipped ${bad} malformed line(s) in ${n}`);
|
|
144
|
+
}
|
|
145
|
+
return [...latest.values()];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const quotaKey = q => JSON.stringify(Object.keys(q).filter(k => k !== 't' && k !== 'provider').sort().map(k => [k, q[k] ? { percent: q[k].percent, resetsAt: q[k].resetsAt } : null]));
|
|
149
|
+
function logQuota(accounts, now) {
|
|
150
|
+
if (!owner) return;
|
|
151
|
+
for (const [provider, acct] of Object.entries(accounts)) {
|
|
152
|
+
// An unknown (stale) window is not a number to log.
|
|
153
|
+
const a = { windows: Object.fromEntries(Object.keys(acct.windows).map(w => [w, acct.windows[w]?.stale ? null : acct.windows[w]])) };
|
|
154
|
+
const row = { provider };
|
|
155
|
+
for (const w of Object.keys(a.windows)) row[w] = a.windows[w] ? { percent: a.windows[w].percent, resetsAt: a.windows[w].resetsAt, readAt: a.windows[w].readAt ?? a.windows[w].at, source: a.windows[w].source } : null;
|
|
156
|
+
if (!Object.values(a.windows).some(Boolean)) continue;
|
|
157
|
+
if (lastQuota.get(provider) === quotaKey(row)) continue;
|
|
158
|
+
lastQuota.set(provider, quotaKey(row));
|
|
159
|
+
append(path.join(dir, '_machine', `quota-${day(now)}.jsonl`), { t: now, ...row });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function append(file, row) {
|
|
164
|
+
try {
|
|
165
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
166
|
+
const fd = fs.openSync(file, 'a');
|
|
167
|
+
try { fs.writeFileSync(fd, JSON.stringify(row) + '\n'); fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
|
|
168
|
+
}
|
|
169
|
+
catch (e) { warn(`cannot write ${file}: ${e.message}`); }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// What identifies a change worth writing at once; tokens, context and activity wait for the tick.
|
|
173
|
+
// running <-> waiting flips are not lifecycle: only ended-or-not counts.
|
|
174
|
+
const lifeOf = s => s.state === 'ended' ? 'ended' : 'alive';
|
|
175
|
+
const materialOf = s => JSON.stringify([lifeOf(s), s.turn === 'aborted', s.parent, s.link?.kind, s.link?.label, s.resume, s.kind, s.name, s.model, s.effort, s.orphan]);
|
|
176
|
+
const summaryOf = (s, repo, now) => ({ v: 1, provider: s.provider, id: s.id, billing: s.billing ?? 'unknown', repo, cwd: s.cwd, model: s.model, effort: s.effort, name: s.name, kind: s.kind, agentType: s.agentType ?? null, state: s.state, turn: s.turn ?? null,
|
|
177
|
+
startedAt: s.startedAt, lastActivityAt: s.lastActivityAt, tokens: s.tokens, context: s.context, parent: s.parent, link: s.link, runs: s.runs, resume: s.resume,
|
|
178
|
+
orphan: s.orphan, share: s.share, writtenAt: now });
|
|
179
|
+
|
|
180
|
+
// sessions must carry .repo. Writes summaries and lifecycle events for what changed since the last call.
|
|
181
|
+
function record(sessions, now) {
|
|
182
|
+
if (!owner) return;
|
|
183
|
+
for (const s of sessions) {
|
|
184
|
+
const k = `${s.provider}:${s.id}`, prev = known.get(k), mat = materialOf(s);
|
|
185
|
+
const events = [];
|
|
186
|
+
const mark = (event, t, extra = {}) => events.push({ t, event, provider: s.provider, id: s.id, ...extra });
|
|
187
|
+
if (!prev) {
|
|
188
|
+
mark('start', s.startedAt, { kind: s.kind, cwd: s.cwd });
|
|
189
|
+
if (s.parent) mark('link', s.startedAt, { parent: s.parent, kind: s.link?.kind, label: s.link?.label });
|
|
190
|
+
if (s.state === 'ended') mark('end', s.lastActivityAt);
|
|
191
|
+
if (s.orphan) mark('orphan', now, { parent: s.parent });
|
|
192
|
+
} else {
|
|
193
|
+
if (s.orphan && !prev.summary.orphan) mark('orphan', now, { parent: s.parent });
|
|
194
|
+
const p = prev.summary;
|
|
195
|
+
if (lifeOf(p) !== lifeOf(s) && s.state === 'ended') mark('end', now);
|
|
196
|
+
if (s.resume > (p.resume ?? 0)) mark('resume', now, { run: s.resume + 1 });
|
|
197
|
+
if (p.parent !== s.parent || p.link?.kind !== s.link?.kind) mark('link', now, { parent: s.parent, kind: s.link?.kind, label: s.link?.label });
|
|
198
|
+
}
|
|
199
|
+
if (prev && prev.material === mat && now - prev.writtenAt < TICK_MS) continue;
|
|
200
|
+
const summary = summaryOf(s, s.repo, now);
|
|
201
|
+
if (prev && prev.material === mat && JSON.stringify({ ...summary, state: lifeOf(summary), writtenAt: 0 }) === JSON.stringify({ ...prev.summary, state: lifeOf(prev.summary), writtenAt: 0 })) continue;
|
|
202
|
+
const file = path.join(dir, s.repo, 'sessions', `${safe(s.provider)}-${safe(s.id)}.json`);
|
|
203
|
+
try {
|
|
204
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
205
|
+
atomicWrite(file, JSON.stringify(summary));
|
|
206
|
+
known.set(k, { summary, material: mat, writtenAt: now });
|
|
207
|
+
for (const e of events) append(path.join(dir, s.repo, `events-${day(e.t)}.jsonl`), e);
|
|
208
|
+
} catch (e) { warn(`cannot write ${file}: ${e.message}`); }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function publishWeb(url) {
|
|
213
|
+
if (owner) atomicWrite(webFile, JSON.stringify({ pid: process.pid, url }));
|
|
214
|
+
}
|
|
215
|
+
function readWeb() {
|
|
216
|
+
try {
|
|
217
|
+
const lock = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
|
|
218
|
+
const web = JSON.parse(fs.readFileSync(webFile, 'utf8'));
|
|
219
|
+
return web.pid === (lock.pid ?? lock) && /^http:\/\/127\.0\.0\.1:\d+$/.test(web.url) ? web.url : null;
|
|
220
|
+
} catch { return null; }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return { acquire, release, cleanup, load, record, logQuota, publishWeb, readWeb, warnings, get owner() { return owner; } };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = { createLog };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The refresh loop body: sessions + links + account quota + shares + log. Everything external is injectable.
|
|
2
|
+
const os = require('node:os');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { load } = require('./catalog.js');
|
|
5
|
+
const { collect } = require('./collect.js');
|
|
6
|
+
const { createTracker } = require('./state.js');
|
|
7
|
+
const agents = require('./agents/index.js');
|
|
8
|
+
const { quotaProvidersOf } = require('./present.js');
|
|
9
|
+
const { createQuota } = require('./quota.js');
|
|
10
|
+
const { computeShares } = require('./share.js');
|
|
11
|
+
const { createLog } = require('./log.js');
|
|
12
|
+
const { repoKey } = require('./repokey.js');
|
|
13
|
+
|
|
14
|
+
const DAY_MS = 86400e3;
|
|
15
|
+
const RETRY_LOCK_MS = 30 * 1000;
|
|
16
|
+
// Logs live outside the repo: $AGENTMONI_HOME, else %LOCALAPPDATA%\agentmoni, else ~/.agentmoni.
|
|
17
|
+
const defaultLogDir = () => process.env.AGENTMONI_HOME || (process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'agentmoni') : path.join(os.homedir(), '.agentmoni'));
|
|
18
|
+
|
|
19
|
+
// days: how far back transcripts are read; it must cover the longest declared window plus a day of margin.
|
|
20
|
+
function createMonitor({ registry = agents, catalog = load(), logDir = defaultLogDir(), days = 8, fetch, collectOptions = {}, isAlive, startOf, ...agentOptions } = {}) {
|
|
21
|
+
const tracker = createTracker();
|
|
22
|
+
const log = createLog({ dir: logDir, isAlive, startOf, ...agentOptions });
|
|
23
|
+
let saved = [];
|
|
24
|
+
const quota = createQuota({ ...agentOptions, logDir, fetch }, registry);
|
|
25
|
+
|
|
26
|
+
// Takes the log lock (read-only when another monitor holds it), drops files older than 30 days, and restores runs
|
|
27
|
+
// and history from the summaries so a restart keeps what the last monitor saw.
|
|
28
|
+
let triedAt = 0;
|
|
29
|
+
function start(now = Date.now()) {
|
|
30
|
+
triedAt = now;
|
|
31
|
+
if (log.acquire()) log.cleanup(now);
|
|
32
|
+
saved = log.load();
|
|
33
|
+
for (const s of saved) tracker.restore(`${s.provider}:${s.id}`, s.runs);
|
|
34
|
+
}
|
|
35
|
+
function stop() { log.release(); }
|
|
36
|
+
|
|
37
|
+
// waitQuota: wait for the usage endpoint (one-shot tools); the live loop reads the cache and lets the request finish by itself.
|
|
38
|
+
async function refresh({ now = Date.now(), waitQuota = false } = {}) {
|
|
39
|
+
// A read-only monitor takes the log over when the owner is gone.
|
|
40
|
+
if (!log.owner && now - triedAt >= RETRY_LOCK_MS) { triedAt = now; if (log.acquire()) { log.cleanup(now); saved = log.load(); } }
|
|
41
|
+
const { sessions, ambiguous } = collect({ since: now - days * DAY_MS, now, tracker, registry, ...collectOptions });
|
|
42
|
+
const quotaProviders = quotaProvidersOf({ sessions, history: saved.map(s => ({ ...s, state: 'ended' })) }, registry);
|
|
43
|
+
const asking = quota.refresh(now, quotaProviders, sessions);
|
|
44
|
+
if (waitQuota) await asking;
|
|
45
|
+
const accounts = quota.read(sessions, now);
|
|
46
|
+
const shares = computeShares({ catalog, sessions, history: saved.filter(s => !new Set(sessions.map(x => `${x.provider}:${x.id}`)).has(`${s.provider}:${s.id}`)), accounts, now });
|
|
47
|
+
for (const s of shares.sessions) s.repo = repoKey(s.cwd);
|
|
48
|
+
log.record(shares.sessions, now);
|
|
49
|
+
log.logQuota(Object.fromEntries(quotaProviders.map(p => [p, accounts[p]])), now);
|
|
50
|
+
// Sessions whose transcripts are gone (or older than `days`) are still known from their summaries.
|
|
51
|
+
const live = new Set(shares.sessions.map(s => `${s.provider}:${s.id}`));
|
|
52
|
+
const history = saved.filter(s => !live.has(`${s.provider}:${s.id}`)).map(s => ({ ...s, state: 'ended', fromLog: true }));
|
|
53
|
+
return { sessions: shares.sessions, history, accounts, outside: shares.outside, warnings: [...shares.warnings, ...log.warnings], ambiguous };
|
|
54
|
+
}
|
|
55
|
+
return { start, stop, refresh, catalog, log };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { createMonitor, defaultLogDir };
|