openhome-cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-Q4UKUXDB.js → chunk-OAKGNZQM.js} +11 -1
- package/dist/cli.js +1196 -1132
- package/dist/{store-DR7EKQ5T.js → store-USDMWKXY.js} +7 -3
- package/package.json +1 -1
- package/src/api/client.ts +76 -25
- package/src/api/contracts.ts +21 -0
- package/src/api/endpoints.ts +1 -2
- package/src/cli.ts +1 -9
- package/src/commands/assign.ts +5 -4
- package/src/commands/delete.ts +5 -4
- package/src/commands/init.ts +14 -2
- package/src/commands/list.ts +5 -4
- package/src/commands/login.ts +23 -1
- package/src/commands/status.ts +4 -3
- package/src/commands/toggle.ts +5 -4
- package/src/config/store.ts +11 -1
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getApiKey,
|
|
3
3
|
getConfig,
|
|
4
|
+
getJwt,
|
|
4
5
|
getTrackedAbilities,
|
|
5
6
|
registerAbility,
|
|
6
7
|
saveApiKey,
|
|
7
|
-
saveConfig
|
|
8
|
-
|
|
8
|
+
saveConfig,
|
|
9
|
+
saveJwt
|
|
10
|
+
} from "./chunk-OAKGNZQM.js";
|
|
9
11
|
export {
|
|
10
12
|
getApiKey,
|
|
11
13
|
getConfig,
|
|
14
|
+
getJwt,
|
|
12
15
|
getTrackedAbilities,
|
|
13
16
|
registerAbility,
|
|
14
17
|
saveApiKey,
|
|
15
|
-
saveConfig
|
|
18
|
+
saveConfig,
|
|
19
|
+
saveJwt
|
|
16
20
|
};
|
package/package.json
CHANGED
package/src/api/client.ts
CHANGED
|
@@ -10,6 +10,8 @@ import type {
|
|
|
10
10
|
DeleteCapabilityResponse,
|
|
11
11
|
ToggleCapabilityResponse,
|
|
12
12
|
AssignCapabilitiesResponse,
|
|
13
|
+
InstalledCapability,
|
|
14
|
+
AbilitySummaryWithExtras,
|
|
13
15
|
} from "./contracts.js";
|
|
14
16
|
import { API_BASE, ENDPOINTS } from "./endpoints.js";
|
|
15
17
|
|
|
@@ -59,6 +61,7 @@ export class ApiClient implements IApiClient {
|
|
|
59
61
|
constructor(
|
|
60
62
|
private readonly apiKey: string,
|
|
61
63
|
baseUrl?: string,
|
|
64
|
+
private readonly jwt?: string,
|
|
62
65
|
) {
|
|
63
66
|
this.baseUrl = baseUrl ?? API_BASE;
|
|
64
67
|
if (!this.baseUrl.startsWith("https://")) {
|
|
@@ -69,33 +72,42 @@ export class ApiClient implements IApiClient {
|
|
|
69
72
|
private async request<T>(
|
|
70
73
|
path: string,
|
|
71
74
|
options: RequestInit = {},
|
|
75
|
+
useJwt = false,
|
|
72
76
|
): Promise<T> {
|
|
77
|
+
const token = useJwt ? this.jwt : this.apiKey;
|
|
73
78
|
const url = `${this.baseUrl}${path}`;
|
|
74
79
|
const response = await fetch(url, {
|
|
75
80
|
...options,
|
|
76
81
|
headers: {
|
|
77
|
-
Authorization: `Bearer ${
|
|
82
|
+
Authorization: `Bearer ${token}`,
|
|
78
83
|
...(options.headers ?? {}),
|
|
79
84
|
},
|
|
80
85
|
});
|
|
81
86
|
|
|
82
87
|
if (!response.ok) {
|
|
83
|
-
|
|
88
|
+
if (response.status === 404) {
|
|
89
|
+
throw new NotImplementedError(path);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let body: Record<string, unknown> | null = null;
|
|
84
93
|
try {
|
|
85
|
-
body = (await response.json()) as
|
|
94
|
+
body = (await response.json()) as Record<string, unknown>;
|
|
86
95
|
} catch {
|
|
87
96
|
// ignore parse errors
|
|
88
97
|
}
|
|
89
98
|
|
|
90
|
-
if (
|
|
99
|
+
if (
|
|
100
|
+
(body as ApiErrorResponse | null)?.error?.code === "NOT_IMPLEMENTED"
|
|
101
|
+
) {
|
|
91
102
|
throw new NotImplementedError(path);
|
|
92
103
|
}
|
|
93
104
|
|
|
94
|
-
|
|
95
|
-
body?.
|
|
96
|
-
body?.error?.message ??
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
const message =
|
|
106
|
+
(body?.detail as string) ??
|
|
107
|
+
(body as ApiErrorResponse | null)?.error?.message ??
|
|
108
|
+
response.statusText;
|
|
109
|
+
|
|
110
|
+
throw new ApiError(String(response.status), message);
|
|
99
111
|
}
|
|
100
112
|
|
|
101
113
|
return response.json() as Promise<T>;
|
|
@@ -154,15 +166,38 @@ export class ApiClient implements IApiClient {
|
|
|
154
166
|
}
|
|
155
167
|
|
|
156
168
|
async listAbilities(): Promise<ListAbilitiesResponse> {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
169
|
+
const data = await this.request<InstalledCapability[]>(
|
|
170
|
+
ENDPOINTS.listCapabilities,
|
|
171
|
+
{ method: "GET" },
|
|
172
|
+
true, // uses JWT
|
|
173
|
+
);
|
|
174
|
+
// Normalise to AbilitySummary shape
|
|
175
|
+
return {
|
|
176
|
+
abilities: data.map((c) => ({
|
|
177
|
+
ability_id: String(c.id),
|
|
178
|
+
unique_name: c.name,
|
|
179
|
+
display_name: c.name,
|
|
180
|
+
version: 1,
|
|
181
|
+
status: c.enabled ? "active" : "disabled",
|
|
182
|
+
personality_ids: [],
|
|
183
|
+
created_at: c.last_updated ?? new Date().toISOString(),
|
|
184
|
+
updated_at: c.last_updated ?? new Date().toISOString(),
|
|
185
|
+
trigger_words: c.trigger_words,
|
|
186
|
+
category: c.category,
|
|
187
|
+
})),
|
|
188
|
+
};
|
|
160
189
|
}
|
|
161
190
|
|
|
162
191
|
async getAbility(id: string): Promise<GetAbilityResponse> {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
192
|
+
// No single-get endpoint — fetch all and filter
|
|
193
|
+
const { abilities } = await this.listAbilities();
|
|
194
|
+
const found = abilities.find(
|
|
195
|
+
(a) => a.ability_id === id || a.unique_name === id,
|
|
196
|
+
);
|
|
197
|
+
if (!found) {
|
|
198
|
+
throw new ApiError("404", `Ability "${id}" not found.`);
|
|
199
|
+
}
|
|
200
|
+
return { ...found, validation_errors: [], deploy_history: [] };
|
|
166
201
|
}
|
|
167
202
|
|
|
168
203
|
async verifyApiKey(apiKey: string): Promise<VerifyApiKeyResponse> {
|
|
@@ -205,13 +240,26 @@ export class ApiClient implements IApiClient {
|
|
|
205
240
|
id: string,
|
|
206
241
|
enabled: boolean,
|
|
207
242
|
): Promise<ToggleCapabilityResponse> {
|
|
243
|
+
// Fetch current state first so we can PUT back the full object
|
|
244
|
+
const { abilities } = await this.listAbilities();
|
|
245
|
+
const current = abilities.find((a) => a.ability_id === id);
|
|
246
|
+
if (!current) {
|
|
247
|
+
throw new ApiError("404", `Ability "${id}" not found.`);
|
|
248
|
+
}
|
|
208
249
|
return this.request<ToggleCapabilityResponse>(
|
|
209
250
|
ENDPOINTS.editInstalledCapability(id),
|
|
210
251
|
{
|
|
211
|
-
method: "
|
|
252
|
+
method: "PUT",
|
|
212
253
|
headers: { "Content-Type": "application/json" },
|
|
213
|
-
body: JSON.stringify({
|
|
254
|
+
body: JSON.stringify({
|
|
255
|
+
enabled,
|
|
256
|
+
name: current.unique_name,
|
|
257
|
+
category: (current as AbilitySummaryWithExtras).category ?? "skill",
|
|
258
|
+
trigger_words:
|
|
259
|
+
(current as AbilitySummaryWithExtras).trigger_words ?? [],
|
|
260
|
+
}),
|
|
214
261
|
},
|
|
262
|
+
true, // uses JWT
|
|
215
263
|
);
|
|
216
264
|
}
|
|
217
265
|
|
|
@@ -219,13 +267,16 @@ export class ApiClient implements IApiClient {
|
|
|
219
267
|
personalityId: string,
|
|
220
268
|
capabilityIds: number[],
|
|
221
269
|
): Promise<AssignCapabilitiesResponse> {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
270
|
+
// Uses multipart/form-data — JSON is rejected
|
|
271
|
+
const form = new FormData();
|
|
272
|
+
form.append("personality_id", personalityId);
|
|
273
|
+
for (const capId of capabilityIds) {
|
|
274
|
+
form.append("matching_capabilities", String(capId));
|
|
275
|
+
}
|
|
276
|
+
return this.request<AssignCapabilitiesResponse>(
|
|
277
|
+
ENDPOINTS.editPersonality,
|
|
278
|
+
{ method: "PUT", body: form },
|
|
279
|
+
true, // uses JWT
|
|
280
|
+
);
|
|
230
281
|
}
|
|
231
282
|
}
|
package/src/api/contracts.ts
CHANGED
|
@@ -48,6 +48,27 @@ export interface AbilitySummary {
|
|
|
48
48
|
updated_at: string;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// Extended with fields from the real API
|
|
52
|
+
export interface AbilitySummaryWithExtras extends AbilitySummary {
|
|
53
|
+
trigger_words?: string[];
|
|
54
|
+
category?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Raw shape returned by get-installed-capabilities
|
|
58
|
+
export interface InstalledCapability {
|
|
59
|
+
id: number;
|
|
60
|
+
name: string;
|
|
61
|
+
category: string;
|
|
62
|
+
enabled: boolean;
|
|
63
|
+
trigger_words: string[];
|
|
64
|
+
last_updated?: string;
|
|
65
|
+
image_file?: string;
|
|
66
|
+
default?: boolean;
|
|
67
|
+
system_capability?: boolean;
|
|
68
|
+
agent_capability?: boolean;
|
|
69
|
+
shortcut?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
51
72
|
export interface ListAbilitiesResponse {
|
|
52
73
|
abilities: AbilitySummary[];
|
|
53
74
|
}
|
package/src/api/endpoints.ts
CHANGED
|
@@ -6,8 +6,7 @@ export const ENDPOINTS = {
|
|
|
6
6
|
getPersonalities: "/api/sdk/get_personalities",
|
|
7
7
|
verifyApiKey: "/api/sdk/verify_apikey/",
|
|
8
8
|
uploadCapability: "/api/capabilities/add-capability/",
|
|
9
|
-
listCapabilities: "/api/capabilities/get-
|
|
10
|
-
getCapability: (id: string) => `/api/capabilities/get-capability/${id}/`,
|
|
9
|
+
listCapabilities: "/api/capabilities/get-installed-capabilities/",
|
|
11
10
|
deleteCapability: (id: string) =>
|
|
12
11
|
`/api/capabilities/delete-capability/${id}/`,
|
|
13
12
|
bulkDeleteCapabilities: "/api/capabilities/delete-capability/",
|
package/src/cli.ts
CHANGED
|
@@ -58,12 +58,7 @@ async function interactiveMenu(): Promise<void> {
|
|
|
58
58
|
{
|
|
59
59
|
value: "init",
|
|
60
60
|
label: "✨ Create Ability",
|
|
61
|
-
hint: "Scaffold a new ability
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
value: "deploy",
|
|
65
|
-
label: "🚀 Deploy",
|
|
66
|
-
hint: "Upload ability to OpenHome",
|
|
61
|
+
hint: "Scaffold and deploy a new ability",
|
|
67
62
|
},
|
|
68
63
|
{
|
|
69
64
|
value: "chat",
|
|
@@ -134,9 +129,6 @@ async function interactiveMenu(): Promise<void> {
|
|
|
134
129
|
case "init":
|
|
135
130
|
await initCommand();
|
|
136
131
|
break;
|
|
137
|
-
case "deploy":
|
|
138
|
-
await deployCommand();
|
|
139
|
-
break;
|
|
140
132
|
case "chat":
|
|
141
133
|
await chatCommand();
|
|
142
134
|
break;
|
package/src/commands/assign.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ApiClient, NotImplementedError } from "../api/client.js";
|
|
2
2
|
import { MockApiClient } from "../api/mock-client.js";
|
|
3
|
-
import { getApiKey, getConfig } from "../config/store.js";
|
|
3
|
+
import { getApiKey, getConfig, getJwt } from "../config/store.js";
|
|
4
4
|
import { error, success, info, p, handleCancel } from "../ui/format.js";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
|
|
@@ -14,12 +14,13 @@ export async function assignCommand(
|
|
|
14
14
|
if (opts.mock) {
|
|
15
15
|
client = new MockApiClient();
|
|
16
16
|
} else {
|
|
17
|
-
const apiKey = getApiKey();
|
|
18
|
-
|
|
17
|
+
const apiKey = getApiKey() ?? "";
|
|
18
|
+
const jwt = getJwt() ?? undefined;
|
|
19
|
+
if (!apiKey && !jwt) {
|
|
19
20
|
error("Not authenticated. Run: openhome login");
|
|
20
21
|
process.exit(1);
|
|
21
22
|
}
|
|
22
|
-
client = new ApiClient(apiKey, getConfig().api_base_url);
|
|
23
|
+
client = new ApiClient(apiKey, getConfig().api_base_url, jwt);
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const s = p.spinner();
|
package/src/commands/delete.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ApiClient, NotImplementedError } from "../api/client.js";
|
|
2
2
|
import { MockApiClient } from "../api/mock-client.js";
|
|
3
|
-
import { getApiKey, getConfig } from "../config/store.js";
|
|
3
|
+
import { getApiKey, getConfig, getJwt } from "../config/store.js";
|
|
4
4
|
import { error, success, p, handleCancel } from "../ui/format.js";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
|
|
@@ -15,12 +15,13 @@ export async function deleteCommand(
|
|
|
15
15
|
if (opts.mock) {
|
|
16
16
|
client = new MockApiClient();
|
|
17
17
|
} else {
|
|
18
|
-
const apiKey = getApiKey();
|
|
19
|
-
|
|
18
|
+
const apiKey = getApiKey() ?? "";
|
|
19
|
+
const jwt = getJwt() ?? undefined;
|
|
20
|
+
if (!apiKey && !jwt) {
|
|
20
21
|
error("Not authenticated. Run: openhome login");
|
|
21
22
|
process.exit(1);
|
|
22
23
|
}
|
|
23
|
-
client = new ApiClient(apiKey, getConfig().api_base_url);
|
|
24
|
+
client = new ApiClient(apiKey, getConfig().api_base_url, jwt);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
// Fetch abilities to let user pick
|
package/src/commands/init.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { homedir } from "node:os";
|
|
|
10
10
|
import { validateAbility } from "../validation/validator.js";
|
|
11
11
|
import { registerAbility } from "../config/store.js";
|
|
12
12
|
import { success, error, warn, info, p, handleCancel } from "../ui/format.js";
|
|
13
|
+
import { deployCommand } from "./deploy.js";
|
|
13
14
|
|
|
14
15
|
type TemplateType =
|
|
15
16
|
| "basic"
|
|
@@ -889,7 +890,18 @@ export async function initCommand(nameArg?: string): Promise<void> {
|
|
|
889
890
|
warn(`${w.file ? `[${w.file}] ` : ""}${w.message}`);
|
|
890
891
|
}
|
|
891
892
|
|
|
892
|
-
|
|
893
|
+
if (result.passed) {
|
|
894
|
+
const deployNow = await p.confirm({
|
|
895
|
+
message: "Deploy to OpenHome now?",
|
|
896
|
+
initialValue: true,
|
|
897
|
+
});
|
|
898
|
+
handleCancel(deployNow);
|
|
899
|
+
|
|
900
|
+
if (deployNow) {
|
|
901
|
+
await deployCommand(targetDir);
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
893
905
|
|
|
894
|
-
p.outro(`Ability "${name}" is ready
|
|
906
|
+
p.outro(`Ability "${name}" is ready! Run: openhome deploy`);
|
|
895
907
|
}
|
package/src/commands/list.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ApiClient, NotImplementedError } from "../api/client.js";
|
|
2
2
|
import { MockApiClient } from "../api/mock-client.js";
|
|
3
|
-
import { getApiKey, getConfig } from "../config/store.js";
|
|
3
|
+
import { getApiKey, getConfig, getJwt } from "../config/store.js";
|
|
4
4
|
import { error, warn, info, table, p } from "../ui/format.js";
|
|
5
5
|
import type { TableRow } from "../ui/format.js";
|
|
6
6
|
import chalk from "chalk";
|
|
@@ -30,12 +30,13 @@ export async function listCommand(
|
|
|
30
30
|
if (opts.mock) {
|
|
31
31
|
client = new MockApiClient();
|
|
32
32
|
} else {
|
|
33
|
-
const apiKey = getApiKey();
|
|
34
|
-
|
|
33
|
+
const apiKey = getApiKey() ?? "";
|
|
34
|
+
const jwt = getJwt() ?? undefined;
|
|
35
|
+
if (!apiKey && !jwt) {
|
|
35
36
|
error("Not authenticated. Run: openhome login");
|
|
36
37
|
process.exit(1);
|
|
37
38
|
}
|
|
38
|
-
client = new ApiClient(apiKey, getConfig().api_base_url);
|
|
39
|
+
client = new ApiClient(apiKey, getConfig().api_base_url, jwt);
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
const s = p.spinner();
|
package/src/commands/login.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ApiClient } from "../api/client.js";
|
|
2
2
|
import type { Personality } from "../api/contracts.js";
|
|
3
|
-
import { saveApiKey } from "../config/store.js";
|
|
3
|
+
import { saveApiKey, saveJwt } from "../config/store.js";
|
|
4
4
|
import { success, error, info, p, handleCancel } from "../ui/format.js";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
|
|
@@ -38,6 +38,28 @@ export async function loginCommand(): Promise<void> {
|
|
|
38
38
|
saveApiKey(apiKey as string);
|
|
39
39
|
success("API key saved.");
|
|
40
40
|
|
|
41
|
+
// Ask for JWT for full management access
|
|
42
|
+
p.note(
|
|
43
|
+
[
|
|
44
|
+
"Some features (list, toggle, delete, assign) require a session token.",
|
|
45
|
+
"To get it: go to app.openhome.com → open DevTools Console → run:",
|
|
46
|
+
" localStorage.getItem('access_token')",
|
|
47
|
+
].join("\n"),
|
|
48
|
+
"Optional: Session Token",
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const jwtInput = await p.text({
|
|
52
|
+
message: "Paste your session token (or press Enter to skip)",
|
|
53
|
+
placeholder: "eyJhbGci...",
|
|
54
|
+
});
|
|
55
|
+
handleCancel(jwtInput);
|
|
56
|
+
|
|
57
|
+
const jwt = (jwtInput as string | undefined)?.trim();
|
|
58
|
+
if (jwt) {
|
|
59
|
+
saveJwt(jwt);
|
|
60
|
+
success("Session token saved.");
|
|
61
|
+
}
|
|
62
|
+
|
|
41
63
|
// Show agents on this account
|
|
42
64
|
if (agents.length > 0) {
|
|
43
65
|
p.note(
|
package/src/commands/status.ts
CHANGED
|
@@ -105,12 +105,13 @@ export async function statusCommand(
|
|
|
105
105
|
if (opts.mock) {
|
|
106
106
|
client = new MockApiClient();
|
|
107
107
|
} else {
|
|
108
|
-
const apiKey = getApiKey();
|
|
109
|
-
|
|
108
|
+
const apiKey = getApiKey() ?? "";
|
|
109
|
+
const jwt = getJwt() ?? undefined;
|
|
110
|
+
if (!apiKey && !jwt) {
|
|
110
111
|
error("Not authenticated. Run: openhome login");
|
|
111
112
|
process.exit(1);
|
|
112
113
|
}
|
|
113
|
-
client = new ApiClient(apiKey, getConfig().api_base_url);
|
|
114
|
+
client = new ApiClient(apiKey, getConfig().api_base_url, jwt);
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
const s = p.spinner();
|
package/src/commands/toggle.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ApiClient, NotImplementedError } from "../api/client.js";
|
|
2
2
|
import { MockApiClient } from "../api/mock-client.js";
|
|
3
|
-
import { getApiKey, getConfig } from "../config/store.js";
|
|
3
|
+
import { getApiKey, getConfig, getJwt } from "../config/store.js";
|
|
4
4
|
import { error, success, p, handleCancel } from "../ui/format.js";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
|
|
@@ -15,12 +15,13 @@ export async function toggleCommand(
|
|
|
15
15
|
if (opts.mock) {
|
|
16
16
|
client = new MockApiClient();
|
|
17
17
|
} else {
|
|
18
|
-
const apiKey = getApiKey();
|
|
19
|
-
|
|
18
|
+
const apiKey = getApiKey() ?? "";
|
|
19
|
+
const jwt = getJwt() ?? undefined;
|
|
20
|
+
if (!apiKey && !jwt) {
|
|
20
21
|
error("Not authenticated. Run: openhome login");
|
|
21
22
|
process.exit(1);
|
|
22
23
|
}
|
|
23
|
-
client = new ApiClient(apiKey, getConfig().api_base_url);
|
|
24
|
+
client = new ApiClient(apiKey, getConfig().api_base_url, jwt);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
// Fetch abilities
|
package/src/config/store.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface CliConfig {
|
|
|
19
19
|
api_base_url?: string;
|
|
20
20
|
default_personality_id?: string;
|
|
21
21
|
api_key?: string;
|
|
22
|
+
jwt?: string;
|
|
22
23
|
abilities?: TrackedAbility[];
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -129,9 +130,18 @@ export function getTrackedAbilities(): TrackedAbility[] {
|
|
|
129
130
|
export function saveApiKey(key: string): void {
|
|
130
131
|
const saved = keychainSet(key);
|
|
131
132
|
if (!saved) {
|
|
132
|
-
// Fallback: save in config file (less secure)
|
|
133
133
|
const config = getConfig();
|
|
134
134
|
config.api_key = key;
|
|
135
135
|
saveConfig(config);
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
+
|
|
139
|
+
export function getJwt(): string | null {
|
|
140
|
+
return getConfig().jwt ?? null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function saveJwt(jwt: string): void {
|
|
144
|
+
const config = getConfig();
|
|
145
|
+
config.jwt = jwt;
|
|
146
|
+
saveConfig(config);
|
|
147
|
+
}
|