@tokentop/ttop 0.2.2 → 0.4.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.
Files changed (160) hide show
  1. package/README.md +2 -5
  2. package/package.json +8 -5
  3. package/src/agents/aggregator.ts +39 -19
  4. package/src/agents/costing.ts +11 -9
  5. package/src/agents/index.ts +7 -9
  6. package/src/agents/types.ts +2 -2
  7. package/src/cli.ts +28 -23
  8. package/src/config/schema.test.ts +42 -40
  9. package/src/config/schema.ts +26 -26
  10. package/src/demo/simulator.ts +161 -122
  11. package/src/plugins/agents/index.ts +2 -2
  12. package/src/plugins/auth-sources.ts +42 -38
  13. package/src/plugins/lifecycle.ts +42 -38
  14. package/src/plugins/loader.test.ts +49 -49
  15. package/src/plugins/loader.ts +67 -63
  16. package/src/plugins/notification-bus.ts +21 -16
  17. package/src/plugins/notifications/index.ts +2 -2
  18. package/src/plugins/notifications/terminal-bell.ts +36 -23
  19. package/src/plugins/notifications/visual-flash.ts +31 -19
  20. package/src/plugins/npm-installer.ts +23 -25
  21. package/src/plugins/plugin-context-factory.ts +10 -10
  22. package/src/plugins/plugin-host.ts +12 -5
  23. package/src/plugins/providers/anthropic.ts +64 -57
  24. package/src/plugins/providers/antigravity.ts +81 -69
  25. package/src/plugins/providers/codex.ts +42 -42
  26. package/src/plugins/providers/gemini.ts +106 -87
  27. package/src/plugins/providers/github-copilot.ts +49 -45
  28. package/src/plugins/providers/index.ts +10 -12
  29. package/src/plugins/providers/minimax.ts +50 -44
  30. package/src/plugins/providers/openai-api.ts +82 -62
  31. package/src/plugins/providers/opencode-zen.ts +46 -39
  32. package/src/plugins/providers/perplexity.ts +55 -46
  33. package/src/plugins/providers/zai.ts +68 -57
  34. package/src/plugins/registry.ts +80 -71
  35. package/src/plugins/sandbox-guard.test.ts +45 -48
  36. package/src/plugins/sandbox-guard.ts +20 -25
  37. package/src/plugins/sandbox.ts +26 -48
  38. package/src/plugins/themes/catppuccin-latte.ts +29 -29
  39. package/src/plugins/themes/catppuccin-mocha.ts +29 -29
  40. package/src/plugins/themes/claude-code.ts +29 -29
  41. package/src/plugins/themes/dracula.ts +29 -29
  42. package/src/plugins/themes/github-light.ts +29 -29
  43. package/src/plugins/themes/gruvbox-dark.ts +29 -29
  44. package/src/plugins/themes/gruvbox-light.ts +29 -29
  45. package/src/plugins/themes/index.ts +15 -15
  46. package/src/plugins/themes/kanagawa.ts +29 -29
  47. package/src/plugins/themes/nord.ts +29 -29
  48. package/src/plugins/themes/one-dark.ts +29 -29
  49. package/src/plugins/themes/opencode.ts +31 -31
  50. package/src/plugins/themes/rose-pine-dawn.ts +29 -29
  51. package/src/plugins/themes/rose-pine.ts +29 -29
  52. package/src/plugins/themes/solarized-light.ts +29 -29
  53. package/src/plugins/themes/tokyo-night.ts +29 -29
  54. package/src/plugins/types/agent.ts +5 -5
  55. package/src/plugins/types/base.ts +35 -24
  56. package/src/plugins/types/index.ts +9 -9
  57. package/src/plugins/types/notification.ts +12 -12
  58. package/src/plugins/types/provider.ts +12 -8
  59. package/src/plugins/types/theme.ts +31 -23
  60. package/src/plugins/update-checker.ts +6 -6
  61. package/src/pricing/estimator.test.ts +39 -44
  62. package/src/pricing/estimator.ts +7 -13
  63. package/src/pricing/fallback.test.ts +30 -34
  64. package/src/pricing/fallback.ts +54 -27
  65. package/src/pricing/index.ts +18 -12
  66. package/src/pricing/models-dev.ts +6 -6
  67. package/src/storage/database.ts +56 -46
  68. package/src/storage/db.ts +12 -12
  69. package/src/storage/index.ts +44 -45
  70. package/src/storage/migrations/index.ts +2 -2
  71. package/src/storage/paths.ts +13 -13
  72. package/src/storage/persistence-service.ts +206 -0
  73. package/src/storage/repos/agentSessions.ts +33 -23
  74. package/src/storage/repos/providerSnapshots.ts +5 -5
  75. package/src/storage/repos/usageEvents.ts +40 -28
  76. package/src/storage/retention.ts +146 -0
  77. package/src/storage/types.ts +5 -5
  78. package/src/tui/App.tsx +180 -137
  79. package/src/tui/components/CommandPalette.tsx +46 -34
  80. package/src/tui/components/DebugConsole.tsx +39 -33
  81. package/src/tui/components/DebugPanel.tsx +314 -183
  82. package/src/tui/components/FooterHints.tsx +2 -2
  83. package/src/tui/components/GhostProviderCard.tsx +37 -35
  84. package/src/tui/components/Header.tsx +68 -35
  85. package/src/tui/components/HelpOverlay.tsx +99 -29
  86. package/src/tui/components/InlineGauge.tsx +10 -7
  87. package/src/tui/components/InlineSparkline.tsx +17 -11
  88. package/src/tui/components/KpiStrip.tsx +128 -98
  89. package/src/tui/components/LimitGauge.tsx +96 -59
  90. package/src/tui/components/ModalBackdrop.tsx +4 -4
  91. package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
  92. package/src/tui/components/ProviderCard.tsx +232 -226
  93. package/src/tui/components/ProviderDetailPanel.tsx +133 -58
  94. package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
  95. package/src/tui/components/ProvidersList.tsx +215 -126
  96. package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
  97. package/src/tui/components/SessionsTable.tsx +320 -169
  98. package/src/tui/components/SettingsModal.tsx +593 -269
  99. package/src/tui/components/SidebarBreakdown.tsx +30 -16
  100. package/src/tui/components/Skeleton.tsx +24 -17
  101. package/src/tui/components/SmartSidebar.tsx +360 -258
  102. package/src/tui/components/Sparkline.tsx +64 -52
  103. package/src/tui/components/Spinner.tsx +3 -3
  104. package/src/tui/components/StatusBar.tsx +27 -27
  105. package/src/tui/components/ThemePicker.tsx +67 -66
  106. package/src/tui/components/Toast.tsx +9 -6
  107. package/src/tui/components/UsageGauge.tsx +10 -13
  108. package/src/tui/components/index.ts +5 -5
  109. package/src/tui/contexts/AgentSessionContext.tsx +164 -151
  110. package/src/tui/contexts/ConfigContext.tsx +15 -13
  111. package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
  112. package/src/tui/contexts/DemoModeContext.tsx +22 -15
  113. package/src/tui/contexts/DrawerContext.tsx +11 -9
  114. package/src/tui/contexts/InputContext.tsx +2 -2
  115. package/src/tui/contexts/LogContext.tsx +62 -47
  116. package/src/tui/contexts/PluginContext.tsx +238 -183
  117. package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
  118. package/src/tui/contexts/StorageContext.tsx +95 -149
  119. package/src/tui/contexts/ThemeContext.tsx +21 -16
  120. package/src/tui/contexts/TimeWindowContext.tsx +72 -61
  121. package/src/tui/contexts/ToastContext.tsx +10 -7
  122. package/src/tui/contexts/index.ts +9 -5
  123. package/src/tui/createApp.tsx +8 -12
  124. package/src/tui/debug/captureFrame.ts +25 -28
  125. package/src/tui/debug/snapshot.tsx +179 -162
  126. package/src/tui/driver/assertions.ts +30 -22
  127. package/src/tui/driver/cli.ts +231 -221
  128. package/src/tui/driver/coverage.ts +27 -44
  129. package/src/tui/driver/demo-snapshot.ts +33 -33
  130. package/src/tui/driver/diff.ts +26 -32
  131. package/src/tui/driver/driver.ts +43 -48
  132. package/src/tui/driver/index.ts +36 -40
  133. package/src/tui/driver/recorder.ts +37 -48
  134. package/src/tui/driver/test-workflow.ts +51 -51
  135. package/src/tui/driver/test.ts +25 -25
  136. package/src/tui/hooks/index.ts +24 -20
  137. package/src/tui/hooks/useAnimatedValue.ts +13 -18
  138. package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
  139. package/src/tui/hooks/useDashboardState.ts +26 -24
  140. package/src/tui/hooks/useEmaActivity.ts +82 -74
  141. package/src/tui/hooks/useEntranceAnimation.ts +11 -11
  142. package/src/tui/hooks/useExitAnimation.ts +7 -3
  143. package/src/tui/hooks/usePulse.ts +4 -4
  144. package/src/tui/hooks/useSafeRenderer.ts +5 -5
  145. package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
  146. package/src/tui/hooks/useValueFlash.ts +10 -14
  147. package/src/tui/index.tsx +29 -6
  148. package/src/tui/shutdown.ts +19 -0
  149. package/src/tui/utils/providerColor.ts +1 -1
  150. package/src/tui/views/Dashboard.tsx +164 -102
  151. package/src/tui/views/HistoricalTrendsView.tsx +579 -250
  152. package/src/tui/views/ProjectsView.tsx +495 -243
  153. package/src/tui/views/RealTimeDashboard.tsx +176 -116
  154. package/src/tui/views/SettingsView.tsx +225 -189
  155. package/src/tui/views/index.ts +2 -2
  156. package/src/types/opentui-test-utils.d.ts +7 -7
  157. package/src/utils/clipboard.ts +29 -29
  158. package/src/utils/currency.test.ts +53 -53
  159. package/src/utils/currency.ts +35 -32
  160. package/src/version.ts +1 -1
@@ -1,17 +1,22 @@
1
+ import type { PluginsConfig } from "@/config/schema.ts";
2
+ import {
3
+ discoverLocalPlugins,
4
+ loadLocalPlugin,
5
+ loadNpmPlugin,
6
+ resolvePluginPath,
7
+ } from "./loader.ts";
8
+ import { installAllNpmPlugins, resolveNpmPluginPath } from "./npm-installer.ts";
1
9
  import type {
10
+ AgentPlugin,
2
11
  AnyPlugin,
3
- PluginType,
12
+ NotificationPlugin,
4
13
  PluginByType,
14
+ PluginType,
5
15
  ProviderPlugin,
6
- AgentPlugin,
7
16
  ThemePlugin,
8
- NotificationPlugin,
9
- } from './types/index.ts';
10
- import { loadLocalPlugin, loadNpmPlugin, discoverLocalPlugins, resolvePluginPath } from './loader.ts';
11
- import { installAllNpmPlugins, resolveNpmPluginPath } from './npm-installer.ts';
12
- import type { PluginsConfig } from '@/config/schema.ts';
17
+ } from "./types/index.ts";
13
18
 
14
- export type PluginSource = 'builtin' | 'local' | 'npm';
19
+ export type PluginSource = "builtin" | "local" | "npm";
15
20
 
16
21
  type PluginStore = {
17
22
  provider: Map<string, ProviderPlugin>;
@@ -30,46 +35,30 @@ class PluginRegistryImpl {
30
35
 
31
36
  private sources = new Map<string, PluginSource>();
32
37
  private initialized = false;
38
+ private officialIds = new Set<string>();
33
39
 
34
40
  private sourceKey(type: string, id: string): string {
35
41
  return `${type}-${id}`;
36
42
  }
37
43
 
38
- register(plugin: AnyPlugin, source: PluginSource = 'builtin'): void {
44
+ register(plugin: AnyPlugin, source: PluginSource = "builtin"): void {
39
45
  const key = this.sourceKey(plugin.type, plugin.id);
40
46
  const existingSource = this.sources.get(key);
41
47
 
42
- if (existingSource === 'builtin' && source !== 'builtin') {
48
+ // Priority: local > npm > builtin
49
+ // If a higher-priority source already registered this plugin, skip.
50
+ const priority: Record<PluginSource, number> = { local: 2, npm: 1, builtin: 0 };
51
+ if (existingSource && priority[existingSource] > priority[source]) {
43
52
  return;
44
53
  }
45
54
 
46
- this.sources.set(key, source);
47
- switch (plugin.type) {
48
- case 'provider':
49
- if (this.plugins.provider.has(plugin.id)) {
50
- console.warn(`Plugin "${plugin.id}" already registered, overwriting`);
51
- }
52
- this.plugins.provider.set(plugin.id, plugin);
53
- break;
54
- case 'agent':
55
- if (this.plugins.agent.has(plugin.id)) {
56
- console.warn(`Plugin "${plugin.id}" already registered, overwriting`);
57
- }
58
- this.plugins.agent.set(plugin.id, plugin);
59
- break;
60
- case 'theme':
61
- if (this.plugins.theme.has(plugin.id)) {
62
- console.warn(`Plugin "${plugin.id}" already registered, overwriting`);
63
- }
64
- this.plugins.theme.set(plugin.id, plugin);
65
- break;
66
- case 'notification':
67
- if (this.plugins.notification.has(plugin.id)) {
68
- console.warn(`Plugin "${plugin.id}" already registered, overwriting`);
69
- }
70
- this.plugins.notification.set(plugin.id, plugin);
71
- break;
55
+ if (existingSource && existingSource !== source) {
56
+ console.info(`Plugin "${plugin.id}" overridden by ${source} (was ${existingSource})`);
72
57
  }
58
+
59
+ this.sources.set(key, source);
60
+ const map = this.plugins[plugin.type] as Map<string, AnyPlugin>;
61
+ map.set(plugin.id, plugin);
73
62
  }
74
63
 
75
64
  unregister(type: PluginType, id: string): boolean {
@@ -113,43 +102,59 @@ class PluginRegistryImpl {
113
102
  return this.sources.get(this.sourceKey(type, id));
114
103
  }
115
104
 
105
+ isOfficial(type: PluginType, id: string): boolean {
106
+ return this.officialIds.has(this.sourceKey(type, id));
107
+ }
108
+
116
109
  async loadBuiltinPlugins(): Promise<void> {
117
110
  const [providers, agents, themes, notifications] = await Promise.all([
118
- import('./providers/index.ts'),
119
- import('./agents/index.ts'),
120
- import('./themes/index.ts'),
121
- import('./notifications/index.ts'),
111
+ import("./providers/index.ts"),
112
+ import("./agents/index.ts"),
113
+ import("./themes/index.ts"),
114
+ import("./notifications/index.ts"),
122
115
  ]);
123
116
 
124
117
  for (const plugin of Object.values(providers)) {
125
- if (isProviderPlugin(plugin)) this.register(plugin, 'builtin');
118
+ if (isProviderPlugin(plugin)) {
119
+ this.register(plugin, "builtin");
120
+ this.officialIds.add(this.sourceKey(plugin.type, plugin.id));
121
+ }
126
122
  }
127
123
 
128
124
  for (const plugin of Object.values(agents)) {
129
- if (isAgentPlugin(plugin)) this.register(plugin, 'builtin');
125
+ if (isAgentPlugin(plugin)) {
126
+ this.register(plugin, "builtin");
127
+ this.officialIds.add(this.sourceKey(plugin.type, plugin.id));
128
+ }
130
129
  }
131
130
 
132
131
  for (const plugin of Object.values(themes)) {
133
- if (isThemePlugin(plugin)) this.register(plugin, 'builtin');
132
+ if (isThemePlugin(plugin)) {
133
+ this.register(plugin, "builtin");
134
+ this.officialIds.add(this.sourceKey(plugin.type, plugin.id));
135
+ }
134
136
  }
135
137
 
136
138
  for (const plugin of Object.values(notifications)) {
137
- if (isNotificationPlugin(plugin)) this.register(plugin, 'builtin');
139
+ if (isNotificationPlugin(plugin)) {
140
+ this.register(plugin, "builtin");
141
+ this.officialIds.add(this.sourceKey(plugin.type, plugin.id));
142
+ }
138
143
  }
139
144
  }
140
145
 
141
146
  async loadLocalPlugins(extraPaths: string[] = []): Promise<void> {
142
147
  const discoveredPaths = await discoverLocalPlugins();
143
- const resolvedExtraPaths = extraPaths.map(p => resolvePluginPath(p));
148
+ const resolvedExtraPaths = extraPaths.map((p) => resolvePluginPath(p));
144
149
  const allPaths = [...discoveredPaths, ...resolvedExtraPaths];
145
150
 
146
- for (const pluginPath of allPaths) {
147
- const result = await loadLocalPlugin(pluginPath);
151
+ const results = await Promise.all(allPaths.map((p) => loadLocalPlugin(p)));
152
+ for (const [i, result] of results.entries()) {
148
153
  if (result.success && result.plugin) {
149
- this.register(result.plugin, 'local');
154
+ this.register(result.plugin, "local");
150
155
  console.info(`Loaded local plugin: ${result.plugin.name} (${result.plugin.id})`);
151
156
  } else {
152
- console.warn(`Failed to load plugin from ${pluginPath}: ${result.error}`);
157
+ console.warn(`Failed to load plugin from ${allPaths[i]}: ${result.error}`);
153
158
  }
154
159
  }
155
160
  }
@@ -166,11 +171,16 @@ class PluginRegistryImpl {
166
171
  }
167
172
  }
168
173
 
169
- for (const packageName of packages) {
170
- const resolvedPath = resolveNpmPluginPath(packageName);
171
- const result = await loadNpmPlugin(packageName, resolvedPath);
174
+ const loadResults = await Promise.all(
175
+ packages.map(async (packageName) => {
176
+ const resolvedPath = resolveNpmPluginPath(packageName);
177
+ const result = await loadNpmPlugin(packageName, resolvedPath);
178
+ return { packageName, result };
179
+ }),
180
+ );
181
+ for (const { packageName, result } of loadResults) {
172
182
  if (result.success && result.plugin) {
173
- this.register(result.plugin, 'npm');
183
+ this.register(result.plugin, "npm");
174
184
  console.info(`Loaded npm plugin: ${result.plugin.name} (${packageName})`);
175
185
  } else {
176
186
  console.warn(`Failed to load npm plugin ${packageName}: ${result.error}`);
@@ -190,10 +200,7 @@ class PluginRegistryImpl {
190
200
 
191
201
  await this.loadBuiltinPlugins();
192
202
 
193
- const localPaths = [
194
- ...(config?.plugins?.local ?? []),
195
- ...(config?.cliPlugins ?? []),
196
- ];
203
+ const localPaths = [...(config?.plugins?.local ?? []), ...(config?.cliPlugins ?? [])];
197
204
  await this.loadLocalPlugins(localPaths);
198
205
 
199
206
  const npmPackages = config?.plugins?.npm ?? [];
@@ -203,7 +210,7 @@ class PluginRegistryImpl {
203
210
 
204
211
  const disabled = config?.plugins?.disabled ?? [];
205
212
  for (const pluginId of disabled) {
206
- for (const type of ['provider', 'agent', 'theme', 'notification'] as PluginType[]) {
213
+ for (const type of ["provider", "agent", "theme", "notification"] as PluginType[]) {
207
214
  if (this.has(type, pluginId)) {
208
215
  this.disablePlugin(type, pluginId);
209
216
  console.info(`Disabled plugin: ${pluginId}`);
@@ -213,38 +220,40 @@ class PluginRegistryImpl {
213
220
 
214
221
  this.initialized = true;
215
222
  console.info(
216
- `Plugin registry initialized: ${this.count('provider')} providers, ` +
217
- `${this.count('agent')} agents, ${this.count('theme')} themes, ` +
218
- `${this.count('notification')} notifications`
223
+ `Plugin registry initialized: ${this.count("provider")} providers, ` +
224
+ `${this.count("agent")} agents, ${this.count("theme")} themes, ` +
225
+ `${this.count("notification")} notifications`,
219
226
  );
220
227
  }
221
228
  }
222
229
 
223
- function hasPluginShape(obj: unknown): obj is { id: string; type: string; name: string; version: string } {
230
+ function hasPluginShape(
231
+ obj: unknown,
232
+ ): obj is { id: string; type: string; name: string; version: string } {
224
233
  return (
225
234
  obj !== null &&
226
- typeof obj === 'object' &&
227
- 'id' in obj &&
228
- 'type' in obj &&
229
- 'name' in obj &&
230
- 'version' in obj
235
+ typeof obj === "object" &&
236
+ "id" in obj &&
237
+ "type" in obj &&
238
+ "name" in obj &&
239
+ "version" in obj
231
240
  );
232
241
  }
233
242
 
234
243
  function isProviderPlugin(obj: unknown): obj is ProviderPlugin {
235
- return hasPluginShape(obj) && obj.type === 'provider';
244
+ return hasPluginShape(obj) && obj.type === "provider";
236
245
  }
237
246
 
238
247
  function isAgentPlugin(obj: unknown): obj is AgentPlugin {
239
- return hasPluginShape(obj) && obj.type === 'agent';
248
+ return hasPluginShape(obj) && obj.type === "agent";
240
249
  }
241
250
 
242
251
  function isThemePlugin(obj: unknown): obj is ThemePlugin {
243
- return hasPluginShape(obj) && obj.type === 'theme';
252
+ return hasPluginShape(obj) && obj.type === "theme";
244
253
  }
245
254
 
246
255
  function isNotificationPlugin(obj: unknown): obj is NotificationPlugin {
247
- return hasPluginShape(obj) && obj.type === 'notification';
256
+ return hasPluginShape(obj) && obj.type === "notification";
248
257
  }
249
258
 
250
259
  export const pluginRegistry = new PluginRegistryImpl();
@@ -1,37 +1,34 @@
1
- import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
2
  import {
3
3
  deepFreeze,
4
4
  getActivePluginGuard,
5
5
  installGlobalFetchGuard,
6
6
  runInPluginGuard,
7
- } from './sandbox-guard.ts';
8
- import { PluginPermissionError } from './types/base.ts';
7
+ } from "./sandbox-guard.ts";
8
+ import { PluginPermissionError } from "./types/base.ts";
9
9
 
10
10
  const originalFetch = globalThis.fetch;
11
11
  const fetchCalls: string[] = [];
12
12
 
13
13
  const mockFetch: typeof fetch = Object.assign(
14
14
  (input: string | URL | Request, _init?: RequestInit): Promise<Response> => {
15
- const url = typeof input === 'string'
16
- ? input
17
- : input instanceof URL
18
- ? input.toString()
19
- : input.url;
15
+ const url =
16
+ typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
20
17
  fetchCalls.push(url);
21
- return Promise.resolve(new Response('ok', { status: 200 }));
18
+ return Promise.resolve(new Response("ok", { status: 200 }));
22
19
  },
23
20
  { preconnect: originalFetch.preconnect },
24
21
  );
25
22
 
26
- describe('deepFreeze', () => {
27
- test('freezes a plain object', () => {
23
+ describe("deepFreeze", () => {
24
+ test("freezes a plain object", () => {
28
25
  const obj = { a: 1 };
29
26
  deepFreeze(obj);
30
27
 
31
28
  expect(Object.isFrozen(obj)).toBe(true);
32
29
  });
33
30
 
34
- test('freezes nested objects recursively', () => {
31
+ test("freezes nested objects recursively", () => {
35
32
  const obj = {
36
33
  top: {
37
34
  child: {
@@ -47,19 +44,19 @@ describe('deepFreeze', () => {
47
44
  expect(Object.isFrozen(obj.top.child)).toBe(true);
48
45
  });
49
46
 
50
- test('returns the same reference', () => {
47
+ test("returns the same reference", () => {
51
48
  const obj = { nested: { value: 1 } };
52
49
  const frozen = deepFreeze(obj);
53
50
 
54
51
  expect(frozen).toBe(obj);
55
52
  });
56
53
 
57
- test('handles null and undefined gracefully', () => {
54
+ test("handles null and undefined gracefully", () => {
58
55
  expect(deepFreeze(null)).toBeNull();
59
56
  expect(deepFreeze(undefined)).toBeUndefined();
60
57
  });
61
58
 
62
- test('handles circular references without infinite loop', () => {
59
+ test("handles circular references without infinite loop", () => {
63
60
  const obj: { self?: unknown; nested: { parent?: unknown } } = {
64
61
  nested: {},
65
62
  };
@@ -73,7 +70,7 @@ describe('deepFreeze', () => {
73
70
  expect(Object.isFrozen(obj.nested)).toBe(true);
74
71
  });
75
72
 
76
- test('handles already-frozen objects', () => {
73
+ test("handles already-frozen objects", () => {
77
74
  const obj = Object.freeze({ already: true });
78
75
 
79
76
  const frozen = deepFreeze(obj);
@@ -83,44 +80,44 @@ describe('deepFreeze', () => {
83
80
  });
84
81
  });
85
82
 
86
- describe('runInPluginGuard and getActivePluginGuard', () => {
87
- test('returns undefined outside guard', () => {
83
+ describe("runInPluginGuard and getActivePluginGuard", () => {
84
+ test("returns undefined outside guard", () => {
88
85
  expect(getActivePluginGuard()).toBeUndefined();
89
86
  });
90
87
 
91
- test('returns plugin context inside guard', () => {
92
- const context = runInPluginGuard('plugin-a', {}, () => getActivePluginGuard());
88
+ test("returns plugin context inside guard", () => {
89
+ const context = runInPluginGuard("plugin-a", {}, () => getActivePluginGuard());
93
90
 
94
91
  expect(context).toBeDefined();
95
- expect(context?.pluginId).toBe('plugin-a');
92
+ expect(context?.pluginId).toBe("plugin-a");
96
93
  });
97
94
 
98
- test('preserves context across async/await', async () => {
99
- const context = await runInPluginGuard('plugin-async', {}, async () => {
95
+ test("preserves context across async/await", async () => {
96
+ const context = await runInPluginGuard("plugin-async", {}, async () => {
100
97
  await Promise.resolve();
101
98
  return getActivePluginGuard();
102
99
  });
103
100
 
104
101
  expect(context).toBeDefined();
105
- expect(context?.pluginId).toBe('plugin-async');
102
+ expect(context?.pluginId).toBe("plugin-async");
106
103
  });
107
104
 
108
- test('nested guards use the innermost context', () => {
109
- const contexts = runInPluginGuard('outer-plugin', {}, () => {
105
+ test("nested guards use the innermost context", () => {
106
+ const contexts = runInPluginGuard("outer-plugin", {}, () => {
110
107
  const outerBefore = getActivePluginGuard();
111
- const inner = runInPluginGuard('inner-plugin', {}, () => getActivePluginGuard());
108
+ const inner = runInPluginGuard("inner-plugin", {}, () => getActivePluginGuard());
112
109
  const outerAfter = getActivePluginGuard();
113
110
 
114
111
  return { outerBefore, inner, outerAfter };
115
112
  });
116
113
 
117
- expect(contexts.outerBefore?.pluginId).toBe('outer-plugin');
118
- expect(contexts.inner?.pluginId).toBe('inner-plugin');
119
- expect(contexts.outerAfter?.pluginId).toBe('outer-plugin');
114
+ expect(contexts.outerBefore?.pluginId).toBe("outer-plugin");
115
+ expect(contexts.inner?.pluginId).toBe("inner-plugin");
116
+ expect(contexts.outerAfter?.pluginId).toBe("outer-plugin");
120
117
  });
121
118
  });
122
119
 
123
- describe('installGlobalFetchGuard', () => {
120
+ describe("installGlobalFetchGuard", () => {
124
121
  beforeAll(() => {
125
122
  globalThis.fetch = mockFetch;
126
123
  installGlobalFetchGuard();
@@ -130,62 +127,62 @@ describe('installGlobalFetchGuard', () => {
130
127
  globalThis.fetch = originalFetch;
131
128
  });
132
129
 
133
- test('outside guard, fetch calls pass through', async () => {
130
+ test("outside guard, fetch calls pass through", async () => {
134
131
  fetchCalls.length = 0;
135
132
 
136
- const response = await fetch('https://outside-guard.example');
133
+ const response = await fetch("https://outside-guard.example");
137
134
 
138
135
  expect(response.status).toBe(200);
139
136
  expect(fetchCalls).toHaveLength(1);
140
- expect(fetchCalls[0]).toContain('outside-guard.example');
137
+ expect(fetchCalls[0]).toContain("outside-guard.example");
141
138
  });
142
139
 
143
- test('inside guard with no network permission throws PluginPermissionError', () => {
140
+ test("inside guard with no network permission throws PluginPermissionError", () => {
144
141
  expect(() => {
145
- runInPluginGuard('plugin-no-network', {}, () => {
146
- void fetch('https://blocked.example');
142
+ runInPluginGuard("plugin-no-network", {}, () => {
143
+ void fetch("https://blocked.example");
147
144
  });
148
145
  }).toThrow(PluginPermissionError);
149
146
  });
150
147
 
151
- test('inside guard with allowedDomains blocks unlisted domains', () => {
148
+ test("inside guard with allowedDomains blocks unlisted domains", () => {
152
149
  expect(() => {
153
150
  runInPluginGuard(
154
- 'plugin-allowlist',
151
+ "plugin-allowlist",
155
152
  {
156
153
  network: {
157
154
  enabled: true,
158
- allowedDomains: ['allowed.example'],
155
+ allowedDomains: ["allowed.example"],
159
156
  },
160
157
  },
161
158
  () => {
162
- void fetch('https://not-allowed.example/path');
159
+ void fetch("https://not-allowed.example/path");
163
160
  },
164
161
  );
165
162
  }).toThrow(PluginPermissionError);
166
163
  });
167
164
 
168
- test('inside guard with allowedDomains allows listed domains and subdomains', async () => {
165
+ test("inside guard with allowedDomains allows listed domains and subdomains", async () => {
169
166
  fetchCalls.length = 0;
170
167
 
171
168
  const response = await runInPluginGuard(
172
- 'plugin-allowlist',
169
+ "plugin-allowlist",
173
170
  {
174
171
  network: {
175
172
  enabled: true,
176
- allowedDomains: ['allowed.example'],
173
+ allowedDomains: ["allowed.example"],
177
174
  },
178
175
  },
179
176
  async () => {
180
- const rootResponse = await fetch('https://allowed.example/path');
181
- await fetch('https://api.allowed.example/path');
177
+ const rootResponse = await fetch("https://allowed.example/path");
178
+ await fetch("https://api.allowed.example/path");
182
179
  return rootResponse;
183
180
  },
184
181
  );
185
182
 
186
183
  expect(response.status).toBe(200);
187
184
  expect(fetchCalls).toHaveLength(2);
188
- expect(fetchCalls[0]).toContain('allowed.example/path');
189
- expect(fetchCalls[1]).toContain('api.allowed.example/path');
185
+ expect(fetchCalls[0]).toContain("allowed.example/path");
186
+ expect(fetchCalls[1]).toContain("api.allowed.example/path");
190
187
  });
191
188
  });
@@ -11,9 +11,9 @@
11
11
  * 2. **Deep-freeze** — prevents plugins from mutating the PluginContext.
12
12
  */
13
13
 
14
- import { AsyncLocalStorage } from 'async_hooks';
15
- import { PluginPermissionError, type PluginPermissions } from './types/base.ts';
16
- import { createPluginLogger } from './sandbox.ts';
14
+ import { AsyncLocalStorage } from "async_hooks";
15
+ import { createPluginLogger } from "./sandbox.ts";
16
+ import { PluginPermissionError, type PluginPermissions } from "./types/base.ts";
17
17
 
18
18
  // ---------------------------------------------------------------------------
19
19
  // Plugin execution context (AsyncLocalStorage)
@@ -68,10 +68,7 @@ export function installGlobalFetchGuard(): void {
68
68
  const originalFetch = globalThis.fetch;
69
69
 
70
70
  const guardedFetch: typeof fetch = Object.assign(
71
- function guardedFetch(
72
- input: string | URL | Request,
73
- init?: RequestInit,
74
- ): Promise<Response> {
71
+ function guardedFetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {
75
72
  const guard = pluginGuardStorage.getStore();
76
73
 
77
74
  if (!guard) {
@@ -82,34 +79,32 @@ export function installGlobalFetchGuard(): void {
82
79
 
83
80
  if (!permissions.network?.enabled) {
84
81
  const log = createPluginLogger(pluginId);
85
- log.error('Network access blocked — plugin has not declared network permissions');
86
- throw new PluginPermissionError(
87
- pluginId,
88
- 'network',
89
- 'Network access not permitted',
90
- );
82
+ log.error("Network access blocked — plugin has not declared network permissions");
83
+ throw new PluginPermissionError(pluginId, "network", "Network access not permitted");
91
84
  }
92
85
 
93
86
  const allowedDomains = permissions.network.allowedDomains;
94
87
  if (allowedDomains && allowedDomains.length > 0) {
95
- const url = typeof input === 'string'
96
- ? new URL(input)
97
- : input instanceof URL
98
- ? input
99
- : new URL(input.url);
88
+ const url =
89
+ typeof input === "string"
90
+ ? new URL(input)
91
+ : input instanceof URL
92
+ ? input
93
+ : new URL(input.url);
100
94
 
101
95
  const isAllowed = allowedDomains.some(
102
- (domain) =>
103
- url.hostname === domain || url.hostname.endsWith(`.${domain}`),
96
+ (domain) => url.hostname === domain || url.hostname.endsWith(`.${domain}`),
104
97
  );
105
98
 
106
99
  if (!isAllowed) {
107
100
  const log = createPluginLogger(pluginId);
108
- log.error(`Network blocked: domain "${url.hostname}" not in allowlist [${allowedDomains.join(', ')}]`);
101
+ log.error(
102
+ `Network blocked: domain "${url.hostname}" not in allowlist [${allowedDomains.join(", ")}]`,
103
+ );
109
104
  throw new PluginPermissionError(
110
105
  pluginId,
111
- 'network',
112
- `Domain "${url.hostname}" not in allowlist: ${allowedDomains.join(', ')}`,
106
+ "network",
107
+ `Domain "${url.hostname}" not in allowlist: ${allowedDomains.join(", ")}`,
113
108
  );
114
109
  }
115
110
  }
@@ -136,7 +131,7 @@ const frozenObjects = new WeakSet<object>();
136
131
  */
137
132
  export function deepFreeze<T>(obj: T): T {
138
133
  if (obj === null || obj === undefined) return obj;
139
- if (typeof obj !== 'object') return obj;
134
+ if (typeof obj !== "object") return obj;
140
135
 
141
136
  const o = obj as object;
142
137
 
@@ -146,7 +141,7 @@ export function deepFreeze<T>(obj: T): T {
146
141
  Object.freeze(o);
147
142
 
148
143
  for (const value of Object.values(o)) {
149
- if (value !== null && typeof value === 'object') {
144
+ if (value !== null && typeof value === "object") {
150
145
  deepFreeze(value);
151
146
  }
152
147
  }