@phnx-labs/agents-cli 1.22.40 → 1.22.41
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/CHANGELOG.md +22 -0
- package/README.md +5 -0
- package/dist/bin/agents +0 -0
- package/dist/bootstrap.js +0 -1
- package/dist/cli/command-registry.js +1 -2
- package/dist/commands/browser.d.ts +15 -0
- package/dist/commands/browser.js +115 -40
- package/dist/commands/feed.js +3 -11
- package/dist/commands/secrets.js +87 -97
- package/dist/commands/sessions-picker.d.ts +1 -0
- package/dist/commands/sessions-picker.js +27 -5
- package/dist/commands/sessions-share.d.ts +25 -0
- package/dist/commands/sessions-share.js +166 -0
- package/dist/commands/sessions.js +2 -0
- package/dist/commands/setup-browser.js +1 -1
- package/dist/commands/setup-preferences.js +2 -2
- package/dist/commands/webhook.js +14 -9
- package/dist/lib/browser/cdp.js +4 -0
- package/dist/lib/browser/chrome.js +12 -1
- package/dist/lib/browser/drivers/ssh.js +1 -0
- package/dist/lib/browser/profiles.d.ts +3 -3
- package/dist/lib/browser/profiles.js +7 -7
- package/dist/lib/browser/service.d.ts +24 -1
- package/dist/lib/browser/service.js +38 -14
- package/dist/lib/browser/types.d.ts +1 -1
- package/dist/lib/daemon-webhooks.d.ts +3 -1
- package/dist/lib/daemon-webhooks.js +12 -7
- package/dist/lib/device-config.js +1 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/observe-aliases.d.ts +2 -2
- package/dist/lib/observe-aliases.js +2 -11
- package/dist/lib/project-key.js +7 -0
- package/dist/lib/runner.js +21 -6
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.d.ts +1 -1
- package/dist/lib/secrets/bundles.js +1 -1
- package/dist/lib/secrets/headless.d.ts +16 -0
- package/dist/lib/secrets/headless.js +21 -0
- package/dist/lib/secrets/remote.d.ts +9 -7
- package/dist/lib/secrets/remote.js +18 -9
- package/dist/lib/session/share-html.d.ts +55 -0
- package/dist/lib/session/share-html.js +319 -0
- package/dist/lib/settings-manifest.d.ts +2 -0
- package/dist/lib/settings-manifest.js +81 -3
- package/dist/lib/share/publish.d.ts +38 -0
- package/dist/lib/share/publish.js +81 -8
- package/dist/lib/startup/command-registry.d.ts +2 -1
- package/dist/lib/startup/command-registry.js +4 -2
- package/dist/lib/triggers/handlers.d.ts +37 -2
- package/dist/lib/triggers/handlers.js +56 -5
- package/dist/lib/triggers/webhook.d.ts +80 -6
- package/dist/lib/triggers/webhook.js +127 -3
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/wrap.d.ts +33 -0
- package/dist/lib/wrap.js +70 -0
- package/package.json +1 -1
|
@@ -264,6 +264,76 @@ export function verifyLinearTimestamp(payload, now = Date.now(), toleranceMs = 6
|
|
|
264
264
|
const ts = payload.webhookTimestamp;
|
|
265
265
|
return typeof ts === 'number' && Math.abs(now - ts) <= toleranceMs;
|
|
266
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Verify a Slack request signature (the `v0` scheme). Slack signs the base
|
|
269
|
+
* string `v0:${timestamp}:${rawBody}` with the app's signing secret and sends
|
|
270
|
+
* the hex digest as `X-Slack-Signature: v0=<hex>`, alongside the unix
|
|
271
|
+
* `X-Slack-Request-Timestamp`. The timestamp is BOTH part of the signed base
|
|
272
|
+
* string AND checked for freshness here — a request older than `toleranceSec`
|
|
273
|
+
* (default 5 min) is rejected, so a captured, correctly-signed body cannot be
|
|
274
|
+
* replayed later. Fails closed on any missing/malformed header.
|
|
275
|
+
*/
|
|
276
|
+
export function verifySlackSignature(headers, rawBody, secret, now = Date.now(), toleranceSec = 300) {
|
|
277
|
+
const ts = header(headers, 'x-slack-request-timestamp');
|
|
278
|
+
if (!ts || !/^\d+$/.test(ts))
|
|
279
|
+
return false;
|
|
280
|
+
if (Math.abs(Math.floor(now / 1000) - Number(ts)) > toleranceSec)
|
|
281
|
+
return false;
|
|
282
|
+
const received = header(headers, 'x-slack-signature');
|
|
283
|
+
const signature = received?.startsWith('v0=') ? received.slice('v0='.length) : undefined;
|
|
284
|
+
// Concatenate the base-string prefix with the raw body bytes so a non-ASCII
|
|
285
|
+
// payload hashes identically to Slack's own `v0:${ts}:${body}` string.
|
|
286
|
+
const base = Buffer.concat([Buffer.from(`v0:${ts}:`, 'utf-8'), rawBody]);
|
|
287
|
+
const expected = crypto.createHmac('sha256', secret).update(base).digest('hex');
|
|
288
|
+
return timingSafeHexEqual(signature, expected);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Parse a Slack delivery body into a normalized {@link SlackPayload}. Slack
|
|
292
|
+
* sends slash commands as `application/x-www-form-urlencoded` and Events API
|
|
293
|
+
* deliveries (including the `url_verification` handshake) as
|
|
294
|
+
* `application/json` — this is the one place that content-type branch lives.
|
|
295
|
+
*/
|
|
296
|
+
export function parseSlackBody(contentType, rawBody) {
|
|
297
|
+
if ((contentType ?? '').includes('application/x-www-form-urlencoded')) {
|
|
298
|
+
const form = new URLSearchParams(rawBody.toString('utf-8'));
|
|
299
|
+
// A slash command carries no thread; its reply posts to the channel.
|
|
300
|
+
return {
|
|
301
|
+
type: 'slash_command',
|
|
302
|
+
command: form.get('command') ?? undefined,
|
|
303
|
+
text: form.get('text') ?? undefined,
|
|
304
|
+
channel: form.get('channel_id') ?? undefined,
|
|
305
|
+
user: form.get('user_id') ?? undefined,
|
|
306
|
+
response_url: form.get('response_url') ?? undefined,
|
|
307
|
+
team: form.get('team_id') ?? undefined,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const json = rawBody.length > 0 ? JSON.parse(rawBody.toString('utf-8')) : {};
|
|
311
|
+
const type = String(json.type ?? '');
|
|
312
|
+
if (type === 'url_verification') {
|
|
313
|
+
return { type, challenge: typeof json.challenge === 'string' ? json.challenge : '' };
|
|
314
|
+
}
|
|
315
|
+
const event = (json.event ?? {});
|
|
316
|
+
return {
|
|
317
|
+
type: type || 'event_callback',
|
|
318
|
+
event_id: typeof json.event_id === 'string' ? json.event_id : undefined,
|
|
319
|
+
event_type: typeof event.type === 'string' ? event.type : undefined,
|
|
320
|
+
text: typeof event.text === 'string' ? event.text : undefined,
|
|
321
|
+
channel: typeof event.channel === 'string' ? event.channel : undefined,
|
|
322
|
+
thread_ts: typeof event.thread_ts === 'string'
|
|
323
|
+
? event.thread_ts
|
|
324
|
+
: typeof event.ts === 'string'
|
|
325
|
+
? event.ts
|
|
326
|
+
: undefined,
|
|
327
|
+
user: typeof event.user === 'string' ? event.user : undefined,
|
|
328
|
+
team: typeof json.team_id === 'string' ? json.team_id : undefined,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/** The `event` name a Slack delivery fires under: the slash command, or the event subtype. */
|
|
332
|
+
export function slackEventName(payload) {
|
|
333
|
+
if (payload.type === 'slash_command')
|
|
334
|
+
return payload.command ?? 'slash_command';
|
|
335
|
+
return payload.event_type ?? payload.type;
|
|
336
|
+
}
|
|
267
337
|
export function createMemoryDeliveryStore(maxEntries = 1000) {
|
|
268
338
|
const seen = new Map();
|
|
269
339
|
const touch = (id) => {
|
|
@@ -405,7 +475,7 @@ function deliveryId(source, headers, rawBody) {
|
|
|
405
475
|
return `${source}:${named ?? crypto.createHash('sha256').update(rawBody).digest('hex')}`;
|
|
406
476
|
}
|
|
407
477
|
function sourceFromPath(pathname) {
|
|
408
|
-
const match = /^\/hooks\/(github|linear)\/?$/.exec(pathname ?? '');
|
|
478
|
+
const match = /^\/hooks\/(github|linear|slack)\/?$/.exec(pathname ?? '');
|
|
409
479
|
return match ? match[1] : null;
|
|
410
480
|
}
|
|
411
481
|
async function readRawBody(req, maxBytes) {
|
|
@@ -461,6 +531,11 @@ export function waitForListening(server) {
|
|
|
461
531
|
* duplicate from an in-flight set, since `deliveryStore.seen` only reports
|
|
462
532
|
* completed deliveries and would otherwise let a mid-flight retry double-fire.
|
|
463
533
|
*
|
|
534
|
+
* `onMatch` fires as soon as matching is known, before any dispatch; `onDelivery`
|
|
535
|
+
* fires only once every matched routine/handler has settled — see their docs on
|
|
536
|
+
* `WebhookServerOptions`. A caller that wants a prompt "this fired" log uses
|
|
537
|
+
* `onMatch`, not `onDelivery` (RUSH-2722).
|
|
538
|
+
*
|
|
464
539
|
* Returns the underlying server so callers can `close()` it.
|
|
465
540
|
*/
|
|
466
541
|
export function startWebhookServer(options) {
|
|
@@ -482,8 +557,17 @@ export function startWebhookServer(options) {
|
|
|
482
557
|
try {
|
|
483
558
|
const context = buildWebhookContext(webhook);
|
|
484
559
|
const fireOptions = options.fire ?? {};
|
|
560
|
+
// Match FIRST, before any dispatch — matching is a pure in-memory lookup,
|
|
561
|
+
// so this is the earliest point the receiver knows what fired, and the
|
|
562
|
+
// right time to log it (RUSH-2722). Dispatching a `run.command` handler
|
|
563
|
+
// can then block on the shelled-out process for minutes; that must never
|
|
564
|
+
// hold the "fired" log back with it.
|
|
565
|
+
const matchedJobs = matchJobsToWebhook(fireOptions.jobs ?? listJobs(), webhook);
|
|
566
|
+
const matchedHandlers = listHandlers().filter((handler) => handlerMatchesWebhook(handler, webhook));
|
|
567
|
+
options.onMatch?.(webhook, matchedJobs.map((job) => job.name), matchedHandlers.map((handler) => handler.name));
|
|
485
568
|
const firedJobs = await fireWebhookJobs(webhook, {
|
|
486
569
|
...fireOptions,
|
|
570
|
+
jobs: matchedJobs,
|
|
487
571
|
context,
|
|
488
572
|
skipJobNames: deliveryStore.completedJobs(id),
|
|
489
573
|
onJobFired: (job, firedJob) => {
|
|
@@ -492,7 +576,6 @@ export function startWebhookServer(options) {
|
|
|
492
576
|
fireOptions.onJobFired?.(job, firedJob);
|
|
493
577
|
},
|
|
494
578
|
});
|
|
495
|
-
const matchedHandlers = listHandlers().filter((handler) => handlerMatchesWebhook(handler, webhook));
|
|
496
579
|
const firedHandlers = [];
|
|
497
580
|
const handlerErrors = [];
|
|
498
581
|
await Promise.allSettled(matchedHandlers.map(async (handler) => {
|
|
@@ -563,13 +646,54 @@ export function startWebhookServer(options) {
|
|
|
563
646
|
const rawBody = await readRawBody(req, maxBodyBytes);
|
|
564
647
|
const valid = source === 'github'
|
|
565
648
|
? verifyGithubSignature(req.headers, rawBody, secret)
|
|
566
|
-
:
|
|
649
|
+
: source === 'linear'
|
|
650
|
+
? verifyLinearSignature(req.headers, rawBody, secret)
|
|
651
|
+
: verifySlackSignature(req.headers, rawBody, secret);
|
|
567
652
|
if (!valid) {
|
|
568
653
|
emit('webhook.rejected', { source, reason: 'invalid signature' });
|
|
569
654
|
res.writeHead(401, { 'content-type': 'application/json' });
|
|
570
655
|
res.end(JSON.stringify({ ok: false, error: 'invalid signature' }));
|
|
571
656
|
return;
|
|
572
657
|
}
|
|
658
|
+
// Slack diverges from the GitHub/Linear JSON path: two content-types
|
|
659
|
+
// (slash = form, events = JSON), a one-time url_verification handshake,
|
|
660
|
+
// a body-carried event id, and a 200 ack Slack shows to the caller.
|
|
661
|
+
if (source === 'slack') {
|
|
662
|
+
const slack = parseSlackBody(header(req.headers, 'content-type'), rawBody);
|
|
663
|
+
// One-time Events API URL handshake: echo the challenge, dispatch nothing.
|
|
664
|
+
if (slack.type === 'url_verification') {
|
|
665
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
666
|
+
res.end(JSON.stringify({ challenge: slack.challenge ?? '' }));
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const slackEvent = slackEventName(slack);
|
|
670
|
+
// Slash commands carry no event_id, so fall back to a body hash for dedup.
|
|
671
|
+
const slackId = `slack:${slack.event_id ?? crypto.createHash('sha256').update(rawBody).digest('hex')}`;
|
|
672
|
+
emit('webhook.received', { source, event: slackEvent, deliveryId: slackId });
|
|
673
|
+
if (deliveryStore.seen(slackId) || inFlight.has(slackId)) {
|
|
674
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
675
|
+
res.end(JSON.stringify({ ok: true, duplicate: true }));
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (!rateLimiter.take(source)) {
|
|
679
|
+
emit('webhook.rejected', { source, event: slackEvent, deliveryId: slackId, reason: 'rate limit exceeded' });
|
|
680
|
+
res.writeHead(429, { 'content-type': 'application/json' });
|
|
681
|
+
res.end(JSON.stringify({ ok: false, error: 'rate limit exceeded' }));
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const slackWebhook = { source, event: slackEvent, payload: slack };
|
|
685
|
+
emit('webhook.authorized', { source, event: slackEvent, deliveryId: slackId });
|
|
686
|
+
// ACK FIRST (RUSH-2548): a 200 inside Slack's 3s window. A slash
|
|
687
|
+
// command renders the ack text to the caller; an event delivery
|
|
688
|
+
// ignores the body. Dispatch outlives the ack, same as GitHub/Linear.
|
|
689
|
+
inFlight.add(slackId);
|
|
690
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
691
|
+
res.end(slack.type === 'slash_command'
|
|
692
|
+
? JSON.stringify({ response_type: 'ephemeral', text: 'On it — replying in this thread.' })
|
|
693
|
+
: '');
|
|
694
|
+
void settleDelivery(slackWebhook, slackId).finally(() => inFlight.delete(slackId));
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
573
697
|
const id = deliveryId(source, req.headers, rawBody);
|
|
574
698
|
const event = source === 'github' ? (header(req.headers, 'x-github-event') ?? '') : '';
|
|
575
699
|
emit('webhook.received', { source, event, deliveryId: id });
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -1110,7 +1110,7 @@ export interface HostEntry {
|
|
|
1110
1110
|
/** Browser profile definition stored in agents.yaml. */
|
|
1111
1111
|
export interface BrowserProfileConfig {
|
|
1112
1112
|
description?: string;
|
|
1113
|
-
browser: 'chrome' | 'comet' | 'chromium' | 'brave' | 'edge' | 'custom';
|
|
1113
|
+
browser: 'chrome' | 'comet' | 'chromium' | 'brave' | 'edge' | 'arc' | 'custom';
|
|
1114
1114
|
binary?: string;
|
|
1115
1115
|
electron?: boolean;
|
|
1116
1116
|
/**
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal word-wrap — one shared helper for the whole CLI.
|
|
3
|
+
*
|
|
4
|
+
* Multi-line word wrapping is the gap the single-line helpers in
|
|
5
|
+
* `session/width.ts` (`truncateToWidth`, `padToWidth`) do not fill. Anywhere that
|
|
6
|
+
* renders variable-length text into a fixed-width terminal wraps through THIS,
|
|
7
|
+
* never a per-field slice.
|
|
8
|
+
*
|
|
9
|
+
* Width is measured with `stringWidth` (from the canonical `session/width.ts`),
|
|
10
|
+
* NOT `String.length`: the text this wraps is routinely ANSI-coloured (chalk /
|
|
11
|
+
* marked-terminal output) and may carry OSC-8 hyperlinks, where `.length`
|
|
12
|
+
* over-counts the invisible escape bytes. Words are split on whitespace, so a
|
|
13
|
+
* wrap boundary always falls between escape sequences, never inside one.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Hard-wrap `text` to `cols` visible columns.
|
|
17
|
+
*
|
|
18
|
+
* - Splits on existing newlines first (each paragraph wraps independently).
|
|
19
|
+
* - Wraps on whitespace; a single word wider than the available width is emitted
|
|
20
|
+
* on its own line rather than split mid-word (and never mid-escape).
|
|
21
|
+
* - Width is the visible display width via `stringWidth`, so ANSI colour and
|
|
22
|
+
* OSC-8 hyperlink escapes do not inflate the count.
|
|
23
|
+
* - `hangingIndent` pads every line AFTER the first with that many spaces, so a
|
|
24
|
+
* labelled value ("Latest <text>") keeps its continuation lines aligned under
|
|
25
|
+
* the value. The wrapping width is `cols - indent`.
|
|
26
|
+
* - Floor: when `hangingIndent` is set, the width is floored at 8 so a
|
|
27
|
+
* pathologically large indent still makes forward progress. With no indent the
|
|
28
|
+
* floor is 1, so a legitimately small `cols` is respected, never overridden. A
|
|
29
|
+
* non-finite `cols` degrades to the floor rather than silently disabling wrap.
|
|
30
|
+
*
|
|
31
|
+
* Returns the wrapped lines (never mutates input). An empty string yields [''].
|
|
32
|
+
*/
|
|
33
|
+
export declare function wrapToWidth(text: string, cols: number, hangingIndent?: number): string[];
|
package/dist/lib/wrap.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal word-wrap — one shared helper for the whole CLI.
|
|
3
|
+
*
|
|
4
|
+
* Multi-line word wrapping is the gap the single-line helpers in
|
|
5
|
+
* `session/width.ts` (`truncateToWidth`, `padToWidth`) do not fill. Anywhere that
|
|
6
|
+
* renders variable-length text into a fixed-width terminal wraps through THIS,
|
|
7
|
+
* never a per-field slice.
|
|
8
|
+
*
|
|
9
|
+
* Width is measured with `stringWidth` (from the canonical `session/width.ts`),
|
|
10
|
+
* NOT `String.length`: the text this wraps is routinely ANSI-coloured (chalk /
|
|
11
|
+
* marked-terminal output) and may carry OSC-8 hyperlinks, where `.length`
|
|
12
|
+
* over-counts the invisible escape bytes. Words are split on whitespace, so a
|
|
13
|
+
* wrap boundary always falls between escape sequences, never inside one.
|
|
14
|
+
*/
|
|
15
|
+
import { stringWidth } from './session/width.js';
|
|
16
|
+
/**
|
|
17
|
+
* Hard-wrap `text` to `cols` visible columns.
|
|
18
|
+
*
|
|
19
|
+
* - Splits on existing newlines first (each paragraph wraps independently).
|
|
20
|
+
* - Wraps on whitespace; a single word wider than the available width is emitted
|
|
21
|
+
* on its own line rather than split mid-word (and never mid-escape).
|
|
22
|
+
* - Width is the visible display width via `stringWidth`, so ANSI colour and
|
|
23
|
+
* OSC-8 hyperlink escapes do not inflate the count.
|
|
24
|
+
* - `hangingIndent` pads every line AFTER the first with that many spaces, so a
|
|
25
|
+
* labelled value ("Latest <text>") keeps its continuation lines aligned under
|
|
26
|
+
* the value. The wrapping width is `cols - indent`.
|
|
27
|
+
* - Floor: when `hangingIndent` is set, the width is floored at 8 so a
|
|
28
|
+
* pathologically large indent still makes forward progress. With no indent the
|
|
29
|
+
* floor is 1, so a legitimately small `cols` is respected, never overridden. A
|
|
30
|
+
* non-finite `cols` degrades to the floor rather than silently disabling wrap.
|
|
31
|
+
*
|
|
32
|
+
* Returns the wrapped lines (never mutates input). An empty string yields [''].
|
|
33
|
+
*/
|
|
34
|
+
export function wrapToWidth(text, cols, hangingIndent = 0) {
|
|
35
|
+
const indent = Math.max(0, Math.trunc(hangingIndent));
|
|
36
|
+
const floor = indent > 0 ? 8 : 1;
|
|
37
|
+
const width = Number.isFinite(cols) ? Math.max(floor, cols - indent) : floor;
|
|
38
|
+
const pad = ' '.repeat(indent);
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const para of String(text).split('\n')) {
|
|
41
|
+
const words = para.split(/\s+/).filter(w => w.length > 0);
|
|
42
|
+
if (words.length === 0) {
|
|
43
|
+
out.push('');
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
let line = '';
|
|
47
|
+
let lineWidth = 0;
|
|
48
|
+
for (const word of words) {
|
|
49
|
+
const wordWidth = stringWidth(word);
|
|
50
|
+
if (line === '') {
|
|
51
|
+
line = word;
|
|
52
|
+
lineWidth = wordWidth;
|
|
53
|
+
}
|
|
54
|
+
else if (lineWidth + 1 + wordWidth > width) {
|
|
55
|
+
out.push(line);
|
|
56
|
+
line = word;
|
|
57
|
+
lineWidth = wordWidth;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
line = `${line} ${word}`;
|
|
61
|
+
lineWidth += 1 + wordWidth;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (line !== '')
|
|
65
|
+
out.push(line);
|
|
66
|
+
}
|
|
67
|
+
if (out.length === 0)
|
|
68
|
+
out.push('');
|
|
69
|
+
return out.map((l, i) => (i === 0 ? l : pad + l));
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.41",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|