@mem9/opencode 0.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.
- package/README.md +268 -0
- package/package.json +54 -0
- package/src/index.ts +1 -0
- package/src/server/backend.ts +39 -0
- package/src/server/config.ts +251 -0
- package/src/server/debug.ts +112 -0
- package/src/server/hooks.ts +349 -0
- package/src/server/index.ts +73 -0
- package/src/server/ingest/select.ts +18 -0
- package/src/server/ingest/submit.ts +56 -0
- package/src/server/recall/format.ts +84 -0
- package/src/server/recall/query.ts +127 -0
- package/src/server/server-backend.ts +170 -0
- package/src/server/session-transcript.ts +65 -0
- package/src/server/setup-flow.ts +17 -0
- package/src/server/tools.ts +189 -0
- package/src/shared/credentials-store.ts +55 -0
- package/src/shared/defaults.ts +13 -0
- package/src/shared/platform-paths.ts +73 -0
- package/src/shared/plugin-meta.ts +1 -0
- package/src/shared/setup-files.ts +492 -0
- package/src/shared/types.ts +74 -0
- package/src/tui/index.ts +622 -0
package/src/tui/index.ts
ADDED
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type {
|
|
3
|
+
TuiCommand,
|
|
4
|
+
TuiPlugin,
|
|
5
|
+
TuiPluginApi,
|
|
6
|
+
} from "@opencode-ai/plugin/tui";
|
|
7
|
+
import {
|
|
8
|
+
resolveMem9Home,
|
|
9
|
+
resolveMem9Paths,
|
|
10
|
+
type Mem9ResolvedPaths,
|
|
11
|
+
} from "../shared/platform-paths.js";
|
|
12
|
+
import { PLUGIN_ID } from "../shared/plugin-meta.js";
|
|
13
|
+
import {
|
|
14
|
+
loadSetupState,
|
|
15
|
+
provisionApiKey,
|
|
16
|
+
writeScopeConfig,
|
|
17
|
+
writeSetupFiles,
|
|
18
|
+
type ScopeConfigState,
|
|
19
|
+
type SetupProfileSummary,
|
|
20
|
+
type SetupScope,
|
|
21
|
+
type SetupState,
|
|
22
|
+
} from "../shared/setup-files.js";
|
|
23
|
+
|
|
24
|
+
type SetupAction =
|
|
25
|
+
| "auto-api-key"
|
|
26
|
+
| "manual-api-key"
|
|
27
|
+
| "use-profile-in-scope"
|
|
28
|
+
| "configure-scope";
|
|
29
|
+
|
|
30
|
+
type ScopeFlowMode = "profile-only" | "full-config";
|
|
31
|
+
|
|
32
|
+
interface ProfileDraft {
|
|
33
|
+
profileId: string;
|
|
34
|
+
label: string;
|
|
35
|
+
baseUrl: string;
|
|
36
|
+
apiKey: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ScopeDraft {
|
|
40
|
+
scope: SetupScope;
|
|
41
|
+
profileId: string;
|
|
42
|
+
debug: boolean;
|
|
43
|
+
defaultTimeoutMs: number;
|
|
44
|
+
searchTimeoutMs: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SetupActionOption {
|
|
48
|
+
title: string;
|
|
49
|
+
value: SetupAction;
|
|
50
|
+
description: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getProjectDir(api: TuiPluginApi): string {
|
|
54
|
+
const worktree = api.state.path.worktree.trim();
|
|
55
|
+
if (worktree.length > 0) {
|
|
56
|
+
return worktree;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return api.state.path.directory;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getProjectName(api: TuiPluginApi): string {
|
|
63
|
+
const name = path.basename(getProjectDir(api)).trim();
|
|
64
|
+
return name.length > 0 ? name : "this project";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolvePaths(api: TuiPluginApi): Mem9ResolvedPaths {
|
|
68
|
+
return resolveMem9Paths({
|
|
69
|
+
configDir: api.state.path.config,
|
|
70
|
+
dataDir: api.state.path.state,
|
|
71
|
+
projectDir: getProjectDir(api),
|
|
72
|
+
mem9Home: resolveMem9Home(process.env),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function createProfileDraft(state: SetupState): ProfileDraft {
|
|
77
|
+
return {
|
|
78
|
+
profileId: state.suggestedNewProfileId,
|
|
79
|
+
label: state.suggestedLabel,
|
|
80
|
+
baseUrl: state.suggestedBaseUrl,
|
|
81
|
+
apiKey: "",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function createScopeDraft(
|
|
86
|
+
state: SetupState,
|
|
87
|
+
scope: SetupScope,
|
|
88
|
+
): ScopeDraft {
|
|
89
|
+
const scopeState = state.scopeStates[scope];
|
|
90
|
+
return {
|
|
91
|
+
scope,
|
|
92
|
+
profileId: scopeState.profileId ?? state.usableProfiles[0]?.profileId ?? "",
|
|
93
|
+
debug: scopeState.debug,
|
|
94
|
+
defaultTimeoutMs: scopeState.defaultTimeoutMs,
|
|
95
|
+
searchTimeoutMs: scopeState.searchTimeoutMs,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function showError(
|
|
100
|
+
api: TuiPluginApi,
|
|
101
|
+
error: unknown,
|
|
102
|
+
retryCommand = false,
|
|
103
|
+
): void {
|
|
104
|
+
const suffix = retryCommand ? " Run /mem9-setup again when you are ready to retry." : "";
|
|
105
|
+
api.ui.toast({
|
|
106
|
+
variant: "error",
|
|
107
|
+
title: "mem9 setup failed",
|
|
108
|
+
message: `${error instanceof Error ? error.message : String(error)}${suffix}`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function showProfileSavedSuccess(api: TuiPluginApi, profileId: string): void {
|
|
113
|
+
api.ui.toast({
|
|
114
|
+
variant: "success",
|
|
115
|
+
title: "mem9 configured",
|
|
116
|
+
message: `Saved profile ${profileId} and set it as the default user profile. Restart OpenCode to reload mem9.`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function showScopeSavedSuccess(
|
|
121
|
+
api: TuiPluginApi,
|
|
122
|
+
scope: SetupScope,
|
|
123
|
+
profileId: string,
|
|
124
|
+
): void {
|
|
125
|
+
const scopeLabel = scope === "user" ? "user settings" : "project settings";
|
|
126
|
+
api.ui.toast({
|
|
127
|
+
variant: "success",
|
|
128
|
+
title: "mem9 configured",
|
|
129
|
+
message: `Saved ${scopeLabel} with profile ${profileId}. Restart OpenCode to reload mem9.`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isReusableProfileID(state: SetupState, profileId: string): boolean {
|
|
134
|
+
return state.usableProfiles.some((profile) => profile.profileId === profileId);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function buildSetupActionOptions(
|
|
138
|
+
state: Pick<SetupState, "usableProfiles">,
|
|
139
|
+
): SetupActionOption[] {
|
|
140
|
+
const options: SetupActionOption[] = [
|
|
141
|
+
{
|
|
142
|
+
title: "Get a mem9 API key automatically",
|
|
143
|
+
value: "auto-api-key",
|
|
144
|
+
description: "Request a new mem9 API key and save it as a profile.",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
title: "Add an existing mem9 API key",
|
|
148
|
+
value: "manual-api-key",
|
|
149
|
+
description: "Paste a mem9 API key and save it as a profile.",
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
if (state.usableProfiles.length > 0) {
|
|
154
|
+
options.push(
|
|
155
|
+
{
|
|
156
|
+
title: "Use an existing mem9 profile in a scope",
|
|
157
|
+
value: "use-profile-in-scope",
|
|
158
|
+
description: "Choose a saved profile and apply it to user or project settings.",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
title: "Configure user/project settings",
|
|
162
|
+
value: "configure-scope",
|
|
163
|
+
description: "Change scope profile, debug logging, and request timeouts.",
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return options;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parsePositiveInteger(value: string, field: string): number | null {
|
|
172
|
+
const trimmed = value.trim();
|
|
173
|
+
if (trimmed.length === 0) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
178
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return Math.floor(parsed);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function submitManualProfile(
|
|
186
|
+
api: TuiPluginApi,
|
|
187
|
+
paths: Mem9ResolvedPaths,
|
|
188
|
+
draft: ProfileDraft,
|
|
189
|
+
): Promise<void> {
|
|
190
|
+
try {
|
|
191
|
+
await writeSetupFiles({
|
|
192
|
+
paths,
|
|
193
|
+
profileId: draft.profileId,
|
|
194
|
+
label: draft.label,
|
|
195
|
+
baseUrl: draft.baseUrl,
|
|
196
|
+
apiKey: draft.apiKey,
|
|
197
|
+
});
|
|
198
|
+
api.ui.dialog.clear();
|
|
199
|
+
showProfileSavedSuccess(api, draft.profileId);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
showError(api, error);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function submitProvisionedProfile(
|
|
206
|
+
api: TuiPluginApi,
|
|
207
|
+
paths: Mem9ResolvedPaths,
|
|
208
|
+
draft: ProfileDraft,
|
|
209
|
+
): Promise<void> {
|
|
210
|
+
try {
|
|
211
|
+
api.ui.toast({
|
|
212
|
+
variant: "info",
|
|
213
|
+
title: "mem9 setup",
|
|
214
|
+
message: "Requesting a new mem9 API key...",
|
|
215
|
+
duration: 3000,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const apiKey = await provisionApiKey({
|
|
219
|
+
baseUrl: draft.baseUrl,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await writeSetupFiles({
|
|
223
|
+
paths,
|
|
224
|
+
profileId: draft.profileId,
|
|
225
|
+
label: draft.label,
|
|
226
|
+
baseUrl: draft.baseUrl,
|
|
227
|
+
apiKey,
|
|
228
|
+
});
|
|
229
|
+
api.ui.dialog.clear();
|
|
230
|
+
showProfileSavedSuccess(api, draft.profileId);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
api.ui.dialog.clear();
|
|
233
|
+
showError(api, error, true);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function submitScopeConfigDraft(
|
|
238
|
+
api: TuiPluginApi,
|
|
239
|
+
paths: Mem9ResolvedPaths,
|
|
240
|
+
draft: ScopeDraft,
|
|
241
|
+
): Promise<void> {
|
|
242
|
+
try {
|
|
243
|
+
await writeScopeConfig({
|
|
244
|
+
paths,
|
|
245
|
+
scope: draft.scope,
|
|
246
|
+
profileId: draft.profileId,
|
|
247
|
+
debug: draft.debug,
|
|
248
|
+
defaultTimeoutMs: draft.defaultTimeoutMs,
|
|
249
|
+
searchTimeoutMs: draft.searchTimeoutMs,
|
|
250
|
+
});
|
|
251
|
+
api.ui.dialog.clear();
|
|
252
|
+
showScopeSavedSuccess(api, draft.scope, draft.profileId);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
showError(api, error);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function showActionDialog(
|
|
259
|
+
api: TuiPluginApi,
|
|
260
|
+
paths: Mem9ResolvedPaths,
|
|
261
|
+
state: SetupState,
|
|
262
|
+
): void {
|
|
263
|
+
const options = buildSetupActionOptions(state);
|
|
264
|
+
|
|
265
|
+
api.ui.dialog.replace(() =>
|
|
266
|
+
api.ui.DialogSelect<SetupAction>({
|
|
267
|
+
title: "What do you want to set up?",
|
|
268
|
+
current: options[0]?.value,
|
|
269
|
+
options,
|
|
270
|
+
onSelect: (option) => {
|
|
271
|
+
if (option.value === "auto-api-key" || option.value === "manual-api-key") {
|
|
272
|
+
const draft = createProfileDraft(state);
|
|
273
|
+
showProfileIdDialog(api, paths, state, option.value, draft);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (option.value === "use-profile-in-scope") {
|
|
278
|
+
showScopeDialog(api, paths, state, "profile-only");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
showScopeDialog(api, paths, state, "full-config");
|
|
283
|
+
},
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function showProfileIdDialog(
|
|
289
|
+
api: TuiPluginApi,
|
|
290
|
+
paths: Mem9ResolvedPaths,
|
|
291
|
+
state: SetupState,
|
|
292
|
+
action: "auto-api-key" | "manual-api-key",
|
|
293
|
+
draft: ProfileDraft,
|
|
294
|
+
): void {
|
|
295
|
+
api.ui.dialog.replace(() =>
|
|
296
|
+
api.ui.DialogPrompt({
|
|
297
|
+
title: "Profile ID",
|
|
298
|
+
value: draft.profileId,
|
|
299
|
+
placeholder: state.suggestedNewProfileId,
|
|
300
|
+
onConfirm: (value) => {
|
|
301
|
+
const next = value.trim();
|
|
302
|
+
if (next.length === 0) {
|
|
303
|
+
api.ui.toast({
|
|
304
|
+
variant: "warning",
|
|
305
|
+
message: "Profile ID is required.",
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (isReusableProfileID(state, next)) {
|
|
311
|
+
api.ui.toast({
|
|
312
|
+
variant: "warning",
|
|
313
|
+
message: "That profile already has credentials. Pick a new profile ID or use the existing profile in scope settings.",
|
|
314
|
+
});
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
draft.profileId = next;
|
|
319
|
+
if (draft.label.trim().length === 0) {
|
|
320
|
+
draft.label = next === "default" ? "Personal" : next;
|
|
321
|
+
}
|
|
322
|
+
showProfileLabelDialog(api, paths, state, action, draft);
|
|
323
|
+
},
|
|
324
|
+
onCancel: () => {
|
|
325
|
+
showActionDialog(api, paths, state);
|
|
326
|
+
},
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function showProfileLabelDialog(
|
|
332
|
+
api: TuiPluginApi,
|
|
333
|
+
paths: Mem9ResolvedPaths,
|
|
334
|
+
state: SetupState,
|
|
335
|
+
action: "auto-api-key" | "manual-api-key",
|
|
336
|
+
draft: ProfileDraft,
|
|
337
|
+
): void {
|
|
338
|
+
api.ui.dialog.replace(() =>
|
|
339
|
+
api.ui.DialogPrompt({
|
|
340
|
+
title: "Profile label",
|
|
341
|
+
value: draft.label,
|
|
342
|
+
placeholder: draft.profileId === "default" ? "Personal" : draft.profileId,
|
|
343
|
+
onConfirm: (value) => {
|
|
344
|
+
const next = value.trim();
|
|
345
|
+
if (next.length === 0) {
|
|
346
|
+
api.ui.toast({
|
|
347
|
+
variant: "warning",
|
|
348
|
+
message: "Profile label is required.",
|
|
349
|
+
});
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
draft.label = next;
|
|
354
|
+
showProfileBaseUrlDialog(api, paths, state, action, draft);
|
|
355
|
+
},
|
|
356
|
+
onCancel: () => {
|
|
357
|
+
showProfileIdDialog(api, paths, state, action, draft);
|
|
358
|
+
},
|
|
359
|
+
}),
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function showProfileBaseUrlDialog(
|
|
364
|
+
api: TuiPluginApi,
|
|
365
|
+
paths: Mem9ResolvedPaths,
|
|
366
|
+
state: SetupState,
|
|
367
|
+
action: "auto-api-key" | "manual-api-key",
|
|
368
|
+
draft: ProfileDraft,
|
|
369
|
+
): void {
|
|
370
|
+
api.ui.dialog.replace(() =>
|
|
371
|
+
api.ui.DialogPrompt({
|
|
372
|
+
title: "mem9 API URL",
|
|
373
|
+
value: draft.baseUrl,
|
|
374
|
+
placeholder: "https://api.mem9.ai",
|
|
375
|
+
onConfirm: (value) => {
|
|
376
|
+
const next = value.trim();
|
|
377
|
+
if (next.length === 0) {
|
|
378
|
+
api.ui.toast({
|
|
379
|
+
variant: "warning",
|
|
380
|
+
message: "mem9 API URL is required.",
|
|
381
|
+
});
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
draft.baseUrl = next;
|
|
386
|
+
if (action === "auto-api-key") {
|
|
387
|
+
void submitProvisionedProfile(api, paths, draft);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
showProfileApiKeyDialog(api, paths, state, draft);
|
|
392
|
+
},
|
|
393
|
+
onCancel: () => {
|
|
394
|
+
showProfileLabelDialog(api, paths, state, action, draft);
|
|
395
|
+
},
|
|
396
|
+
}),
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function showProfileApiKeyDialog(
|
|
401
|
+
api: TuiPluginApi,
|
|
402
|
+
paths: Mem9ResolvedPaths,
|
|
403
|
+
state: SetupState,
|
|
404
|
+
draft: ProfileDraft,
|
|
405
|
+
): void {
|
|
406
|
+
api.ui.toast({
|
|
407
|
+
variant: "warning",
|
|
408
|
+
message: "The current OpenCode prompt is plain text. Your API key stays visible while typing.",
|
|
409
|
+
duration: 4000,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
api.ui.dialog.replace(() =>
|
|
413
|
+
api.ui.DialogPrompt({
|
|
414
|
+
title: "mem9 API key",
|
|
415
|
+
placeholder: "mk_...",
|
|
416
|
+
onConfirm: (value) => {
|
|
417
|
+
const next = value.trim();
|
|
418
|
+
if (next.length === 0) {
|
|
419
|
+
api.ui.toast({
|
|
420
|
+
variant: "warning",
|
|
421
|
+
message: "mem9 API key is required.",
|
|
422
|
+
});
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
draft.apiKey = next;
|
|
427
|
+
void submitManualProfile(api, paths, draft);
|
|
428
|
+
},
|
|
429
|
+
onCancel: () => {
|
|
430
|
+
showProfileBaseUrlDialog(api, paths, state, "manual-api-key", draft);
|
|
431
|
+
},
|
|
432
|
+
}),
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function showScopeDialog(
|
|
437
|
+
api: TuiPluginApi,
|
|
438
|
+
paths: Mem9ResolvedPaths,
|
|
439
|
+
state: SetupState,
|
|
440
|
+
mode: ScopeFlowMode,
|
|
441
|
+
): void {
|
|
442
|
+
const projectName = getProjectName(api);
|
|
443
|
+
|
|
444
|
+
api.ui.dialog.replace(() =>
|
|
445
|
+
api.ui.DialogSelect<SetupScope>({
|
|
446
|
+
title: "Which settings scope do you want to update?",
|
|
447
|
+
current: "user",
|
|
448
|
+
options: [
|
|
449
|
+
{
|
|
450
|
+
title: "User settings",
|
|
451
|
+
value: "user",
|
|
452
|
+
description: "Use the same default mem9 settings across OpenCode on this machine.",
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
title: "Project settings",
|
|
456
|
+
value: "project",
|
|
457
|
+
description: `Only override mem9 settings for ${projectName}.`,
|
|
458
|
+
},
|
|
459
|
+
],
|
|
460
|
+
onSelect: (option) => {
|
|
461
|
+
const draft = createScopeDraft(state, option.value);
|
|
462
|
+
showScopeProfileDialog(api, paths, state, mode, draft);
|
|
463
|
+
},
|
|
464
|
+
}),
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function showScopeProfileDialog(
|
|
469
|
+
api: TuiPluginApi,
|
|
470
|
+
paths: Mem9ResolvedPaths,
|
|
471
|
+
state: SetupState,
|
|
472
|
+
mode: ScopeFlowMode,
|
|
473
|
+
draft: ScopeDraft,
|
|
474
|
+
): void {
|
|
475
|
+
api.ui.dialog.replace(() =>
|
|
476
|
+
api.ui.DialogSelect<string>({
|
|
477
|
+
title: "Which mem9 profile should this scope use?",
|
|
478
|
+
current: draft.profileId,
|
|
479
|
+
options: state.profiles.map((profile) => ({
|
|
480
|
+
title: `${profile.label} (${profile.profileId})`,
|
|
481
|
+
value: profile.profileId,
|
|
482
|
+
description: `${profile.baseUrl} • ${profile.hasApiKey ? "API key configured" : "API key missing"}`,
|
|
483
|
+
disabled: !profile.hasApiKey,
|
|
484
|
+
})),
|
|
485
|
+
onSelect: (option) => {
|
|
486
|
+
draft.profileId = option.value;
|
|
487
|
+
if (mode === "profile-only") {
|
|
488
|
+
void submitScopeConfigDraft(api, paths, draft);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
showScopeDebugDialog(api, paths, state, draft);
|
|
493
|
+
},
|
|
494
|
+
}),
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function showScopeDebugDialog(
|
|
499
|
+
api: TuiPluginApi,
|
|
500
|
+
paths: Mem9ResolvedPaths,
|
|
501
|
+
state: SetupState,
|
|
502
|
+
draft: ScopeDraft,
|
|
503
|
+
): void {
|
|
504
|
+
api.ui.dialog.replace(() =>
|
|
505
|
+
api.ui.DialogSelect<boolean>({
|
|
506
|
+
title: "Debug logging",
|
|
507
|
+
current: draft.debug,
|
|
508
|
+
options: [
|
|
509
|
+
{
|
|
510
|
+
title: "Disabled",
|
|
511
|
+
value: false,
|
|
512
|
+
description: "Keep debug logging off.",
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
title: "Enabled",
|
|
516
|
+
value: true,
|
|
517
|
+
description: "Write redacted debug logs to the OpenCode state directory.",
|
|
518
|
+
},
|
|
519
|
+
],
|
|
520
|
+
onSelect: (option) => {
|
|
521
|
+
draft.debug = option.value;
|
|
522
|
+
showDefaultTimeoutDialog(api, paths, state, draft);
|
|
523
|
+
},
|
|
524
|
+
}),
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function showDefaultTimeoutDialog(
|
|
529
|
+
api: TuiPluginApi,
|
|
530
|
+
paths: Mem9ResolvedPaths,
|
|
531
|
+
state: SetupState,
|
|
532
|
+
draft: ScopeDraft,
|
|
533
|
+
): void {
|
|
534
|
+
api.ui.dialog.replace(() =>
|
|
535
|
+
api.ui.DialogPrompt({
|
|
536
|
+
title: "Default request timeout (ms)",
|
|
537
|
+
value: String(draft.defaultTimeoutMs),
|
|
538
|
+
placeholder: "8000",
|
|
539
|
+
onConfirm: (value) => {
|
|
540
|
+
const parsed = parsePositiveInteger(value, "defaultTimeoutMs");
|
|
541
|
+
if (parsed === null) {
|
|
542
|
+
api.ui.toast({
|
|
543
|
+
variant: "warning",
|
|
544
|
+
message: "Default timeout must be a positive integer.",
|
|
545
|
+
});
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
draft.defaultTimeoutMs = parsed;
|
|
550
|
+
showSearchTimeoutDialog(api, paths, state, draft);
|
|
551
|
+
},
|
|
552
|
+
onCancel: () => {
|
|
553
|
+
showScopeProfileDialog(api, paths, state, "full-config", draft);
|
|
554
|
+
},
|
|
555
|
+
}),
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function showSearchTimeoutDialog(
|
|
560
|
+
api: TuiPluginApi,
|
|
561
|
+
paths: Mem9ResolvedPaths,
|
|
562
|
+
state: SetupState,
|
|
563
|
+
draft: ScopeDraft,
|
|
564
|
+
): void {
|
|
565
|
+
api.ui.dialog.replace(() =>
|
|
566
|
+
api.ui.DialogPrompt({
|
|
567
|
+
title: "Search timeout (ms)",
|
|
568
|
+
value: String(draft.searchTimeoutMs),
|
|
569
|
+
placeholder: "15000",
|
|
570
|
+
onConfirm: (value) => {
|
|
571
|
+
const parsed = parsePositiveInteger(value, "searchTimeoutMs");
|
|
572
|
+
if (parsed === null) {
|
|
573
|
+
api.ui.toast({
|
|
574
|
+
variant: "warning",
|
|
575
|
+
message: "Search timeout must be a positive integer.",
|
|
576
|
+
});
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
draft.searchTimeoutMs = parsed;
|
|
581
|
+
void submitScopeConfigDraft(api, paths, draft);
|
|
582
|
+
},
|
|
583
|
+
onCancel: () => {
|
|
584
|
+
showDefaultTimeoutDialog(api, paths, state, draft);
|
|
585
|
+
},
|
|
586
|
+
}),
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function startSetup(api: TuiPluginApi): Promise<void> {
|
|
591
|
+
try {
|
|
592
|
+
const paths = resolvePaths(api);
|
|
593
|
+
const state = await loadSetupState(paths);
|
|
594
|
+
showActionDialog(api, paths, state);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
showError(api, error);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const tui: TuiPlugin = async (api): Promise<void> => {
|
|
601
|
+
const dispose = api.command.register((): TuiCommand[] => [
|
|
602
|
+
{
|
|
603
|
+
title: "mem9: setup",
|
|
604
|
+
value: "mem9-setup",
|
|
605
|
+
category: "mem9",
|
|
606
|
+
description: "Manage mem9 API keys, profiles, and scope settings.",
|
|
607
|
+
slash: {
|
|
608
|
+
name: "mem9-setup",
|
|
609
|
+
},
|
|
610
|
+
onSelect: () => {
|
|
611
|
+
void startSetup(api);
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
]);
|
|
615
|
+
|
|
616
|
+
api.lifecycle.onDispose(dispose);
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
export default {
|
|
620
|
+
id: PLUGIN_ID,
|
|
621
|
+
tui,
|
|
622
|
+
};
|