@volter/supercode-teams 0.3.113 → 0.3.115

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/bin/workspace.mjs CHANGED
@@ -1010,13 +1010,19 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
1010
1010
  const sync = saved ? { ...(readSyncStatus(state.directory) ?? { not_syncing: [], last_error: null, queued_captures: 0 }), agent_identity: agentIdentityStatus(teamsHome()) } : null;
1011
1011
  // The mail this machine's mailbox still has to deliver: after a failed attempt, or for a machine that is offline.
1012
1012
  let mailWaiting = [];
1013
- try { mailWaiting = JSON.parse(execFileSync(process.env.SUPERCODE_TEAMS_TEST_BIN ?? process.env.SUPERCODE_BIN ?? 'supercode', ['message', 'waiting', '--json'], { encoding: 'utf8', timeout: 15000, stdio: ['ignore', 'pipe', 'ignore'] })); } catch { /* an older supercode has no such door */ }
1014
- const value = { version: 1, context: scope(context), data: { mail_waiting: mailWaiting, reachable, account, connector: saved ? { enrolled: true, running: running || busy, ...(busy ? { busy: true } : {}), machine: saved.machine, source: saved.source, sync } : { enrolled: false, running: false }, sources: reachable && !context.machine_only ? (await team.sources.list()).items : [] } };
1013
+ // asked of the daemon, which keeps these records (mail step 1); a daemon that does not answer is said, never read as none
1014
+ let mailWaitingError = null;
1015
+ try {
1016
+ const channel = await openLocalMachine(paths(teamsHome()).socket);
1017
+ try { const rpc = await nodeRpc(channel); mailWaiting = (await rpc.call('harness.v1.mail.waiting', {}, { timeoutMs: 15000 }))?.rows ?? []; }
1018
+ finally { channel.close?.(); }
1019
+ } catch (error) { mailWaitingError = error.message; }
1020
+ const value = { version: 1, context: scope(context), data: { mail_waiting: mailWaiting, ...(mailWaitingError ? { mail_waiting_error: mailWaitingError } : {}), reachable, account, connector: saved ? { enrolled: true, running: running || busy, ...(busy ? { busy: true } : {}), machine: saved.machine, source: saved.source, sync } : { enrolled: false, running: false }, sources: reachable && !context.machine_only ? (await team.sources.list()).items : [] } };
1015
1021
  if (flags.json) return output(value);
1016
1022
  const lines = [`Team ${context.team_name ?? context.team_id} · ${new URL(context.origin).host}${reachable ? '' : ' · unreachable'}`];
1017
1023
  lines.push(saved?.machine ? `Connector ${saved.machine.name ?? saved.machine.id} · ${running ? 'running' : busy ? 'running (busy: its control socket did not answer within 1 s)' : 'not running'}${saved.service?.kind ? ` (${saved.service.kind})` : ''}` : 'Connector not connected (supercode teams connect)');
1018
1024
  lines.push(...syncStatusLines(sync));
1019
- lines.push(...mailWaitingLines(mailWaiting));
1025
+ lines.push(...(mailWaitingError ? [`Mail waiting mail unknown: this machine's daemon did not answer (${mailWaitingError})`] : mailWaitingLines(mailWaiting)));
1020
1026
  process.stdout.write(`${lines.join('\n')}\n`);
1021
1027
  return;
1022
1028
  }
package/machine.mjs CHANGED
@@ -42,10 +42,12 @@ import { Agents } from './mail/agents.mjs';
42
42
  import { Relays } from './mail/relay.mjs';
43
43
  import { serveRelayEndpoint } from './mail/relay-endpoint.mjs';
44
44
  import { MailCarrier } from './mail/carrier.mjs';
45
+
46
+ const NO_CALLER = "Can't tell which session is running this command, so replies would have nowhere to go. Nothing was sent. Run it from your agent session's own shell tool.";
45
47
  import { MailSend } from './mail/send.mjs';
46
48
  import { AgentVerbs } from './mail/verbs.mjs';
47
49
  import { deliverToRuntime } from './mail/runtime.mjs';
48
- import { envelope as envelopeOf, linkOf, parseAddress, senderIdentity } from './mail/compose.mjs';
50
+ import { envelope as envelopeOf, linkOf, makeAddress, parseAddress, senderIdentity } from './mail/compose.mjs';
49
51
  import { codexQuestions } from './native-questions.mjs';
50
52
  import { Duplex, PassThrough } from 'node:stream';
51
53
 
@@ -124,6 +126,13 @@ export function claudeUsageLimitWait(screen) {
124
126
  /** The settle floor for a pane whose kind has no composer pattern: the screen
125
127
  * must have stopped changing for this long before the pane counts as ready. */
126
128
  const INITIAL_INPUT_SETTLE_MS = 1_000;
129
+ /** A Windows Codex's typed first input waits, before pasting, for an empty composer on a screen unchanged this long
130
+ * (CODEX_PASTE_SETTLE_DEADLINE_MS at most); gives the paste CODEX_PASTE_SHOWN_MS to show, and watches
131
+ * CODEX_PASTE_LATE_MS more to clear one shown after that. */
132
+ const CODEX_PASTE_SETTLE_MS = 1_500;
133
+ const CODEX_PASTE_SETTLE_DEADLINE_MS = 30_000;
134
+ const CODEX_PASTE_SHOWN_MS = 5_000;
135
+ const CODEX_PASTE_LATE_MS = 15_000;
127
136
 
128
137
  /**
129
138
  * Claude asks whether to trust a folder it has not opened before, whatever its permission mode, and has no flag that
@@ -1927,6 +1936,30 @@ export class TeamsMachine extends MachineCore {
1927
1936
  * a claim is the same user's word, held to what the map holds. A request the Teams connector forwards from another
1928
1937
  * machine keeps the caller it carries. A local claim the map does not hold is refused.
1929
1938
  */
1939
+ /** The caller's unread mail claimed under its reader's process (a reader that ends gives it back), with the token its
1940
+ * reader acknowledges once it has shown it: `message inbox`, `read_messages` and `message wait`. */
1941
+ async #claimInbox(caller, { all, last, pid, via }) {
1942
+ const read = await this.mail.inbox(caller, { all, last, pid, via });
1943
+ let token = null;
1944
+ if (read.claimed.length) {
1945
+ token = randomBytes(12).toString('hex');
1946
+ this.inboxClaims ??= new Map();
1947
+ // a reader that never acknowledges leaves its claims under its own pid (given back when it ends): its token
1948
+ // is let go after a while, never held for good
1949
+ for (const [old] of [...this.inboxClaims].slice(0, Math.max(0, this.inboxClaims.size - 255))) this.inboxClaims.delete(old);
1950
+ this.inboxClaims.set(token, { address: caller, claimed: read.claimed });
1951
+ }
1952
+ // `plain`: each message as its reader is shown it (the envelope rendered), with no listing's line about when
1953
+ const plain = read.stored.map((item) => render(item.envelope));
1954
+ const claimedIds = new Set(read.claimed.map((item) => item.envelope.id));
1955
+ const kept = inboxKept(read.stored.length,
1956
+ (index) => Buffer.byteLength(read.rendered[index] ?? '') + Buffer.byteLength(plain[index] ?? '') + Buffer.byteLength(JSON.stringify(read.rows[index] ?? null)),
1957
+ (index) => claimedIds.has(read.stored[index].envelope.id));
1958
+ const left = read.stored.length - kept.length;
1959
+ const note = left ? [inboxNote(left)] : [];
1960
+ return { code: 0, address: caller, rendered: [...note, ...kept.map((index) => read.rendered[index])], plain: [...note, ...kept.map((index) => plain[index])], rows: kept.map((index) => read.rows[index]), omitted: left, token };
1961
+ }
1962
+
1930
1963
  #callerOf(entry, params) {
1931
1964
  const named = typeof params.caller === 'string' && params.caller ? params.caller : null;
1932
1965
  if (entry.principal !== 'local') return Promise.resolve(named);
@@ -2341,18 +2374,37 @@ export class TeamsMachine extends MachineCore {
2341
2374
  const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2342
2375
  // Styled, so the composer's dim placeholder ("Ask Codex to do anything") reads as empty.
2343
2376
  const composer = async () => terminal.composerText?.('codex', await this.host.capture(session.id, { preserveEscapes: true })) ?? null;
2344
- if (await composer() !== '') return { delivered: false, reason: 'composer-not-empty' };
2377
+ // A composer that reads ready can still be mid-layout: a fresh Codex redraws its screen for about a second more
2378
+ // (measured on volter-desktop: ready from the first frame, redrawn at ~1.1 s and ~1.3 s), and a paste sent into
2379
+ // that is shown late or not at all. Paste only into an empty composer on a screen that has stopped changing.
2380
+ let settled = false;
2381
+ for (const until = Date.now() + CODEX_PASTE_SETTLE_DEADLINE_MS; !settled && !signal.aborted && Date.now() < until;) {
2382
+ const capture = await this.host.capture(session.id, { preserveEscapes: true });
2383
+ settled = terminal.composerText?.('codex', capture) === '' &&
2384
+ this.#screenUnchangedMs(session.id, capture, Date.now()) >= CODEX_PASTE_SETTLE_MS;
2385
+ if (!settled) await pause(150);
2386
+ }
2387
+ if (signal.aborted) return { delivered: false, reason: 'aborted' };
2388
+ if (!settled) return { delivered: false, reason: await composer() === '' ? 'screen-unsettled' : 'composer-not-empty' };
2345
2389
  for (const paste of codexWindowsPastes(body)) {
2346
2390
  if (signal.aborted) return { delivered: false, reason: 'aborted' };
2347
2391
  await this.host.sendInput(session.id, paste, { submitMode: 'none' });
2348
2392
  await pause(300);
2349
2393
  }
2350
2394
  let held = false;
2351
- for (let attempt = 0; attempt < 20 && !held && !signal.aborted; attempt += 1) {
2395
+ for (const until = Date.now() + CODEX_PASTE_SHOWN_MS; !held && !signal.aborted && Date.now() < until;) {
2352
2396
  held = Boolean(await composer());
2353
2397
  if (!held) await pause(150);
2354
2398
  }
2355
- if (!held) return { delivered: false, reason: 'paste-unconfirmed' };
2399
+ if (!held) {
2400
+ // A paste shown after this gave up would sit in the composer unsent: watch a while longer and clear it, so the
2401
+ // pane is left as it was found (the launch records the input as undelivered either way).
2402
+ for (const until = Date.now() + CODEX_PASTE_LATE_MS; !signal.aborted && Date.now() < until;) {
2403
+ if (await composer()) { await this.#clearCodexComposer(session, composer, pause); break; }
2404
+ await pause(250);
2405
+ }
2406
+ return { delivered: false, reason: 'paste-unconfirmed' };
2407
+ }
2356
2408
  await this.host.sendInput(session.id, '\r', { submitMode: 'single' });
2357
2409
  return { delivered: true };
2358
2410
  } catch (error) {
@@ -2362,6 +2414,17 @@ export class TeamsMachine extends MachineCore {
2362
2414
  }
2363
2415
  }
2364
2416
 
2417
+ /** Empty a Windows Codex composer this launch pasted into: line clear, then deletes until it reads empty (a paste
2418
+ * placeholder goes with one delete). Never Enter, Escape or Ctrl-C. */
2419
+ async #clearCodexComposer(session, composer, pause) {
2420
+ await this.host.sendInput(session.id, '\u0015', { submitMode: 'none' });
2421
+ await pause(300);
2422
+ for (let round = 0; round < 40 && await composer(); round += 1) {
2423
+ await this.host.sendInput(session.id, '\u007f'.repeat(200), { submitMode: 'none' });
2424
+ await pause(200);
2425
+ }
2426
+ }
2427
+
2365
2428
  async #deliverPaneText(session, contextKey, text, options = {}) {
2366
2429
  const kind = this.#paneKind(contextKey, session);
2367
2430
  if (kind === 'codex' && codexUpdatePrompt(await this.host.capture(session.id, { lines: 120 }))) return { delivered: false, reason: 'codex-update-prompt: delivery refused; restart Codex with check_for_update_on_startup=false' };
@@ -3673,28 +3736,65 @@ export class TeamsMachine extends MachineCore {
3673
3736
  // the caller's own inbox: its unread mail claimed under its own process (a reader that ends gives it back), then
3674
3737
  // acknowledged once it has printed it (harness.v1.mail.acknowledge)
3675
3738
  const caller = await this.#callerOf(entry, params);
3676
- if (typeof caller !== 'string' || /^sc:[^:]+:board:/.test(caller)) return { code: 2, text: "Can't tell which session is running this command, so replies would have nowhere to go. Nothing was sent. Run it from your agent session's own shell tool." };
3739
+ if (typeof caller !== 'string' || /^sc:[^:]+:board:/.test(caller)) return { code: 2, text: NO_CALLER };
3677
3740
  // `via`: the door the claims record (`inbox`, or `read_messages` for `message mcp`'s tool)
3678
- const via = params.via === 'read_messages' ? 'read_messages' : 'inbox';
3679
- const read = await this.mail.inbox(caller, { all: params.all === true, last, pid: Number(params.claim?.pid) || process.pid, via });
3680
- let token = null;
3681
- if (read.claimed.length) {
3682
- token = randomBytes(12).toString('hex');
3683
- this.inboxClaims ??= new Map();
3684
- // a reader that never acknowledges leaves its claims under its own pid (given back when it ends): its token
3685
- // is let go after a while, never held for good
3686
- for (const [old] of [...this.inboxClaims].slice(0, Math.max(0, this.inboxClaims.size - 255))) this.inboxClaims.delete(old);
3687
- this.inboxClaims.set(token, { address: caller, claimed: read.claimed });
3741
+ return this.#claimInbox(caller, { all: params.all === true, last, pid: Number(params.claim?.pid) || process.pid, via: params.via === 'read_messages' ? 'read_messages' : 'inbox' });
3742
+ }
3743
+ // `message wait` (step 1): the caller's next unread mail (from one sender, with `from`), waited for on the store's
3744
+ // own change events, at most 100 s, then claimed as its inbox claims it; nothing polls
3745
+ case 'harness.v1.mail.wait': {
3746
+ if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('mail is read here by this machine\'s own processes'), { code: 403 });
3747
+ const caller = await this.#callerOf(entry, params);
3748
+ if (typeof caller !== 'string' || /^sc:[^:]+:board:/.test(caller)) return { code: 2, text: NO_CALLER };
3749
+ let wanted = null;
3750
+ if (typeof params.from === 'string' && params.from) { const found = this.mail.resolve(params.from); wanted = found.error ? params.from : found.address; }
3751
+ const seconds = Math.min(100, Math.max(0, Number.isFinite(Number(params.timeout)) ? Math.floor(Number(params.timeout)) : 100));
3752
+ // exactly what its claim would take (store.claimUnread): never a live reader's claimed copy or a user's own turn
3753
+ // waiting for its pane, so a wait never wakes for mail it then cannot claim; an abandoned claim is given back first
3754
+ const waiting = () => this.mailStore.hasClaimable(caller, wanted ? (envelope) => envelope.from === wanted || String(envelope.from_name ?? '').startsWith(wanted) : null);
3755
+ if (!waiting()) {
3756
+ const arrived = await new Promise((resolve) => {
3757
+ const changed = (address) => { if (address === caller && waiting()) done(true); };
3758
+ const done = (value) => { clearTimeout(timer); this.mailStore.off('changed', changed); resolve(value); };
3759
+ const timer = setTimeout(() => done(false), seconds * 1000);
3760
+ this.mailStore.on('changed', changed);
3761
+ });
3762
+ if (!arrived) return { code: 6, timeout: true, text: `No mail within ${seconds} seconds; nothing is lost.` };
3763
+ }
3764
+ // everything unread is handed over, not only the awaited sender's: a message not shown now would stay silent
3765
+ return this.#claimInbox(caller, { all: false, last: null, pid: Number(params.claim?.pid) || process.pid, via: 'wait' });
3766
+ }
3767
+ // harness serve's `sessions.inbox` (step 1), asked here by serve: an operator's own mailbox (`from_name`) has its
3768
+ // unread mail claimed and acknowledged, so a second read does not return it; any other address (`address`) is only
3769
+ // looked at, never consumed
3770
+ case 'harness.v1.mail.operator_inbox': {
3771
+ if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('sessions.inbox is answered here for this machine\'s own harness serve'), { code: 403 });
3772
+ if (params.address != null && params.from_name != null) throw Object.assign(new Error('sessions.inbox takes from_name or address, not both'), { code: 400 });
3773
+ let address;
3774
+ if (typeof params.address === 'string') {
3775
+ if (!parseAddress(params.address)) throw Object.assign(new Error(`not an address: ${params.address}`), { code: 400 });
3776
+ address = params.address;
3777
+ } else {
3778
+ const name = Array.from(String(params.from_name ?? 'supercode').trim(), (c) => /\s|@/.test(c) ? '-' : c).join('');
3779
+ if (!name) throw Object.assign(new Error('from_name must not be empty'), { code: 400 });
3780
+ address = makeAddress(machineName(this.env), 'operator', name);
3781
+ if (!address) throw Object.assign(new Error(`from_name is not a mailbox name: ${name}`), { code: 400 });
3782
+ }
3783
+ const all = params.all === true;
3784
+ const shown = (item, state) => { const envelope = this.mailStore.body(item) ?? item.envelope; return { state, envelope, rendered: render(envelope) }; };
3785
+ if (typeof params.address === 'string') {
3786
+ const { stored } = this.mailStore.inventory(address);
3787
+ return { address, messages: stored.filter((item) => all || item.state === 'unread').map((item) => shown(item, item.state)), looked: true };
3688
3788
  }
3689
- // `plain`: each message as its reader is shown it (the envelope rendered), with no listing's line about when
3690
- const plain = read.stored.map((item) => render(item.envelope));
3691
- const claimedIds = new Set(read.claimed.map((item) => item.envelope.id));
3692
- const kept = inboxKept(read.stored.length,
3693
- (index) => Buffer.byteLength(read.rendered[index] ?? '') + Buffer.byteLength(plain[index] ?? '') + Buffer.byteLength(JSON.stringify(read.rows[index] ?? null)),
3694
- (index) => claimedIds.has(read.stored[index].envelope.id));
3695
- const left = read.stored.length - kept.length;
3696
- const note = left ? [inboxNote(left)] : [];
3697
- return { code: 0, address: caller, rendered: [...note, ...kept.map((index) => read.rendered[index])], plain: [...note, ...kept.map((index) => plain[index])], rows: kept.map((index) => read.rows[index]), omitted: left, token };
3789
+ const read = await this.mail.inbox(address, { all, last: null, pid: process.pid, via: 'sessions.inbox' });
3790
+ const messages = [...(all ? read.stored.filter((item) => item.state === 'read').map((item) => shown(item, 'read')) : []), ...read.claimed.map((item) => shown(item, 'unread'))];
3791
+ this.mail.acknowledge(address, read.claimed);
3792
+ return { address, messages };
3793
+ }
3794
+ // `message waiting` and `message expired` (step 1): the carrier's own records
3795
+ case 'harness.v1.mail.waiting': case 'harness.v1.mail.expired': {
3796
+ if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('mail is read here by this machine\'s own processes'), { code: 403 });
3797
+ return { code: 0, rows: method === 'harness.v1.mail.waiting' ? this.carrier.waiting() : this.carrier.expired() };
3698
3798
  }
3699
3799
  // Who a local client is, from its claim (live-sessions.mjs callerOf): what a command acting on another machine sends
3700
3800
  // there as its caller, resolved here, where the map that holds it is. Local only; never the app door's.
package/mail/carrier.mjs CHANGED
@@ -262,6 +262,49 @@ export class MailCarrier {
262
262
  catch (error) { this.log(`carrier: ${machine}'s wait was not kept: ${error.message}`); }
263
263
  }
264
264
 
265
+ // ---- what waits, and what expired (`message waiting`, `message expired`) -----------------------------------------
266
+
267
+ /** Every message here still waiting to be delivered after a failed attempt, or for a machine that is offline, oldest
268
+ * filed first (mailbox.rs `waiting_mail`, field for field). */
269
+ waiting() {
270
+ const all = [];
271
+ let folders = [];
272
+ try { folders = readdirSync(this.mailRoot); } catch { return all; }
273
+ for (const folder of folders) {
274
+ let ids = [];
275
+ try { ids = readdirSync(join(this.mailRoot, folder, 'wake')); } catch { continue; }
276
+ if (!ids.length) continue;
277
+ const address = this.#address(folder);
278
+ if (!address) continue;
279
+ const machine = address.split(':')[1];
280
+ const wait = machine !== this.machine ? readJson(this.#machineWaitPath(machine)) : null;
281
+ for (const id of ids) {
282
+ const state = this.store.wakeState(address, id);
283
+ if (!wait && !(state.attempts > 0) && state.last_error == null) continue;
284
+ const found = this.store.findIn(address, id);
285
+ const envelope = found ? readJson(found.path) : null;
286
+ if (!envelope) continue;
287
+ all.push({ id, to: address, from: envelope.from ?? '', subject: envelope.subject ?? null, filed_at_ms: Number(envelope.created_at_ms) || 0,
288
+ waiting_for: wait ? wait.machine : null, since_ms: wait?.offline_since_ms ?? state.failing_since_ms ?? (Number(envelope.created_at_ms) || 0),
289
+ attempts: state.attempts ?? 0, last_error: wait?.last_error ?? state.last_error ?? null });
290
+ }
291
+ }
292
+ return all.sort((a, b) => a.filed_at_ms - b.filed_at_ms);
293
+ }
294
+
295
+ /** Every wake that expired here, oldest first (mailbox.rs `expired_wakes`). */
296
+ expired() {
297
+ const all = [];
298
+ let folders = [];
299
+ try { folders = readdirSync(this.mailRoot); } catch { return all; }
300
+ for (const folder of folders) {
301
+ let names = [];
302
+ try { names = readdirSync(join(this.mailRoot, folder, 'expired')); } catch { continue; }
303
+ for (const name of names) { const expiry = readJson(join(this.mailRoot, folder, 'expired', name)); if (expiry?.id) all.push(expiry); }
304
+ }
305
+ return all.sort((a, b) => (a.expired_at_ms ?? 0) - (b.expired_at_ms ?? 0));
306
+ }
307
+
265
308
  // ---- how an attempt went ---------------------------------------------------------------------------------------
266
309
 
267
310
  /** Record how a wake went: done, waiting for its next attempt, or expired at its bound (mail_watch.rs `settle`). */
package/mail/store.mjs CHANGED
@@ -191,6 +191,22 @@ export class MailStore extends EventEmitter {
191
191
  /** The unread mail of `address` (not the user's own turns) moved into `claimed/` under its reader's pid (`pid`, the
192
192
  * reading process: a claim whose reader ends goes back to `new/`), each claim recorded; another reader that took one
193
193
  * first keeps it (mailbox.rs `claim_unread`). Oldest first. */
194
+ /** Whether `claimUnread` would take anything now, taking nothing: abandoned claims given back first, as it does, then
195
+ * any envelope in new/ that is not a user's own turn (`message wait` wakes only for mail it can claim). From the
196
+ * mailbox's own folders, as claimUnread reads them. */
197
+ hasClaimable(address, wanted = null) {
198
+ const box = this.boxOf(address) ?? this.#named(address);
199
+ if (!box) return false;
200
+ this.#recoverAbandoned(box);
201
+ let names = [];
202
+ try { names = readdirSync(join(box.dir, 'new')); } catch { return false; }
203
+ return names.some((file) => {
204
+ if (!file.endsWith('.json')) return false;
205
+ const envelope = readEnvelope(join(box.dir, 'new', file));
206
+ return envelope && typeof envelope.id === 'string' && envelope.kind !== 'user' && (!wanted || wanted(envelope));
207
+ });
208
+ }
209
+
194
210
  claimUnread(address, via, caller, pid = process.pid) {
195
211
  // the mailbox's own folders, read here (one named read): what decides a delivery never comes from the watch-fed
196
212
  // index alone, whose events a platform can drop (an FSEvents drop, an inotify overflow)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.3.113",
3
+ "version": "0.3.115",
4
4
  "type": "module",
5
5
  "description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
6
6
  "license": "MIT",