@matterailab/orbcode 0.1.8 → 0.1.13

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/ui/App.js CHANGED
@@ -6,12 +6,13 @@ import { COLORS, VERSION } from "../branding.js";
6
6
  import { AXON_MODELS, getModel, isValidAxonModel } from "../api/models.js";
7
7
  import { LoginView } from "./LoginView.js";
8
8
  import { APP_URL, fetchBalance, fetchProfile, fetchTaskTitle } from "../auth/auth.js";
9
- import { getAuthToken, loadSettings, saveSettings } from "../config/settings.js";
9
+ import { getAuthToken, getPendingProjectHooks, loadSettings, saveSettings, trustProjectHooks, } from "../config/settings.js";
10
10
  import { Agent } from "../core/agent.js";
11
11
  import { Spinner } from "./components/Spinner.js";
12
12
  import { InputBox } from "./components/InputBox.js";
13
13
  import { ApprovalPrompt } from "./components/ApprovalPrompt.js";
14
14
  import { FollowupPrompt } from "./components/FollowupPrompt.js";
15
+ import { HookTrustPrompt } from "./components/HookTrustPrompt.js";
15
16
  import { StatusBar } from "./components/StatusBar.js";
16
17
  import { ModelPicker } from "./components/ModelPicker.js";
17
18
  import { SessionPicker } from "./components/SessionPicker.js";
@@ -157,6 +158,9 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
157
158
  const [runningTool, setRunningTool] = useState(null);
158
159
  const [pendingApproval, setPendingApproval] = useState(null);
159
160
  const [pendingFollowup, setPendingFollowup] = useState(null);
161
+ // Set when the current project defines hooks that haven't been trusted yet;
162
+ // gates input until the user decides (project hooks run shell commands).
163
+ const [pendingHookTrust, setPendingHookTrust] = useState(null);
160
164
  // FIFO queue of messages the user typed while the LLM was still streaming.
161
165
  // Drained one-per-turn on each `turn-end` event so multi-step work can
162
166
  // keep flowing without making the user wait for the previous response.
@@ -193,6 +197,10 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
193
197
  // we can drain it inside `handleEvent` without re-creating that callback
194
198
  // on every keystroke).
195
199
  const queueRef = useRef([]);
200
+ // Holds the startup prompt while we wait for a project-hook trust decision.
201
+ const deferredPromptRef = useRef(null);
202
+ // Guards endAndExit against double-invocation (Ctrl+D spam).
203
+ const exitingRef = useRef(false);
196
204
  const enqueueMessage = useCallback((text) => {
197
205
  queueRef.current = [...queueRef.current, text];
198
206
  setQueuedMessages(queueRef.current);
@@ -220,7 +228,7 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
220
228
  void fetchTaskTitle(agent.taskId, token).then((title) => {
221
229
  if (title && agentRef.current?.taskId === agent.taskId) {
222
230
  setSessionTitle(title);
223
- setTerminalTitle(`${title} (orbcode)`);
231
+ setTerminalTitle(title);
224
232
  agent.setTitle(title);
225
233
  }
226
234
  });
@@ -296,6 +304,9 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
296
304
  case "completion":
297
305
  pushRow({ kind: "completion", text: event.result });
298
306
  break;
307
+ case "system":
308
+ pushRow({ kind: event.isError ? "error" : "info", text: event.message });
309
+ break;
299
310
  case "error":
300
311
  pushRow({ kind: "error", text: event.message });
301
312
  break;
@@ -343,6 +354,7 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
343
354
  baseUrl: current.baseUrl,
344
355
  autoApproveEdits: current.autoApproveEdits,
345
356
  autoApproveSafeCommands: current.autoApproveSafeCommands,
357
+ hooks: current.hooks,
346
358
  resume,
347
359
  callbacks: {
348
360
  onEvent: handleEvent,
@@ -363,12 +375,16 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
363
375
  resetTranscript();
364
376
  setTasks(session.todos ?? "");
365
377
  setTotalCost(session.totalCost ?? 0);
378
+ // Repopulate the status bar immediately. Without this the context
379
+ // number only appears once the next streaming `usage` chunk arrives,
380
+ // which can be after several seconds of "ctx 0".
381
+ setContextTokens(session.contextTokens ?? 0);
366
382
  if (session.title) {
367
383
  // The stored title is already the backend one (or the prompt
368
384
  // fallback); don't re-fetch for this task.
369
385
  titleTaskRef.current = session.id;
370
386
  setSessionTitle(session.title);
371
- setTerminalTitle(`${session.title} (orbcode)`);
387
+ setTerminalTitle(session.title);
372
388
  }
373
389
  // Replay the conversation into the transcript.
374
390
  for (const message of session.messages) {
@@ -390,6 +406,33 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
390
406
  agentRef.current?.setModel(modelId);
391
407
  pushRow({ kind: "info", text: `Model switched to ${getModel(modelId).name}` });
392
408
  }, [pushRow]);
409
+ // Fire SessionEnd hooks (best-effort, capped at 3s) before quitting.
410
+ const endAndExit = useCallback((reason) => {
411
+ const agent = agentRef.current;
412
+ if (!agent) {
413
+ exit();
414
+ return;
415
+ }
416
+ // Guard against double-invocation (e.g. the user spamming Ctrl+D):
417
+ // the first call schedules the shutdown; subsequent calls are no-ops.
418
+ if (exitingRef.current)
419
+ return;
420
+ exitingRef.current = true;
421
+ // Clear the cap timer when SessionEnd finishes first — Promise.race
422
+ // leaves the loser pending, and a ref'd setTimeout would otherwise
423
+ // keep the event loop alive (delaying the actual exit by up to 3s).
424
+ let capTimer;
425
+ const cap = new Promise((resolve) => {
426
+ capTimer = setTimeout(resolve, 3000);
427
+ capTimer.unref?.();
428
+ });
429
+ void Promise.race([agent.endSession(reason), cap]).finally(() => {
430
+ if (capTimer)
431
+ clearTimeout(capTimer);
432
+ agent.abort();
433
+ exit();
434
+ });
435
+ }, [exit]);
393
436
  const handleCommand = useCallback((command) => {
394
437
  const [name, ...rest] = command.split(/\s+/);
395
438
  const arg = rest.join(" ");
@@ -557,6 +600,9 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
557
600
  break;
558
601
  case "/logout": {
559
602
  clearQueue();
603
+ void agentRef.current?.endSession("logout");
604
+ setPendingHookTrust(null);
605
+ deferredPromptRef.current = null;
560
606
  const updated = { ...settings, token: undefined };
561
607
  setSettings(updated);
562
608
  saveSettings(updated);
@@ -565,12 +611,12 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
565
611
  break;
566
612
  }
567
613
  case "/exit":
568
- exit();
614
+ endAndExit("prompt_input_exit");
569
615
  break;
570
616
  default:
571
617
  pushRow({ kind: "error", text: `Unknown command: ${name}. Try /help.` });
572
618
  }
573
- }, [settings, tasks, contextTokens, totalCost, sessionTitle, exit, pushRow, getAgent, switchModel, resetTranscript, clearQueue]);
619
+ }, [settings, tasks, contextTokens, totalCost, sessionTitle, endAndExit, pushRow, getAgent, switchModel, resetTranscript, clearQueue]);
574
620
  const handleSubmit = useCallback((value) => {
575
621
  if (value.startsWith("/")) {
576
622
  handleCommand(value);
@@ -592,6 +638,28 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
592
638
  setBusyLabel("Thinking");
593
639
  void getAgent().runTurn(value);
594
640
  }, [settings, busy, handleCommand, enqueueMessage, getAgent, pushRow]);
641
+ // Resolve the project-hook trust prompt: enable hooks for this workspace (and
642
+ // the live agent) on approval, then run any prompt we deferred while asking.
643
+ const resolveHookTrust = useCallback((trust) => {
644
+ setPendingHookTrust(null);
645
+ if (trust) {
646
+ trustProjectHooks(process.cwd());
647
+ const updated = loadSettings();
648
+ setSettings(updated);
649
+ agentRef.current?.setHooks(updated.hooks);
650
+ pushRow({ kind: "info", text: "Project hooks trusted — enabled for this workspace." });
651
+ }
652
+ else {
653
+ pushRow({
654
+ kind: "info",
655
+ text: "Project hooks left disabled. Review .orbcode/settings.json and restart to re-decide.",
656
+ });
657
+ }
658
+ const deferred = deferredPromptRef.current;
659
+ deferredPromptRef.current = null;
660
+ if (deferred)
661
+ handleSubmit(deferred);
662
+ }, [handleSubmit, pushRow]);
595
663
  // Apply --resume and an initial prompt (`orbcode "do something"`) on startup.
596
664
  const bootedRef = useRef(false);
597
665
  useEffect(() => {
@@ -600,8 +668,16 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
600
668
  bootedRef.current = true;
601
669
  if (initialSession)
602
670
  handleResume(initialSession);
603
- if (initialPrompt)
671
+ // If this project ships untrusted hooks, ask before running anything; the
672
+ // startup prompt waits until the user decides.
673
+ const pending = getPendingProjectHooks(process.cwd());
674
+ if (pending) {
675
+ setPendingHookTrust(pending);
676
+ deferredPromptRef.current = initialPrompt ?? null;
677
+ }
678
+ else if (initialPrompt) {
604
679
  handleSubmit(initialPrompt);
680
+ }
605
681
  refreshUsage();
606
682
  }, [initialSession, initialPrompt, handleResume, handleSubmit, refreshUsage]);
607
683
  // Resolve the npm version check after first paint so the TUI shows up
@@ -623,8 +699,7 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
623
699
  // followup). InputBox swallows other Ctrl-combos, but this hook is a
624
700
  // sibling of InputBox's hook, so it still sees the key.
625
701
  if (key.ctrl && input === "d") {
626
- agentRef.current?.abort();
627
- exit();
702
+ endAndExit("prompt_input_exit");
628
703
  return;
629
704
  }
630
705
  if (key.escape && busy && !pendingApproval && !pendingFollowup) {
@@ -665,21 +740,26 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
665
740
  pushRow({ kind: "info", text: `Signed in${who ? ` as ${who}` : ""}. Ready when you are.` });
666
741
  }, [pushRow]);
667
742
  const taskLines = useMemo(() => tasks.split("\n").map((l) => l.trim()).filter(Boolean), [tasks]);
668
- const inputActive = view === "chat" && !pendingApproval && !pendingFollowup && !modelPickerOpen && !resumableSessions;
743
+ const inputActive = view === "chat" &&
744
+ !pendingApproval &&
745
+ !pendingFollowup &&
746
+ !pendingHookTrust &&
747
+ !modelPickerOpen &&
748
+ !resumableSessions;
669
749
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: rows, children: (row) => _jsx(RowView, { row: row }, row.id) }, staticKey), view === "login" ? (_jsx(LoginSection, { onLogin: handleLogin })) : (_jsxs(Box, { flexDirection: "column", children: [updateInfo?.updateAvailable && updateInfo.latest && (_jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: COLORS.warning, paddingX: 2, alignSelf: "flex-start", children: [_jsxs(Text, { color: COLORS.warning, bold: true, children: ["\u2191 Update available: v", updateInfo.current, " \u2192 v", updateInfo.latest] }), _jsxs(Text, { children: ["Run ", _jsx(Text, { color: COLORS.accent, children: "orbcode update" }), " to install the latest version, then relaunch."] })] })), streamingReasoning && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: COLORS.thinking, italic: true, children: "\u2726 Thinking\u2026" }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, italic: true, children: tail(streamingReasoning, expandReasoningRef.current ? 30 : 3) }) })] })), streamingText && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: COLORS.primary, children: "\u25CF " }), streamingText] }) })), runningTool && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: COLORS.warning, children: [formatToolName(runningTool.name), " ", _jsx(Text, { dimColor: true, children: runningTool.summary })] }) })), taskLines.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: "Tasks" }), taskLines.slice(0, 10).map((line, i) => (_jsx(Text, { dimColor: /^[-*]\s*\[x\]/i.test(line), children: line
670
750
  .replace(/^[-*]\s*\[x\]/i, " ■")
671
751
  .replace(/^[-*]\s*\[-\]/, " ◧")
672
752
  .replace(/^[-*]\s*\[ \]/, " □") }, i))), taskLines.length > 10 && _jsxs(Text, { dimColor: true, children: [" \u2026 ", taskLines.length - 10, " more"] })] })), modelPickerOpen && (_jsx(Box, { marginTop: 1, children: _jsx(ModelPicker, { currentId: settings.model, onSelect: (modelId) => {
673
753
  setModelPickerOpen(false);
674
754
  switchModel(modelId);
675
- }, onCancel: () => setModelPickerOpen(false) }) })), resumableSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: resumableSessions, onSelect: handleResume, onCancel: () => setResumableSessions(null) }) })), pendingApproval && (_jsx(Box, { marginTop: 1, children: _jsx(ApprovalPrompt, { request: pendingApproval.request, onDecision: (decision) => {
755
+ }, onCancel: () => setModelPickerOpen(false) }) })), resumableSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: resumableSessions, onSelect: handleResume, onCancel: () => setResumableSessions(null) }) })), pendingHookTrust && (_jsx(Box, { marginTop: 1, children: _jsx(HookTrustPrompt, { cwd: process.cwd(), commands: pendingHookTrust.commands, onDecision: resolveHookTrust }) })), pendingApproval && (_jsx(Box, { marginTop: 1, children: _jsx(ApprovalPrompt, { request: pendingApproval.request, onDecision: (decision) => {
676
756
  pendingApproval.resolve(decision);
677
757
  setPendingApproval(null);
678
758
  } }) })), pendingFollowup && (_jsx(Box, { marginTop: 1, children: _jsx(FollowupPrompt, { question: pendingFollowup.question, suggestions: pendingFollowup.suggestions, onAnswer: (answer) => {
679
759
  pushRow({ kind: "user", text: answer });
680
760
  pendingFollowup.resolve(answer);
681
761
  setPendingFollowup(null);
682
- } }) })), busy && !pendingApproval && !pendingFollowup && !streamingText && (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: busyLabel }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [queuedMessages.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, marginBottom: 1, children: [_jsxs(Text, { dimColor: true, bold: true, children: ["Queue (", queuedMessages.length, ")"] }), queuedMessages.slice(0, 5).map((msg, i) => (_jsxs(Text, { dimColor: true, children: [i + 1, ". ", truncateForQueue(msg).replace(/\n/g, "↵")] }, i))), queuedMessages.length > 5 && (_jsxs(Text, { dimColor: true, children: [" \u2026 ", queuedMessages.length - 5, " more"] }))] })), _jsx(InputBox, { active: inputActive, slashCommands: SLASH_COMMANDS, onSubmit: handleSubmit }), _jsx(StatusBar, { modelId: settings.model, contextTokens: contextTokens, totalCost: totalCost, state: busy ? busyLabel : "", approvalMode: approvalMode, busy: busy, title: sessionTitle, plan: usage?.plan, usagePercentage: usage?.usagePercentage, tieredUsage: usage?.tieredUsage })] })] }))] }));
762
+ } }) })), busy && !pendingApproval && !pendingFollowup && !pendingHookTrust && !streamingText && (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: busyLabel }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [queuedMessages.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, marginBottom: 1, children: [_jsxs(Text, { dimColor: true, bold: true, children: ["Queue (", queuedMessages.length, ")"] }), queuedMessages.slice(0, 5).map((msg, i) => (_jsxs(Text, { dimColor: true, children: [i + 1, ". ", truncateForQueue(msg).replace(/\n/g, "↵")] }, i))), queuedMessages.length > 5 && (_jsxs(Text, { dimColor: true, children: [" \u2026 ", queuedMessages.length - 5, " more"] }))] })), _jsx(InputBox, { active: inputActive, slashCommands: SLASH_COMMANDS, onSubmit: handleSubmit }), _jsx(StatusBar, { modelId: settings.model, contextTokens: contextTokens, totalCost: totalCost, state: busy ? busyLabel : "", approvalMode: approvalMode, busy: busy, title: sessionTitle, plan: usage?.plan, usagePercentage: usage?.usagePercentage, tieredUsage: usage?.tieredUsage })] })] }))] }));
683
763
  }
684
764
  function tail(text, lines) {
685
765
  const all = text.split("\n").filter((l) => l.trim());
@@ -0,0 +1,21 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { COLORS } from "../../branding.js";
4
+ const MAX_SHOWN = 8;
5
+ /**
6
+ * Shown at startup when the current project defines hooks that haven't been
7
+ * trusted yet. Project hooks run arbitrary shell commands, so they stay
8
+ * disabled until the user explicitly approves them here.
9
+ */
10
+ export function HookTrustPrompt({ cwd, commands, onDecision }) {
11
+ useInput((input, key) => {
12
+ const lower = input.toLowerCase();
13
+ if (lower === "y")
14
+ onDecision(true);
15
+ else if (lower === "n" || key.escape || key.return)
16
+ onDecision(false);
17
+ });
18
+ const shown = commands.slice(0, MAX_SHOWN);
19
+ const extra = commands.length - shown.length;
20
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: COLORS.error, paddingX: 1, children: [_jsxs(Text, { bold: true, color: COLORS.error, children: ["\u26A0 This project defines ", commands.length, " hook command", commands.length === 1 ? "" : "s"] }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: [cwd, "/.orbcode/settings.json"] }), _jsx(Text, { children: "Project hooks run these shell commands automatically during the session:" }), shown.map((command, i) => (_jsxs(Text, { color: COLORS.warning, children: [" • ", command.length > 100 ? command.slice(0, 99) + "…" : command] }, i))), extra > 0 && _jsxs(Text, { dimColor: true, children: [" \u2026 ", extra, " more"] })] }), _jsx(Text, { dimColor: true, children: "Only trust hooks from a repository you trust. (y) trust & enable \u00B7 (n or Enter) keep disabled" })] }));
21
+ }
@@ -33,25 +33,18 @@ function truncate(text, max) {
33
33
  return text.length > max ? text.slice(0, max - 1) + "…" : text;
34
34
  }
35
35
  /**
36
- * Picks the most constrained window (highest percentage used) and returns a
37
- * compact string like "5hr 12% · resets in 3h 15m".
36
+ * Formats the 5hr window as "5hr limit XX% · resets <relative>".
37
+ * Returns null when tiered usage is unavailable so the line can be hidden.
38
38
  */
39
- function mostConstrainedSummary(tu) {
40
- const windows = [
41
- ["5hr", tu.fiveHour.percentage, tu.fiveHour.resetsAt],
42
- ["wk", tu.weekly.percentage, tu.weekly.resetsAt],
43
- ["mo", tu.monthly.percentage, tu.monthly.resetsAt],
44
- ];
45
- const [label, pct, resetsAt] = windows.reduce((best, cur) => cur[1] >= best[1] ? cur : best);
46
- return `${label} ${Math.round(pct)}% · resets ${formatRelativeTime(resetsAt)}`;
39
+ function fiveHourSummary(tu) {
40
+ if (!tu?.fiveHour)
41
+ return null;
42
+ const { percentage, resetsAt } = tu.fiveHour;
43
+ return `5hr limit ${Math.round(percentage)}% · resets ${formatRelativeTime(resetsAt)}`;
47
44
  }
48
- export function StatusBar({ modelId, contextTokens, totalCost, state, approvalMode, busy, title, plan, usagePercentage, tieredUsage, }) {
45
+ export function StatusBar({ modelId, contextTokens, totalCost: _totalCost, state, approvalMode, busy, title, plan: _plan, usagePercentage: _usagePercentage, tieredUsage, }) {
49
46
  const model = getModel(modelId);
50
47
  const contextPct = Math.min(100, Math.round((contextTokens / model.contextWindow) * 100));
51
- const constrainedLine = tieredUsage
52
- ? mostConstrainedSummary(tieredUsage)
53
- : typeof usagePercentage === "number"
54
- ? `${Math.round(usagePercentage)}% used`
55
- : null;
56
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: approvalMode === "ask" ? undefined : COLORS.warning, children: MODE_LABELS[approvalMode] }), " (shift+tab to cycle)", busy && " · esc to interrupt", state ? _jsxs(Text, { color: COLORS.thinking, children: [" \u00B7 ", state] }) : null] }), _jsxs(Text, { dimColor: true, children: [title ? `${truncate(title, 32)} · ` : "", model.name, " \u00B7 ctx ", contextTokens.toLocaleString(), " (", contextPct, "%)", model.free ? " · free" : ` · $${totalCost.toFixed(4)}`] })] }), (plan || constrainedLine) && (_jsx(Box, { justifyContent: "flex-end", children: _jsxs(Text, { dimColor: true, children: [plan && constrainedLine ? " · " : "", constrainedLine] }) }))] }));
48
+ const fiveHourLine = fiveHourSummary(tieredUsage);
49
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: approvalMode === "ask" ? undefined : COLORS.warning, children: MODE_LABELS[approvalMode] }), " (shift+tab to cycle)", busy && " · esc to interrupt", state ? _jsxs(Text, { color: COLORS.thinking, children: [" \u00B7 ", state] }) : null] }), _jsxs(Text, { dimColor: true, children: [title ? `${truncate(title, 32)} · ` : "", model.name, " \u00B7 ctx ", contextTokens.toLocaleString(), " (", contextPct, "%)"] })] }), fiveHourLine && (_jsx(Box, { justifyContent: "flex-end", children: _jsx(Text, { dimColor: true, children: fiveHourLine }) }))] }));
57
50
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matterailab/orbcode",
3
- "version": "0.1.8",
3
+ "version": "0.1.13",
4
4
  "description": "OrbCode CLI — agentic coding in your terminal, powered by Axon models by MatterAI",
5
5
  "type": "module",
6
6
  "bin": {