n2n-nexus 0.4.2 → 0.7.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.
- package/CHANGELOG.md +114 -0
- package/README.md +77 -117
- package/assets/n2n-nexus-banner.jpg +0 -0
- package/assets/n2n-nexus-icon.png +0 -0
- package/assets/n2n-nexus-logo.png +0 -0
- package/build/client/nexus-client.js +44 -21
- package/build/config/cli.js +0 -3
- package/build/config/index.js +0 -10
- package/build/constants.js +7 -11
- package/build/daemon/index.js +35 -19
- package/build/daemon/rate-limit.js +62 -0
- package/build/daemon/server.js +422 -545
- package/build/daemon/tool-outputs.js +177 -0
- package/build/daemon/tools.js +321 -0
- package/build/index.js +1 -1
- package/build/server/instance-id.js +19 -0
- package/build/server/nexus.js +243 -47
- package/build/storage/direct-messages.js +89 -0
- package/build/storage/docs.js +14 -7
- package/build/storage/events.js +31 -0
- package/build/storage/fs-utils.js +42 -0
- package/build/storage/global-messages.js +44 -0
- package/build/storage/index.js +47 -41
- package/build/storage/migrations.js +86 -0
- package/build/storage/paths.js +0 -3
- package/build/storage/projects.js +4 -5
- package/build/storage/registry.js +67 -25
- package/build/storage/sessions.js +77 -0
- package/build/storage/sqlite-meeting.js +35 -23
- package/build/storage/sqlite.js +73 -24
- package/build/storage/tasks.js +29 -103
- package/build/types.js +1 -1
- package/docs/ARCHITECTURE.md +36 -25
- package/docs/ARCHITECTURE_zh.md +37 -26
- package/docs/ASSISTANT_GUIDE.md +77 -65
- package/docs/README_zh.md +34 -19
- package/llms.txt +12 -2
- package/package.json +24 -14
- package/build/storage/logs.js +0 -60
- package/build/storage/meetings.js +0 -276
- package/build/storage/store.js +0 -153
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { NexusPaths } from "./paths.js";
|
|
4
|
-
import { AsyncMutex } from "../utils/async-mutex.js";
|
|
5
|
-
import { FILE_ENCODING } from "../constants.js";
|
|
6
|
-
/**
|
|
7
|
-
* MeetingStore - Handles all meeting-related storage operations
|
|
8
|
-
*/
|
|
9
|
-
export class MeetingStore {
|
|
10
|
-
static meetingLock = new AsyncMutex();
|
|
11
|
-
static stateLock = new AsyncMutex();
|
|
12
|
-
// --- Path Definitions ---
|
|
13
|
-
static get meetingsDir() { return path.join(NexusPaths.root, "meetings"); }
|
|
14
|
-
static get stateFile() { return path.join(NexusPaths.root, "global", "meeting_state.json"); }
|
|
15
|
-
/**
|
|
16
|
-
* Initialize meeting storage directories
|
|
17
|
-
*/
|
|
18
|
-
static async init() {
|
|
19
|
-
await fs.mkdir(this.meetingsDir, { recursive: true });
|
|
20
|
-
await this.loadStateSafe();
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Check if a path exists
|
|
24
|
-
*/
|
|
25
|
-
static async exists(p) {
|
|
26
|
-
try {
|
|
27
|
-
await fs.access(p);
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Load meeting state with self-healing
|
|
36
|
-
*/
|
|
37
|
-
static async loadStateSafe() {
|
|
38
|
-
const defaultState = { activeMeetings: [], defaultMeetingId: null };
|
|
39
|
-
try {
|
|
40
|
-
if (!await this.exists(this.stateFile)) {
|
|
41
|
-
await fs.writeFile(this.stateFile, JSON.stringify(defaultState, null, 2), FILE_ENCODING);
|
|
42
|
-
return defaultState;
|
|
43
|
-
}
|
|
44
|
-
const content = await fs.readFile(this.stateFile, FILE_ENCODING);
|
|
45
|
-
const cleanContent = content.replace(/^\uFEFF/, '').trim();
|
|
46
|
-
if (!cleanContent)
|
|
47
|
-
throw new Error("Empty file");
|
|
48
|
-
return JSON.parse(cleanContent);
|
|
49
|
-
}
|
|
50
|
-
catch (e) {
|
|
51
|
-
console.warn(`[MeetingStore] Repairing corrupted state file. Error: ${e.message}`);
|
|
52
|
-
await fs.writeFile(this.stateFile, JSON.stringify(defaultState, null, 2), FILE_ENCODING);
|
|
53
|
-
return defaultState;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Get current meeting state
|
|
58
|
-
*/
|
|
59
|
-
static async getState() {
|
|
60
|
-
return this.loadStateSafe();
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Save meeting state
|
|
64
|
-
*/
|
|
65
|
-
static async saveState(state) {
|
|
66
|
-
await fs.writeFile(this.stateFile, JSON.stringify(state, null, 2), FILE_ENCODING);
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Generate a unique meeting ID
|
|
70
|
-
*/
|
|
71
|
-
static generateMeetingId(topic) {
|
|
72
|
-
const now = new Date();
|
|
73
|
-
const timestamp = now.toISOString().replace(/[-:T]/g, '').substring(0, 14);
|
|
74
|
-
// Create slug from topic, fallback to base64 hash for non-ASCII
|
|
75
|
-
let slug = topic
|
|
76
|
-
.toLowerCase()
|
|
77
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
78
|
-
.replace(/^-|-$/g, '')
|
|
79
|
-
.substring(0, 30);
|
|
80
|
-
// If slug is empty (e.g., Chinese topic), use base64 of topic
|
|
81
|
-
if (!slug) {
|
|
82
|
-
slug = Buffer.from(topic).toString('base64').replace(/[^a-zA-Z0-9]/g, '').substring(0, 8).toLowerCase();
|
|
83
|
-
}
|
|
84
|
-
// Add random suffix for uniqueness (prevents collision in same second)
|
|
85
|
-
const suffix = Math.random().toString(36).substring(2, 6);
|
|
86
|
-
return `${timestamp}-${slug || 'meeting'}-${suffix}`;
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Get the file path for a meeting
|
|
90
|
-
*/
|
|
91
|
-
static getMeetingPath(id) {
|
|
92
|
-
return path.join(this.meetingsDir, `${id}.json`);
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Start a new meeting
|
|
96
|
-
*/
|
|
97
|
-
static async startMeeting(topic, initiator) {
|
|
98
|
-
await this.init();
|
|
99
|
-
return this.stateLock.withLock(async () => {
|
|
100
|
-
const id = this.generateMeetingId(topic);
|
|
101
|
-
const meeting = {
|
|
102
|
-
id,
|
|
103
|
-
topic,
|
|
104
|
-
status: "active",
|
|
105
|
-
startTime: new Date().toISOString(),
|
|
106
|
-
initiator,
|
|
107
|
-
participants: [initiator],
|
|
108
|
-
messages: [],
|
|
109
|
-
decisions: []
|
|
110
|
-
};
|
|
111
|
-
// Save meeting file
|
|
112
|
-
await fs.writeFile(this.getMeetingPath(id), JSON.stringify(meeting, null, 2), FILE_ENCODING);
|
|
113
|
-
// Update state
|
|
114
|
-
const state = await this.loadStateSafe();
|
|
115
|
-
state.activeMeetings.push(id);
|
|
116
|
-
state.defaultMeetingId = id;
|
|
117
|
-
await this.saveState(state);
|
|
118
|
-
return meeting;
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Get a meeting by ID
|
|
123
|
-
*/
|
|
124
|
-
static async getMeeting(id) {
|
|
125
|
-
const meetingPath = this.getMeetingPath(id);
|
|
126
|
-
if (!await this.exists(meetingPath))
|
|
127
|
-
return null;
|
|
128
|
-
const content = await fs.readFile(meetingPath, FILE_ENCODING);
|
|
129
|
-
return JSON.parse(content);
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Add a message to a meeting
|
|
133
|
-
*/
|
|
134
|
-
static async addMessage(meetingId, message) {
|
|
135
|
-
await this.meetingLock.withLock(async () => {
|
|
136
|
-
const meeting = await this.getMeeting(meetingId);
|
|
137
|
-
if (!meeting)
|
|
138
|
-
throw new Error(`Meeting '${meetingId}' not found.`);
|
|
139
|
-
if (meeting.status !== "active")
|
|
140
|
-
throw new Error(`Meeting '${meetingId}' is ${meeting.status}, cannot add messages.`);
|
|
141
|
-
// Add message
|
|
142
|
-
meeting.messages.push(message);
|
|
143
|
-
// Track participant
|
|
144
|
-
if (!meeting.participants.includes(message.from)) {
|
|
145
|
-
meeting.participants.push(message.from);
|
|
146
|
-
}
|
|
147
|
-
// Extract decisions
|
|
148
|
-
if (message.category === "DECISION") {
|
|
149
|
-
meeting.decisions.push(message.text);
|
|
150
|
-
}
|
|
151
|
-
await fs.writeFile(this.getMeetingPath(meetingId), JSON.stringify(meeting, null, 2), FILE_ENCODING);
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* End a meeting (close it)
|
|
156
|
-
*/
|
|
157
|
-
static async endMeeting(meetingId, summary, callerId) {
|
|
158
|
-
return this.stateLock.withLock(async () => {
|
|
159
|
-
const meeting = await this.getMeeting(meetingId);
|
|
160
|
-
if (!meeting)
|
|
161
|
-
throw new Error(`Meeting '${meetingId}' not found.`);
|
|
162
|
-
if (meeting.status !== "active")
|
|
163
|
-
throw new Error(`Meeting '${meetingId}' is already ${meeting.status}.`);
|
|
164
|
-
// Permission check: Only initiator can end
|
|
165
|
-
if (callerId && meeting.initiator && meeting.initiator !== callerId) {
|
|
166
|
-
throw new Error(`Permission denied: Only initiator (${meeting.initiator}) can end this meeting.`);
|
|
167
|
-
}
|
|
168
|
-
// Close the meeting
|
|
169
|
-
meeting.status = "closed";
|
|
170
|
-
meeting.endTime = new Date().toISOString();
|
|
171
|
-
if (summary)
|
|
172
|
-
meeting.summary = summary;
|
|
173
|
-
await fs.writeFile(this.getMeetingPath(meetingId), JSON.stringify(meeting, null, 2), FILE_ENCODING);
|
|
174
|
-
// Update state - remove from active meetings
|
|
175
|
-
const state = await this.loadStateSafe();
|
|
176
|
-
state.activeMeetings = state.activeMeetings.filter(id => id !== meetingId);
|
|
177
|
-
state.defaultMeetingId = state.activeMeetings.length > 0
|
|
178
|
-
? state.activeMeetings[state.activeMeetings.length - 1]
|
|
179
|
-
: null;
|
|
180
|
-
await this.saveState(state);
|
|
181
|
-
// Suggest sync targets based on participants (extract project IDs)
|
|
182
|
-
const suggestedSyncTargets = meeting.participants
|
|
183
|
-
.map(p => p.split('@')[1])
|
|
184
|
-
.filter((v, i, a) => v && v !== "Global" && a.indexOf(v) === i);
|
|
185
|
-
return { meeting, suggestedSyncTargets };
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Archive a closed meeting
|
|
190
|
-
*/
|
|
191
|
-
static async archiveMeeting(meetingId, callerId) {
|
|
192
|
-
const meeting = await this.getMeeting(meetingId);
|
|
193
|
-
if (!meeting)
|
|
194
|
-
throw new Error(`Meeting '${meetingId}' not found.`);
|
|
195
|
-
if (meeting.status === "active")
|
|
196
|
-
throw new Error(`Meeting '${meetingId}' is still active. End it first.`);
|
|
197
|
-
// Permission check: Only initiator can archive
|
|
198
|
-
if (callerId && meeting.initiator && meeting.initiator !== callerId) {
|
|
199
|
-
throw new Error(`Permission denied: Only initiator (${meeting.initiator}) can archive this meeting.`);
|
|
200
|
-
}
|
|
201
|
-
meeting.status = "archived";
|
|
202
|
-
await fs.writeFile(this.getMeetingPath(meetingId), JSON.stringify(meeting, null, 2), FILE_ENCODING);
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Reopen a closed or archived meeting
|
|
206
|
-
*/
|
|
207
|
-
static async reopenMeeting(meetingId, _callerId) {
|
|
208
|
-
return this.stateLock.withLock(async () => {
|
|
209
|
-
const meeting = await this.getMeeting(meetingId);
|
|
210
|
-
if (!meeting)
|
|
211
|
-
throw new Error(`Meeting '${meetingId}' not found.`);
|
|
212
|
-
if (meeting.status === "active")
|
|
213
|
-
throw new Error(`Meeting '${meetingId}' is already active.`);
|
|
214
|
-
// Update status to active
|
|
215
|
-
meeting.status = "active";
|
|
216
|
-
meeting.endTime = undefined;
|
|
217
|
-
await fs.writeFile(this.getMeetingPath(meetingId), JSON.stringify(meeting, null, 2), FILE_ENCODING);
|
|
218
|
-
// Update state
|
|
219
|
-
const state = await this.loadStateSafe();
|
|
220
|
-
if (!state.activeMeetings.includes(meetingId)) {
|
|
221
|
-
state.activeMeetings.push(meetingId);
|
|
222
|
-
}
|
|
223
|
-
state.defaultMeetingId = meetingId;
|
|
224
|
-
await this.saveState(state);
|
|
225
|
-
return meeting;
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* List all meetings with optional status filter
|
|
230
|
-
*/
|
|
231
|
-
static async listMeetings(status) {
|
|
232
|
-
await this.init();
|
|
233
|
-
const files = await fs.readdir(this.meetingsDir);
|
|
234
|
-
const meetings = [];
|
|
235
|
-
for (const file of files) {
|
|
236
|
-
if (!file.endsWith('.json'))
|
|
237
|
-
continue;
|
|
238
|
-
const id = file.replace('.json', '');
|
|
239
|
-
const meeting = await this.getMeeting(id);
|
|
240
|
-
if (!meeting)
|
|
241
|
-
continue;
|
|
242
|
-
if (status && meeting.status !== status)
|
|
243
|
-
continue;
|
|
244
|
-
meetings.push({
|
|
245
|
-
id: meeting.id,
|
|
246
|
-
topic: meeting.topic,
|
|
247
|
-
status: meeting.status,
|
|
248
|
-
startTime: meeting.startTime,
|
|
249
|
-
participantCount: meeting.participants.length
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
// Sort by startTime descending
|
|
253
|
-
return meetings.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime());
|
|
254
|
-
}
|
|
255
|
-
/**
|
|
256
|
-
* Get the current active meeting (for auto-routing)
|
|
257
|
-
*/
|
|
258
|
-
static async getActiveMeeting() {
|
|
259
|
-
const state = await this.getState();
|
|
260
|
-
if (!state.defaultMeetingId)
|
|
261
|
-
return null;
|
|
262
|
-
return this.getMeeting(state.defaultMeetingId);
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* Get recent messages from the active meeting
|
|
266
|
-
*/
|
|
267
|
-
static async getRecentMessages(count = 10, meetingId) {
|
|
268
|
-
const targetId = meetingId || (await this.getState()).defaultMeetingId;
|
|
269
|
-
if (!targetId)
|
|
270
|
-
return [];
|
|
271
|
-
const meeting = await this.getMeeting(targetId);
|
|
272
|
-
if (!meeting)
|
|
273
|
-
return [];
|
|
274
|
-
return meeting.messages.slice(-count);
|
|
275
|
-
}
|
|
276
|
-
}
|
package/build/storage/store.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Meeting Store Entry Point
|
|
3
|
-
*
|
|
4
|
-
* Provides a unified interface that automatically selects between:
|
|
5
|
-
* - SQLite backend (preferred, for concurrent access safety)
|
|
6
|
-
* - JSON backend (fallback, for environments without native module support)
|
|
7
|
-
*/
|
|
8
|
-
// Lazy-loaded store implementation
|
|
9
|
-
let storeType = null;
|
|
10
|
-
let SqliteStore = null;
|
|
11
|
-
let JsonStore = null;
|
|
12
|
-
/**
|
|
13
|
-
* Detect and initialize the appropriate store
|
|
14
|
-
*/
|
|
15
|
-
async function getStore() {
|
|
16
|
-
if (storeType === "sqlite" && SqliteStore) {
|
|
17
|
-
return { type: "sqlite", store: SqliteStore };
|
|
18
|
-
}
|
|
19
|
-
if (storeType === "json" && JsonStore) {
|
|
20
|
-
return { type: "json", store: JsonStore };
|
|
21
|
-
}
|
|
22
|
-
// Try SQLite first
|
|
23
|
-
try {
|
|
24
|
-
// Dynamic import to avoid bundling issues
|
|
25
|
-
const sqliteModule = await import("./sqlite-meeting.js");
|
|
26
|
-
SqliteStore = sqliteModule.SqliteMeetingStore;
|
|
27
|
-
SqliteStore.init();
|
|
28
|
-
storeType = "sqlite";
|
|
29
|
-
console.error("[Nexus MeetingStore] Using SQLite backend");
|
|
30
|
-
return { type: "sqlite", store: SqliteStore };
|
|
31
|
-
}
|
|
32
|
-
catch (e) {
|
|
33
|
-
console.error("[Nexus MeetingStore] SQLite unavailable:", e.message);
|
|
34
|
-
console.error("[Nexus MeetingStore] Falling back to JSON backend");
|
|
35
|
-
console.warn("[Nexus MeetingStore] ⚠️ JSON mode is single-process only. For multi-IDE environments, install better-sqlite3.");
|
|
36
|
-
// Fall back to JSON
|
|
37
|
-
const jsonModule = await import("./meetings.js");
|
|
38
|
-
JsonStore = jsonModule.MeetingStore;
|
|
39
|
-
storeType = "json";
|
|
40
|
-
return { type: "json", store: JsonStore };
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Unified Meeting Store Interface
|
|
45
|
-
*/
|
|
46
|
-
export const UnifiedMeetingStore = {
|
|
47
|
-
/**
|
|
48
|
-
* Start a new meeting
|
|
49
|
-
*/
|
|
50
|
-
async startMeeting(topic, initiator) {
|
|
51
|
-
const { store } = await getStore();
|
|
52
|
-
return store.startMeeting(topic, initiator);
|
|
53
|
-
},
|
|
54
|
-
/**
|
|
55
|
-
* Get a meeting by ID
|
|
56
|
-
*/
|
|
57
|
-
async getMeeting(id) {
|
|
58
|
-
const { store } = await getStore();
|
|
59
|
-
return store.getMeeting(id);
|
|
60
|
-
},
|
|
61
|
-
/**
|
|
62
|
-
* Add a message to a meeting
|
|
63
|
-
*/
|
|
64
|
-
async addMessage(meetingId, message) {
|
|
65
|
-
const { store } = await getStore();
|
|
66
|
-
return store.addMessage(meetingId, message);
|
|
67
|
-
},
|
|
68
|
-
/**
|
|
69
|
-
* End a meeting
|
|
70
|
-
*/
|
|
71
|
-
async endMeeting(meetingId, summary, callerId) {
|
|
72
|
-
const { store } = await getStore();
|
|
73
|
-
return store.endMeeting(meetingId, summary, callerId);
|
|
74
|
-
},
|
|
75
|
-
/**
|
|
76
|
-
* Archive a meeting
|
|
77
|
-
*/
|
|
78
|
-
async archiveMeeting(meetingId, callerId) {
|
|
79
|
-
const { store } = await getStore();
|
|
80
|
-
return store.archiveMeeting(meetingId, callerId);
|
|
81
|
-
},
|
|
82
|
-
/**
|
|
83
|
-
* Reopen a meeting
|
|
84
|
-
*/
|
|
85
|
-
async reopenMeeting(meetingId, callerId) {
|
|
86
|
-
const { store } = await getStore();
|
|
87
|
-
return store.reopenMeeting(meetingId, callerId);
|
|
88
|
-
},
|
|
89
|
-
/**
|
|
90
|
-
* List meetings
|
|
91
|
-
*/
|
|
92
|
-
async listMeetings(status) {
|
|
93
|
-
const { store } = await getStore();
|
|
94
|
-
return store.listMeetings(status);
|
|
95
|
-
},
|
|
96
|
-
/**
|
|
97
|
-
* Get the current active meeting
|
|
98
|
-
*/
|
|
99
|
-
async getActiveMeeting() {
|
|
100
|
-
const { store } = await getStore();
|
|
101
|
-
return store.getActiveMeeting();
|
|
102
|
-
},
|
|
103
|
-
/**
|
|
104
|
-
* Get recent messages (incremental for SQLite with instanceId)
|
|
105
|
-
*/
|
|
106
|
-
async getRecentMessages(count, meetingId, instanceId) {
|
|
107
|
-
const { type, store } = await getStore();
|
|
108
|
-
if (type === "sqlite") {
|
|
109
|
-
// SQLite supports incremental reads with cursor tracking
|
|
110
|
-
return store.getRecentMessages(count || 10, meetingId, instanceId);
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
// JSON fallback - no incremental support
|
|
114
|
-
return store.getRecentMessages(count || 10, meetingId);
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
/**
|
|
118
|
-
* Get the current backend type
|
|
119
|
-
*/
|
|
120
|
-
async getBackendType() {
|
|
121
|
-
const { type } = await getStore();
|
|
122
|
-
return type;
|
|
123
|
-
},
|
|
124
|
-
/**
|
|
125
|
-
* Get meeting state (SQLite only, returns empty for JSON)
|
|
126
|
-
*/
|
|
127
|
-
async getState() {
|
|
128
|
-
const { type, store } = await getStore();
|
|
129
|
-
if (type === "sqlite") {
|
|
130
|
-
const sqliteStore = store;
|
|
131
|
-
const activeMeetings = JSON.parse(sqliteStore.getState("active_meetings") || "[]");
|
|
132
|
-
const defaultMeetingId = sqliteStore.getState("default_meeting") || null;
|
|
133
|
-
return { activeMeetings, defaultMeetingId };
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
// JSON backend
|
|
137
|
-
const jsonStore = store;
|
|
138
|
-
const state = await jsonStore.getState();
|
|
139
|
-
return state;
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
/**
|
|
143
|
-
* Get storage info for status display
|
|
144
|
-
* @returns storage_mode and is_degraded flag
|
|
145
|
-
*/
|
|
146
|
-
async getStorageInfo() {
|
|
147
|
-
const { type } = await getStore();
|
|
148
|
-
return {
|
|
149
|
-
storage_mode: type,
|
|
150
|
-
is_degraded: type === "json" // JSON mode is considered degraded
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
};
|