@rozenite/storage-plugin 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/devtools/assets/panel-BlDRXgVW.js +32 -0
  3. package/dist/devtools/assets/panel-DBweZnW5.css +1 -0
  4. package/dist/devtools/panel.html +2 -2
  5. package/dist/react-native/chunks/index.require.js +1 -3
  6. package/dist/react-native/chunks/secure-storage.require.js +2 -6
  7. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
  8. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +266 -295
  9. package/dist/react-native/index.d.ts +14 -2
  10. package/dist/react-native/index.js +2 -10
  11. package/dist/rozenite.json +1 -1
  12. package/package.json +29 -29
  13. package/react-native.ts +11 -36
  14. package/rozenite.config.ts +2 -6
  15. package/sdk.ts +1 -5
  16. package/src/react-native/__tests__/entry-preview-pagination.test.ts +12 -40
  17. package/src/react-native/__tests__/export-snapshot.test.ts +2 -6
  18. package/src/react-native/__tests__/full-entry-request.test.ts +5 -17
  19. package/src/react-native/__tests__/import.test.ts +9 -8
  20. package/src/react-native/__tests__/storage-discovery.test.ts +7 -18
  21. package/src/react-native/__tests__/stress-verification.test.ts +5 -18
  22. package/src/react-native/__tests__/use-storage-agent-tools.test.ts +25 -35
  23. package/src/react-native/adapters/__tests__/mmkv.test.ts +20 -30
  24. package/src/react-native/adapters/async-storage.ts +2 -5
  25. package/src/react-native/adapters/index.ts +2 -8
  26. package/src/react-native/adapters/mmkv.ts +7 -24
  27. package/src/react-native/adapters/secure-storage.ts +2 -6
  28. package/src/react-native/entry-preview-pagination.ts +11 -27
  29. package/src/react-native/export-snapshot.ts +3 -9
  30. package/src/react-native/full-entry-request.ts +3 -8
  31. package/src/react-native/import.ts +10 -21
  32. package/src/react-native/storage-discovery.ts +2 -6
  33. package/src/react-native/storage-view.ts +2 -6
  34. package/src/react-native/useRozeniteStoragePlugin.ts +107 -119
  35. package/src/react-native/useStorageAgentTools.ts +8 -24
  36. package/src/shared/__tests__/entry-preview.test.ts +9 -27
  37. package/src/shared/__tests__/snapshot.test.ts +10 -32
  38. package/src/shared/agent-tools.ts +9 -30
  39. package/src/shared/entry-preview.ts +3 -8
  40. package/src/shared/messaging.ts +15 -11
  41. package/src/shared/snapshot.ts +4 -17
  42. package/src/shared/types.ts +2 -4
  43. package/src/ui/__tests__/binary-value-editor-state.test.ts +6 -16
  44. package/src/ui/__tests__/binary.test.ts +13 -33
  45. package/src/ui/__tests__/large-value-viewer.test.tsx +4 -13
  46. package/src/ui/__tests__/panel.test.tsx +41 -68
  47. package/src/ui/__tests__/storage-groups.test.ts +1 -4
  48. package/src/ui/__tests__/type-conversion.test.ts +8 -24
  49. package/src/ui/__tests__/use-storage-entries.test.tsx +18 -34
  50. package/src/ui/add-entry-dialog.tsx +4 -15
  51. package/src/ui/binary-value-editor-state.ts +6 -24
  52. package/src/ui/binary-value-editor.tsx +5 -17
  53. package/src/ui/binary.ts +6 -19
  54. package/src/ui/edit-entry-dialog.tsx +3 -11
  55. package/src/ui/editor-switcher.tsx +2 -11
  56. package/src/ui/entry-detail-dialog.tsx +5 -14
  57. package/src/ui/format-value.tsx +2 -9
  58. package/src/ui/import-dialog.tsx +7 -19
  59. package/src/ui/large-value-viewer-state.ts +2 -9
  60. package/src/ui/large-value-viewer.tsx +3 -9
  61. package/src/ui/panel.tsx +65 -153
  62. package/src/ui/query-client.tsx +4 -15
  63. package/src/ui/type-conversion.ts +2 -5
  64. package/src/ui/typed-value-editor.tsx +1 -5
  65. package/src/ui/use-storage-entries.ts +6 -21
  66. package/src/ui/utils.ts +1 -4
  67. package/dist/devtools/assets/panel-B6Wp0eie.css +0 -1
  68. package/dist/devtools/assets/panel-D7MFc4HG.js +0 -32
@@ -26,9 +26,7 @@ export type RozeniteStoragePluginOptions = {
26
26
  storages: StorageAdapter[];
27
27
  };
28
28
 
29
- export const useRozeniteStoragePlugin = ({
30
- storages,
31
- }: RozeniteStoragePluginOptions) => {
29
+ export const useRozeniteStoragePlugin = ({ storages }: RozeniteStoragePluginOptions) => {
32
30
  const views = useMemo(() => createStorageViews(storages), [storages]);
33
31
 
34
32
  useStorageAgentTools(views);
@@ -45,16 +43,23 @@ export const useRozeniteStoragePlugin = ({
45
43
  const viewSubscriptions: { remove: () => void }[] = [];
46
44
  let disposed = false;
47
45
 
48
- const sendInvalidation = (
46
+ const sendInvalidation = async (
49
47
  target: StorageSetEntryEvent['target'],
50
48
  key?: string,
51
49
  operation?: StorageInvalidationOperation,
52
50
  ) => {
51
+ const view = views.find(
52
+ (candidate) =>
53
+ candidate.target.adapterId === target.adapterId &&
54
+ candidate.target.storageId === target.storageId,
55
+ );
56
+ const entryCount = view ? (await view.getAllKeys()).length : 0;
53
57
  client.send('storage-invalidated', {
54
58
  type: 'storage-invalidated',
55
59
  target,
56
60
  key,
57
61
  operation,
62
+ entryCount,
58
63
  });
59
64
  };
60
65
 
@@ -70,7 +75,7 @@ export const useRozeniteStoragePlugin = ({
70
75
  }
71
76
 
72
77
  const subscription = await watch((key) => {
73
- sendInvalidation(view.target, key);
78
+ void sendInvalidation(view.target, key);
74
79
  });
75
80
 
76
81
  if (disposed) {
@@ -89,94 +94,82 @@ export const useRozeniteStoragePlugin = ({
89
94
  );
90
95
 
91
96
  const messageSubscriptions = [
92
- client.onMessage(
93
- 'set-entry',
94
- async ({ target, entry }: StorageSetEntryEvent) => {
95
- const view = views.find(
96
- (candidate) =>
97
- candidate.target.adapterId === target.adapterId &&
98
- candidate.target.storageId === target.storageId,
97
+ client.onMessage('set-entry', async ({ target, entry }: StorageSetEntryEvent) => {
98
+ const view = views.find(
99
+ (candidate) =>
100
+ candidate.target.adapterId === target.adapterId &&
101
+ candidate.target.storageId === target.storageId,
102
+ );
103
+
104
+ if (!view) {
105
+ console.warn(
106
+ `[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}`,
99
107
  );
100
-
101
- if (!view) {
102
- console.warn(
103
- `[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}`,
104
- );
105
- return;
106
- }
107
-
108
- try {
109
- await view.set(entry);
110
- sendInvalidation(view.target, entry.key, 'set');
111
- } catch (error) {
112
- console.warn(
113
- `[Rozenite] Storage Plugin: Failed to set entry in ${target.adapterId}/${target.storageId}.`,
114
- error,
115
- );
116
- }
117
- },
118
- ),
119
- client.onMessage(
120
- 'delete-entry',
121
- async ({ target, key }: StorageDeleteEntryEvent) => {
122
- const view = views.find(
123
- (candidate) =>
124
- candidate.target.adapterId === target.adapterId &&
125
- candidate.target.storageId === target.storageId,
108
+ return;
109
+ }
110
+
111
+ try {
112
+ await view.set(entry);
113
+ await sendInvalidation(view.target, entry.key, 'set');
114
+ } catch (error) {
115
+ console.warn(
116
+ `[Rozenite] Storage Plugin: Failed to set entry in ${target.adapterId}/${target.storageId}.`,
117
+ error,
126
118
  );
127
-
128
- if (!view) {
129
- console.warn(
130
- `[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}`,
131
- );
132
- return;
133
- }
134
-
135
- try {
136
- await view.delete(key);
137
- sendInvalidation(view.target, key, 'delete');
138
- } catch (error) {
139
- console.warn(
140
- `[Rozenite] Storage Plugin: Failed to delete entry in ${target.adapterId}/${target.storageId}.`,
141
- error,
142
- );
143
- }
144
- },
145
- ),
146
- client.onMessage(
147
- 'purge-storage',
148
- async ({ target }: StoragePurgeEvent) => {
149
- const view = views.find(
150
- (candidate) =>
151
- candidate.target.adapterId === target.adapterId &&
152
- candidate.target.storageId === target.storageId,
119
+ }
120
+ }),
121
+ client.onMessage('delete-entry', async ({ target, key }: StorageDeleteEntryEvent) => {
122
+ const view = views.find(
123
+ (candidate) =>
124
+ candidate.target.adapterId === target.adapterId &&
125
+ candidate.target.storageId === target.storageId,
126
+ );
127
+
128
+ if (!view) {
129
+ console.warn(
130
+ `[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}`,
153
131
  );
154
-
155
- if (!view) {
156
- console.warn(
157
- `[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}`,
158
- );
159
- return;
160
- }
161
-
162
- try {
163
- await view.purge();
164
- sendInvalidation(view.target, undefined, 'purge');
165
- } catch (error) {
166
- console.warn(
167
- `[Rozenite] Storage Plugin: Failed to purge storage in ${target.adapterId}/${target.storageId}.`,
168
- error,
169
- );
170
- }
171
- },
172
- ),
173
- client.onMessage(
174
- 'discover-storages',
175
- async (event: StorageDiscoverStoragesRequestEvent) => {
176
- const response = await handleStorageDiscoveryRequest(views, event);
177
- client.send(response.type, response);
178
- },
179
- ),
132
+ return;
133
+ }
134
+
135
+ try {
136
+ await view.delete(key);
137
+ await sendInvalidation(view.target, key, 'delete');
138
+ } catch (error) {
139
+ console.warn(
140
+ `[Rozenite] Storage Plugin: Failed to delete entry in ${target.adapterId}/${target.storageId}.`,
141
+ error,
142
+ );
143
+ }
144
+ }),
145
+ client.onMessage('purge-storage', async ({ target }: StoragePurgeEvent) => {
146
+ const view = views.find(
147
+ (candidate) =>
148
+ candidate.target.adapterId === target.adapterId &&
149
+ candidate.target.storageId === target.storageId,
150
+ );
151
+
152
+ if (!view) {
153
+ console.warn(
154
+ `[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}`,
155
+ );
156
+ return;
157
+ }
158
+
159
+ try {
160
+ await view.purge();
161
+ await sendInvalidation(view.target, undefined, 'purge');
162
+ } catch (error) {
163
+ console.warn(
164
+ `[Rozenite] Storage Plugin: Failed to purge storage in ${target.adapterId}/${target.storageId}.`,
165
+ error,
166
+ );
167
+ }
168
+ }),
169
+ client.onMessage('discover-storages', async (event: StorageDiscoverStoragesRequestEvent) => {
170
+ const response = await handleStorageDiscoveryRequest(views, event);
171
+ client.send(response.type, response);
172
+ }),
180
173
  client.onMessage(
181
174
  'list-entry-previews',
182
175
  async (event: StorageListEntryPreviewsRequestEvent) => {
@@ -184,37 +177,32 @@ export const useRozeniteStoragePlugin = ({
184
177
  client.send(response.type, response);
185
178
  },
186
179
  ),
187
- client.onMessage(
188
- 'get-entry',
189
- async (event: StorageGetEntryRequestEvent) => {
190
- const response = await handleFullEntryRequest(views, event);
191
- client.send(response.type, response);
192
- },
193
- ),
194
- client.onMessage(
195
- 'export-snapshot',
196
- async (event: StorageExportSnapshotRequestEvent) => {
197
- const response = await handleExportSnapshotRequest(views, event);
198
- client.send(response.type, response);
199
- },
200
- ),
201
- client.onMessage(
202
- 'preview-import',
203
- async (event: StorageImportPreviewRequestEvent) => {
204
- const response = await handleImportPreviewRequest(views, event);
205
- client.send(response.type, response);
206
- },
207
- ),
208
- client.onMessage(
209
- 'import-entries',
210
- async (event: StorageImportEntriesEvent) => {
211
- await handleImportEntries(views, event, (out) => {
212
- client.send(out.type, out);
213
- });
214
- },
215
- ),
180
+ client.onMessage('get-entry', async (event: StorageGetEntryRequestEvent) => {
181
+ const response = await handleFullEntryRequest(views, event);
182
+ client.send(response.type, response);
183
+ }),
184
+ client.onMessage('export-snapshot', async (event: StorageExportSnapshotRequestEvent) => {
185
+ const response = await handleExportSnapshotRequest(views, event);
186
+ client.send(response.type, response);
187
+ }),
188
+ client.onMessage('preview-import', async (event: StorageImportPreviewRequestEvent) => {
189
+ const response = await handleImportPreviewRequest(views, event);
190
+ client.send(response.type, response);
191
+ }),
192
+ client.onMessage('import-entries', async (event: StorageImportEntriesEvent) => {
193
+ await handleImportEntries(views, event, (out) => {
194
+ client.send(out.type, out);
195
+ });
196
+ }),
216
197
  ];
217
198
 
199
+ // Sent last, so a panel that reacts to it is guaranteed to find the
200
+ // handlers above already listening. The panel is recreated on every app
201
+ // reload and asks for storages as soon as it boots, which usually beats the
202
+ // app's React tree to the punch; without this the panel's only request is
203
+ // dropped and it waits for storages forever.
204
+ client.send('device-ready', { type: 'device-ready' });
205
+
218
206
  return () => {
219
207
  disposed = true;
220
208
  viewSubscriptions.forEach((subscription) => subscription.remove());
@@ -13,10 +13,7 @@ import type {
13
13
  } from '../shared/types';
14
14
  import type { StorageView } from './storage-view';
15
15
 
16
- const parseValueForType = (
17
- type: StorageEntryType,
18
- value: unknown,
19
- ): StorageEntryValue => {
16
+ const parseValueForType = (type: StorageEntryType, value: unknown): StorageEntryValue => {
20
17
  if (type === 'string') {
21
18
  if (typeof value !== 'string') {
22
19
  throw new Error('Expected string value for type=string');
@@ -45,8 +42,7 @@ const parseValueForType = (
45
42
  return value as number[];
46
43
  };
47
44
 
48
- const formatViewRef = (view: StorageView) =>
49
- `${view.target.adapterId}/${view.target.storageId}`;
45
+ const formatViewRef = (view: StorageView) => `${view.target.adapterId}/${view.target.storageId}`;
50
46
 
51
47
  const isSameTarget = (left: StorageTarget, right: StorageTarget) =>
52
48
  left.adapterId === right.adapterId && left.storageId === right.storageId;
@@ -68,10 +64,8 @@ const decodeCursor = (value: string): ListEntriesCursor => {
68
64
  cursor == null ||
69
65
  Array.isArray(cursor) ||
70
66
  (cursor as { version?: unknown }).version !== 1 ||
71
- typeof (cursor as { target?: { adapterId?: unknown } }).target
72
- ?.adapterId !== 'string' ||
73
- typeof (cursor as { target?: { storageId?: unknown } }).target
74
- ?.storageId !== 'string' ||
67
+ typeof (cursor as { target?: { adapterId?: unknown } }).target?.adapterId !== 'string' ||
68
+ typeof (cursor as { target?: { storageId?: unknown } }).target?.storageId !== 'string' ||
75
69
  !Number.isSafeInteger((cursor as { offset?: unknown }).offset) ||
76
70
  (cursor as { offset: number }).offset < 0
77
71
  ) {
@@ -112,11 +106,7 @@ const sanitizeOffset = (offset: number | undefined) => {
112
106
  return offset;
113
107
  };
114
108
 
115
- const resolveStorage = (
116
- views: readonly StorageView[],
117
- adapterId?: string,
118
- storageId?: string,
119
- ) => {
109
+ const resolveStorage = (views: readonly StorageView[], adapterId?: string, storageId?: string) => {
120
110
  if (views.length === 0) {
121
111
  throw new Error('No storages are registered.');
122
112
  }
@@ -158,16 +148,12 @@ const resolveStorage = (
158
148
  return views[0]!;
159
149
  };
160
150
 
161
- const listStorageKeys = async (
162
- view: StorageView,
163
- input: StorageListEntriesArgs,
164
- ) => {
151
+ const listStorageKeys = async (view: StorageView, input: StorageListEntriesArgs) => {
165
152
  const limit = sanitizeLimit(input.limit);
166
153
  if (input.cursor !== undefined && input.offset !== undefined) {
167
154
  throw new Error('Provide either cursor or offset, not both.');
168
155
  }
169
- const cursor =
170
- input.cursor === undefined ? undefined : decodeCursor(input.cursor);
156
+ const cursor = input.cursor === undefined ? undefined : decodeCursor(input.cursor);
171
157
  if (cursor && !isSameTarget(cursor.target, view.target)) {
172
158
  throw new Error(
173
159
  'Cursor does not match the requested storage. Run list-entries again without a cursor to restart pagination.',
@@ -228,9 +214,7 @@ export const createStorageAgentHandlers = (views: readonly StorageView[]) => ({
228
214
  const entry = await view.get(key);
229
215
 
230
216
  if (!entry) {
231
- throw new Error(
232
- `Key "${key}" not found in storage "${formatViewRef(view)}".`,
233
- );
217
+ throw new Error(`Key "${key}" not found in storage "${formatViewRef(view)}".`);
234
218
  }
235
219
 
236
220
  return {
@@ -7,9 +7,7 @@ import {
7
7
 
8
8
  describe('toStorageEntryPreview', () => {
9
9
  it('represents empty and boundary-sized strings completely', () => {
10
- expect(
11
- toStorageEntryPreview({ key: 'empty', type: 'string', value: '' }),
12
- ).toEqual({
10
+ expect(toStorageEntryPreview({ key: 'empty', type: 'string', value: '' })).toEqual({
13
11
  key: 'empty',
14
12
  type: 'string',
15
13
  preview: '',
@@ -18,9 +16,7 @@ describe('toStorageEntryPreview', () => {
18
16
  });
19
17
 
20
18
  const value = 'a'.repeat(MAX_STRING_PREVIEW_CODE_POINTS);
21
- expect(
22
- toStorageEntryPreview({ key: 'boundary', type: 'string', value }),
23
- ).toEqual({
19
+ expect(toStorageEntryPreview({ key: 'boundary', type: 'string', value })).toEqual({
24
20
  key: 'boundary',
25
21
  type: 'string',
26
22
  preview: value,
@@ -32,9 +28,7 @@ describe('toStorageEntryPreview', () => {
32
28
  it('truncates an oversized string at the fixed code-point boundary', () => {
33
29
  const value = `${'a'.repeat(MAX_STRING_PREVIEW_CODE_POINTS)}z`;
34
30
 
35
- expect(
36
- toStorageEntryPreview({ key: 'long', type: 'string', value }),
37
- ).toEqual({
31
+ expect(toStorageEntryPreview({ key: 'long', type: 'string', value })).toEqual({
38
32
  key: 'long',
39
33
  type: 'string',
40
34
  preview: 'a'.repeat(MAX_STRING_PREVIEW_CODE_POINTS),
@@ -46,9 +40,7 @@ describe('toStorageEntryPreview', () => {
46
40
  it('does not split Unicode code points while preserving string length', () => {
47
41
  const value = `${'😀'.repeat(MAX_STRING_PREVIEW_CODE_POINTS)}!`;
48
42
 
49
- expect(
50
- toStorageEntryPreview({ key: 'emoji', type: 'string', value }),
51
- ).toEqual({
43
+ expect(toStorageEntryPreview({ key: 'emoji', type: 'string', value })).toEqual({
52
44
  key: 'emoji',
53
45
  type: 'string',
54
46
  preview: '😀'.repeat(MAX_STRING_PREVIEW_CODE_POINTS),
@@ -58,18 +50,14 @@ describe('toStorageEntryPreview', () => {
58
50
  });
59
51
 
60
52
  it('formats primitive values completely', () => {
61
- expect(
62
- toStorageEntryPreview({ key: 'count', type: 'number', value: 42 }),
63
- ).toEqual({
53
+ expect(toStorageEntryPreview({ key: 'count', type: 'number', value: 42 })).toEqual({
64
54
  key: 'count',
65
55
  type: 'number',
66
56
  preview: '42',
67
57
  valueSize: 2,
68
58
  isTruncated: false,
69
59
  });
70
- expect(
71
- toStorageEntryPreview({ key: 'enabled', type: 'boolean', value: false }),
72
- ).toEqual({
60
+ expect(toStorageEntryPreview({ key: 'enabled', type: 'boolean', value: false })).toEqual({
73
61
  key: 'enabled',
74
62
  type: 'boolean',
75
63
  preview: 'false',
@@ -79,9 +67,7 @@ describe('toStorageEntryPreview', () => {
79
67
  });
80
68
 
81
69
  it('formats empty and boundary-sized buffers as hexadecimal', () => {
82
- expect(
83
- toStorageEntryPreview({ key: 'empty', type: 'buffer', value: [] }),
84
- ).toEqual({
70
+ expect(toStorageEntryPreview({ key: 'empty', type: 'buffer', value: [] })).toEqual({
85
71
  key: 'empty',
86
72
  type: 'buffer',
87
73
  preview: '',
@@ -90,9 +76,7 @@ describe('toStorageEntryPreview', () => {
90
76
  });
91
77
 
92
78
  const value = new Array(MAX_BUFFER_PREVIEW_BYTES).fill(0xab);
93
- expect(
94
- toStorageEntryPreview({ key: 'boundary', type: 'buffer', value }),
95
- ).toEqual({
79
+ expect(toStorageEntryPreview({ key: 'boundary', type: 'buffer', value })).toEqual({
96
80
  key: 'boundary',
97
81
  type: 'buffer',
98
82
  preview: new Array(MAX_BUFFER_PREVIEW_BYTES).fill('AB').join(' '),
@@ -104,9 +88,7 @@ describe('toStorageEntryPreview', () => {
104
88
  it('truncates oversized buffers at the fixed byte boundary', () => {
105
89
  const value = [...new Array(MAX_BUFFER_PREVIEW_BYTES).fill(0xab), 0xcd];
106
90
 
107
- expect(
108
- toStorageEntryPreview({ key: 'long', type: 'buffer', value }),
109
- ).toEqual({
91
+ expect(toStorageEntryPreview({ key: 'long', type: 'buffer', value })).toEqual({
110
92
  key: 'long',
111
93
  type: 'buffer',
112
94
  preview: new Array(MAX_BUFFER_PREVIEW_BYTES).fill('AB').join(' '),
@@ -1,19 +1,8 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import {
3
- buildSnapshot,
4
- computePreview,
5
- parseSnapshot,
6
- type StorageSnapshotV1,
7
- } from '../snapshot';
8
- import type {
9
- StorageCapabilities,
10
- StorageEntry,
11
- StorageTarget,
12
- } from '../types';
13
-
14
- const validSnapshot = (
15
- overrides: Partial<StorageSnapshotV1> = {},
16
- ): StorageSnapshotV1 => ({
2
+ import { buildSnapshot, computePreview, parseSnapshot, type StorageSnapshotV1 } from '../snapshot';
3
+ import type { StorageCapabilities, StorageEntry, StorageTarget } from '../types';
4
+
5
+ const validSnapshot = (overrides: Partial<StorageSnapshotV1> = {}): StorageSnapshotV1 => ({
17
6
  version: 1,
18
7
  plugin: '@rozenite/storage-plugin',
19
8
  createdAt: '2026-05-11T12:00:00.000Z',
@@ -125,16 +114,13 @@ describe('parseSnapshot', () => {
125
114
  const result = parseSnapshot(input);
126
115
  expect(result.ok).toBe(false);
127
116
  if (!result.ok) {
128
- expect(result.error.path).toBe(
129
- 'storage.capabilities.supportedTypes[1]',
130
- );
117
+ expect(result.error.path).toBe('storage.capabilities.supportedTypes[1]');
131
118
  }
132
119
  });
133
120
  });
134
121
 
135
122
  describe('per-entry rejections', () => {
136
- const withEntry = (entry: unknown) =>
137
- parseSnapshot({ ...validSnapshot(), entries: [entry] });
123
+ const withEntry = (entry: unknown) => parseSnapshot({ ...validSnapshot(), entries: [entry] });
138
124
 
139
125
  it('rejects entry missing key', () => {
140
126
  const result = withEntry({ type: 'string', value: 'x' });
@@ -217,9 +203,7 @@ describe('buildSnapshot', () => {
217
203
  const capabilities: StorageCapabilities = {
218
204
  supportedTypes: ['string', 'number'],
219
205
  };
220
- const entries: StorageEntry[] = [
221
- { key: 'token', type: 'string', value: 'abc' },
222
- ];
206
+ const entries: StorageEntry[] = [{ key: 'token', type: 'string', value: 'abc' }];
223
207
 
224
208
  const result = buildSnapshot({
225
209
  target,
@@ -280,9 +264,7 @@ describe('computePreview', () => {
280
264
  entryKeys: new Set(),
281
265
  isBlacklisted: (key) => key.startsWith('__'),
282
266
  });
283
- expect(preview.skippedKeys).toEqual([
284
- { key: '__internal', reason: 'blacklist' },
285
- ]);
267
+ expect(preview.skippedKeys).toEqual([{ key: '__internal', reason: 'blacklist' }]);
286
268
  expect(preview.newKeys).toEqual(['visible']);
287
269
  });
288
270
 
@@ -320,12 +302,8 @@ describe('computePreview', () => {
320
302
  entryKeys: new Set(['existing']),
321
303
  isBlacklisted: (key) => key.startsWith('__'),
322
304
  });
323
- expect(preview.unsupportedTypes).toEqual([
324
- { key: '__blocked', type: 'buffer' },
325
- ]);
326
- expect(preview.skippedKeys).toEqual([
327
- { key: '__skipped', reason: 'blacklist' },
328
- ]);
305
+ expect(preview.unsupportedTypes).toEqual([{ key: '__blocked', type: 'buffer' }]);
306
+ expect(preview.skippedKeys).toEqual([{ key: '__skipped', reason: 'blacklist' }]);
329
307
  expect(preview.overwriteKeys).toEqual(['existing']);
330
308
  expect(preview.newKeys).toEqual(['fresh']);
331
309
  });
@@ -11,8 +11,7 @@ type StorageSelection = Partial<StorageTarget>;
11
11
  const sharedStorageProperties = {
12
12
  adapterId: {
13
13
  type: 'string',
14
- description:
15
- 'Storage adapter ID. Required when multiple adapters are configured.',
14
+ description: 'Storage adapter ID. Required when multiple adapters are configured.',
16
15
  },
17
16
  storageId: {
18
17
  type: 'string',
@@ -105,19 +104,13 @@ export type StorageRemoveEntryResult = {
105
104
  };
106
105
 
107
106
  export const storageToolDefinitions = {
108
- listStorages: defineAgentToolContract<
109
- StorageListStoragesArgs,
110
- StorageListStoragesResult
111
- >({
107
+ listStorages: defineAgentToolContract<StorageListStoragesArgs, StorageListStoragesResult>({
112
108
  name: 'list-storages',
113
109
  description:
114
110
  'List all storage adapters and their storage nodes currently available on the device, including supported entry types. Entry counts are omitted to avoid enumerating every storage.',
115
111
  inputSchema: { type: 'object', properties: {} },
116
112
  }),
117
- listEntries: definePaginatedAgentToolContract<
118
- StorageListEntriesArgs,
119
- StorageListEntriesResult
120
- >({
113
+ listEntries: definePaginatedAgentToolContract<StorageListEntriesArgs, StorageListEntriesResult>({
121
114
  name: 'list-entries',
122
115
  description:
123
116
  'List keys in a storage. This call intentionally does not return entry values to keep responses token-efficient.',
@@ -131,8 +124,7 @@ export const storageToolDefinitions = {
131
124
  },
132
125
  cursor: {
133
126
  type: 'string',
134
- description:
135
- 'Opaque pagination cursor from a previous list-entries call.',
127
+ description: 'Opaque pagination cursor from a previous list-entries call.',
136
128
  },
137
129
  offset: {
138
130
  type: 'number',
@@ -147,10 +139,7 @@ export const storageToolDefinitions = {
147
139
  defaultFields: ['key'],
148
140
  },
149
141
  }),
150
- readEntry: defineAgentToolContract<
151
- StorageReadEntryArgs,
152
- StorageReadEntryResult
153
- >({
142
+ readEntry: defineAgentToolContract<StorageReadEntryArgs, StorageReadEntryResult>({
154
143
  name: 'read-entry',
155
144
  description: 'Read a single storage entry value by key.',
156
145
  inputSchema: {
@@ -162,10 +151,7 @@ export const storageToolDefinitions = {
162
151
  required: ['key'],
163
152
  },
164
153
  }),
165
- createEntry: defineAgentToolContract<
166
- StorageCreateEntryArgs,
167
- StorageCreateEntryResult
168
- >({
154
+ createEntry: defineAgentToolContract<StorageCreateEntryArgs, StorageCreateEntryResult>({
169
155
  name: 'create-entry',
170
156
  description: 'Create a new storage entry. Fails if the key already exists.',
171
157
  inputSchema: {
@@ -186,13 +172,9 @@ export const storageToolDefinitions = {
186
172
  required: ['key', 'type', 'value'],
187
173
  },
188
174
  }),
189
- editEntry: defineAgentToolContract<
190
- StorageEditEntryArgs,
191
- StorageEditEntryResult
192
- >({
175
+ editEntry: defineAgentToolContract<StorageEditEntryArgs, StorageEditEntryResult>({
193
176
  name: 'edit-entry',
194
- description:
195
- 'Edit an existing storage entry. Fails if the key does not exist.',
177
+ description: 'Edit an existing storage entry. Fails if the key does not exist.',
196
178
  inputSchema: {
197
179
  type: 'object',
198
180
  properties: {
@@ -211,10 +193,7 @@ export const storageToolDefinitions = {
211
193
  required: ['key', 'type', 'value'],
212
194
  },
213
195
  }),
214
- removeEntry: defineAgentToolContract<
215
- StorageRemoveEntryArgs,
216
- StorageRemoveEntryResult
217
- >({
196
+ removeEntry: defineAgentToolContract<StorageRemoveEntryArgs, StorageRemoveEntryResult>({
218
197
  name: 'remove-entry',
219
198
  description: 'Remove a storage entry by key.',
220
199
  inputSchema: {
@@ -3,12 +3,9 @@ import type { StorageEntry, StorageEntryPreview } from './types';
3
3
  export const MAX_STRING_PREVIEW_CODE_POINTS = 256;
4
4
  export const MAX_BUFFER_PREVIEW_BYTES = 16;
5
5
 
6
- const toHexPair = (byte: number): string =>
7
- byte.toString(16).toUpperCase().padStart(2, '0');
6
+ const toHexPair = (byte: number): string => byte.toString(16).toUpperCase().padStart(2, '0');
8
7
 
9
- const truncateString = (
10
- value: string,
11
- ): Pick<StorageEntryPreview, 'preview' | 'isTruncated'> => {
8
+ const truncateString = (value: string): Pick<StorageEntryPreview, 'preview' | 'isTruncated'> => {
12
9
  let preview = '';
13
10
  let codePointCount = 0;
14
11
 
@@ -24,9 +21,7 @@ const truncateString = (
24
21
  return { preview, isTruncated: false };
25
22
  };
26
23
 
27
- export const toStorageEntryPreview = (
28
- entry: StorageEntry,
29
- ): StorageEntryPreview => {
24
+ export const toStorageEntryPreview = (entry: StorageEntry): StorageEntryPreview => {
30
25
  if (entry.type === 'string') {
31
26
  return {
32
27
  key: entry.key,