@dayofweek/dcli 1.3.0 → 1.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.
package/dist/client.d.ts CHANGED
@@ -4,7 +4,9 @@
4
4
  */
5
5
  export declare class ApiError extends Error {
6
6
  status: number;
7
- constructor(status: number, message: string);
7
+ code?: string | undefined;
8
+ requestId?: string | undefined;
9
+ constructor(status: number, message: string, code?: string | undefined, requestId?: string | undefined);
8
10
  }
9
11
  export declare class DayOfWeekClient {
10
12
  private baseUrl;
@@ -28,6 +30,75 @@ export declare class DayOfWeekClient {
28
30
  revokeDevice(deviceId: string): Promise<{
29
31
  success: boolean;
30
32
  }>;
33
+ revokeCurrentDevice(): Promise<{
34
+ revoked: true;
35
+ }>;
36
+ brainBootstrap(): Promise<{
37
+ authenticated: true;
38
+ scopes: string[];
39
+ entitled: boolean;
40
+ areas: BrainArea[];
41
+ defaultAreaId?: string;
42
+ }>;
43
+ listBrainAreas(): Promise<BrainArea[]>;
44
+ companyBrainStatus(): Promise<{
45
+ eligible: boolean;
46
+ canEnsure: boolean;
47
+ entityName?: string;
48
+ existingArea: BrainArea | null;
49
+ }>;
50
+ ensureCompanyBrain(): Promise<BrainArea>;
51
+ resolveBrain(uri: string): Promise<BrainResolvedResource>;
52
+ getBrainNote(id: string): Promise<BrainNote>;
53
+ getBrainSource(id: string): Promise<BrainSource>;
54
+ uploadBrainSource(input: {
55
+ areaId: string;
56
+ path: string;
57
+ mimeType: string;
58
+ isMeeting: boolean;
59
+ consentAcknowledged: boolean;
60
+ }): Promise<{
61
+ sourceId: string;
62
+ uri: string;
63
+ httpsUrl: string;
64
+ filename: string;
65
+ byteSize: number;
66
+ sha256: string;
67
+ scanState: "pending" | "clean";
68
+ processingState: string;
69
+ }>;
70
+ downloadBrainSource(input: {
71
+ sourceId: string;
72
+ outputPath: string;
73
+ expectedSha256?: string;
74
+ overwrite?: boolean;
75
+ }): Promise<{
76
+ outputPath: string;
77
+ byteSize: number;
78
+ sha256: string;
79
+ }>;
80
+ searchBrain(query: string, options?: {
81
+ areaId?: string;
82
+ limit?: number;
83
+ }): Promise<unknown>;
84
+ shareBrainNote(input: {
85
+ areaId: string;
86
+ title: string;
87
+ markdown: string;
88
+ sourceName?: string;
89
+ intent: "interactive" | "autonomous";
90
+ }): Promise<BrainNote>;
91
+ updateBrainNote(id: string, input: {
92
+ title?: string;
93
+ markdown: string;
94
+ expectedVersion: number;
95
+ }): Promise<BrainNote>;
96
+ archiveBrainNote(id: string, expectedVersion: number): Promise<BrainNote>;
97
+ restoreBrainNote(id: string, expectedVersion: number): Promise<BrainNote>;
98
+ listBrainAudit(areaId: string, options?: {
99
+ cursor?: string;
100
+ limit?: number;
101
+ }): Promise<unknown>;
31
102
  listEntities(opts?: {
32
103
  type?: string;
33
104
  parent?: string;
@@ -58,6 +129,7 @@ export declare class DayOfWeekClient {
58
129
  getProposal(proposalId: string): Promise<any>;
59
130
  submitProposal(proposal: Record<string, unknown>): Promise<any>;
60
131
  submitBatch(batch: {
132
+ org?: string;
61
133
  batchLabel: string;
62
134
  sourceAgent?: string;
63
135
  proposals: any[];
@@ -82,16 +154,83 @@ export declare class DayOfWeekClient {
82
154
  truncated: boolean;
83
155
  total: number;
84
156
  }>;
85
- getSkillBundle(): Promise<{
157
+ getSkillBundle(name?: string): Promise<{
86
158
  name: string;
87
159
  version: string;
160
+ hash?: string;
88
161
  files: Array<{
89
162
  path: string;
90
163
  content: string;
164
+ sha256?: string;
91
165
  }>;
92
166
  }>;
167
+ listSkillBundles(): Promise<Array<{
168
+ name: string;
169
+ version: string;
170
+ hash: string;
171
+ }>>;
93
172
  getSchema(): Promise<any>;
94
173
  private get;
95
174
  private post;
175
+ private patch;
96
176
  private delete;
177
+ private request;
97
178
  }
179
+ export type BrainArea = {
180
+ id: string;
181
+ name: string;
182
+ kind: "company" | "project";
183
+ role: "owner" | "editor" | "viewer";
184
+ canWrite: boolean;
185
+ canManage: boolean;
186
+ uri: string;
187
+ httpsUrl: string;
188
+ updatedAt: number;
189
+ };
190
+ export type BrainNote = {
191
+ id: string;
192
+ areaId: string;
193
+ areaName: string;
194
+ title: string;
195
+ markdown: string;
196
+ status: string;
197
+ version: number;
198
+ hash?: string;
199
+ sourceId?: string;
200
+ sourceUri?: string;
201
+ uri: string;
202
+ httpsUrl: string;
203
+ createdAt: number;
204
+ updatedAt: number;
205
+ };
206
+ export type BrainSource = {
207
+ id: string;
208
+ areaId: string;
209
+ areaName: string;
210
+ kind: "text" | "audio" | "file";
211
+ filename?: string;
212
+ mimeType?: string;
213
+ byteSize?: number;
214
+ sha256?: string;
215
+ scanState?: "pending" | "clean" | "quarantined" | "failed";
216
+ processingState: string;
217
+ rawText?: string;
218
+ extractedText?: string;
219
+ transcript?: string;
220
+ isMeeting: boolean;
221
+ derivedNoteUri?: string;
222
+ canDownload: boolean;
223
+ uri: string;
224
+ httpsUrl: string;
225
+ uploadedAt: number;
226
+ };
227
+ export type BrainResolvedResource = {
228
+ resourceType: "area";
229
+ area: BrainArea;
230
+ } | {
231
+ resourceType: "note";
232
+ note: BrainNote;
233
+ } | {
234
+ resourceType: "source";
235
+ source: BrainSource;
236
+ };
package/dist/client.js CHANGED
@@ -2,12 +2,21 @@
2
2
  * HTTP client for the Day of Week platform REST API.
3
3
  * All calls go through the proxy at field.dayofweek.com/app/api/dcli.
4
4
  */
5
+ import { createHash, randomUUID } from "node:crypto";
6
+ import { createReadStream, createWriteStream, existsSync, fsyncSync, linkSync, openSync, closeSync, renameSync, statSync, unlinkSync } from "node:fs";
7
+ import { basename, dirname, join } from "node:path";
8
+ import { Readable, Transform } from "node:stream";
9
+ import { pipeline } from "node:stream/promises";
5
10
  const DEFAULT_BASE_URL = "https://field.dayofweek.com/app/api/dcli";
6
11
  export class ApiError extends Error {
7
12
  status;
8
- constructor(status, message) {
13
+ code;
14
+ requestId;
15
+ constructor(status, message, code, requestId) {
9
16
  super(message);
10
17
  this.status = status;
18
+ this.code = code;
19
+ this.requestId = requestId;
11
20
  this.name = "ApiError";
12
21
  }
13
22
  }
@@ -31,6 +40,145 @@ export class DayOfWeekClient {
31
40
  async revokeDevice(deviceId) {
32
41
  return this.delete(`/auth/devices/${deviceId}`);
33
42
  }
43
+ async revokeCurrentDevice() {
44
+ return this.delete("/auth/current-device");
45
+ }
46
+ // ── Shared brain ─────────────────────────────────────────────────────────
47
+ async brainBootstrap() {
48
+ return this.get("/brain/bootstrap");
49
+ }
50
+ async listBrainAreas() {
51
+ return this.get("/brain/areas");
52
+ }
53
+ async companyBrainStatus() {
54
+ return this.get("/brain/company-area");
55
+ }
56
+ async ensureCompanyBrain() {
57
+ return this.post("/brain/company-area", {});
58
+ }
59
+ async resolveBrain(uri) {
60
+ return this.get(`/brain/resolve?uri=${encodeURIComponent(uri)}`);
61
+ }
62
+ async getBrainNote(id) {
63
+ return this.get(`/brain/notes/${encodeURIComponent(id)}`);
64
+ }
65
+ async getBrainSource(id) {
66
+ return this.get(`/brain/sources/${encodeURIComponent(id)}`);
67
+ }
68
+ async uploadBrainSource(input) {
69
+ const info = statSync(input.path);
70
+ if (!info.isFile())
71
+ throw new Error("Upload path is not a regular file");
72
+ const sha256 = await hashFile(input.path);
73
+ const idempotencyKey = `up_${createHash("sha256")
74
+ .update(`${input.areaId}:${sha256}:${input.isMeeting}`)
75
+ .digest("base64url")}`;
76
+ const session = await this.post("/brain/source-uploads", {
77
+ areaId: input.areaId,
78
+ filename: basename(input.path),
79
+ mimeType: input.mimeType,
80
+ byteSize: info.size,
81
+ sha256,
82
+ isMeeting: input.isMeeting,
83
+ consentAcknowledged: input.consentAcknowledged,
84
+ idempotencyKey,
85
+ });
86
+ const upload = await fetch(session.uploadUrl, {
87
+ method: "POST",
88
+ headers: { "Content-Type": input.mimeType },
89
+ body: createReadStream(input.path),
90
+ duplex: "half",
91
+ });
92
+ if (!upload.ok)
93
+ throw new ApiError(upload.status, "Source byte upload failed", "network_error");
94
+ const stored = await upload.json();
95
+ if (!stored.storageId)
96
+ throw new Error("Upload did not return a storage ID");
97
+ const completed = await this.post(`/brain/source-uploads/${encodeURIComponent(session.uploadId)}/complete`, {
98
+ storageId: stored.storageId,
99
+ });
100
+ if (completed.sha256 !== sha256)
101
+ throw new Error("Server checksum does not match the uploaded file");
102
+ return completed;
103
+ }
104
+ async downloadBrainSource(input) {
105
+ if (!input.overwrite && existsSync(input.outputPath))
106
+ throw new Error("Output already exists; use --overwrite");
107
+ const response = await fetch(`${this.baseUrl}/brain/sources/${encodeURIComponent(input.sourceId)}/download`, {
108
+ headers: { Authorization: `Bearer ${this.token}` },
109
+ });
110
+ if (!response.ok)
111
+ throw await apiErrorFromResponse(response);
112
+ if (!response.body)
113
+ throw new Error("Download response had no body");
114
+ const expected = response.headers.get("x-dayofweek-sha256") ?? input.expectedSha256;
115
+ if (!expected || !/^[a-f0-9]{64}$/.test(expected))
116
+ throw new Error("Download response omitted its checksum");
117
+ const tempPath = join(dirname(input.outputPath), `.${basename(input.outputPath)}.dcli-${randomUUID()}.tmp`);
118
+ const hash = createHash("sha256");
119
+ let byteSize = 0;
120
+ const hasher = new Transform({
121
+ transform(chunk, _encoding, callback) {
122
+ hash.update(chunk);
123
+ byteSize += chunk.length;
124
+ callback(null, chunk);
125
+ },
126
+ });
127
+ try {
128
+ await pipeline(Readable.fromWeb(response.body), hasher, createWriteStream(tempPath, { flags: "wx", mode: 0o600 }));
129
+ const descriptor = openSync(tempPath, "r");
130
+ try {
131
+ fsyncSync(descriptor);
132
+ }
133
+ finally {
134
+ closeSync(descriptor);
135
+ }
136
+ const actual = hash.digest("hex");
137
+ if (actual !== expected || (input.expectedSha256 && actual !== input.expectedSha256)) {
138
+ throw new Error("Downloaded source checksum mismatch");
139
+ }
140
+ if (input.overwrite) {
141
+ renameSync(tempPath, input.outputPath);
142
+ }
143
+ else {
144
+ linkSync(tempPath, input.outputPath);
145
+ unlinkSync(tempPath);
146
+ }
147
+ return { outputPath: input.outputPath, byteSize, sha256: actual };
148
+ }
149
+ catch (error) {
150
+ if (existsSync(tempPath))
151
+ unlinkSync(tempPath);
152
+ throw error;
153
+ }
154
+ }
155
+ async searchBrain(query, options) {
156
+ return this.post("/brain/search", {
157
+ query,
158
+ areaId: options?.areaId,
159
+ limit: options?.limit,
160
+ });
161
+ }
162
+ async shareBrainNote(input) {
163
+ return this.post("/brain/notes", input);
164
+ }
165
+ async updateBrainNote(id, input) {
166
+ return this.patch(`/brain/notes/${encodeURIComponent(id)}`, input);
167
+ }
168
+ async archiveBrainNote(id, expectedVersion) {
169
+ return this.delete(`/brain/notes/${encodeURIComponent(id)}?expectedVersion=${expectedVersion}`);
170
+ }
171
+ async restoreBrainNote(id, expectedVersion) {
172
+ return this.post(`/brain/notes/${encodeURIComponent(id)}/restore`, { expectedVersion });
173
+ }
174
+ async listBrainAudit(areaId, options) {
175
+ const query = new URLSearchParams({ areaId });
176
+ if (options?.cursor)
177
+ query.set("cursor", options.cursor);
178
+ if (options?.limit)
179
+ query.set("limit", String(options.limit));
180
+ return this.get(`/brain/audit?${query}`);
181
+ }
34
182
  // ── Read ──────────────────────────────────────────────────────────────────
35
183
  async listEntities(opts) {
36
184
  const params = new URLSearchParams();
@@ -132,8 +280,11 @@ export class DayOfWeekClient {
132
280
  return this.get(`/admin/proposals${qs ? `?${qs}` : ""}`);
133
281
  }
134
282
  // ── Skill ─────────────────────────────────────────────────────────────────
135
- async getSkillBundle() {
136
- return this.get("/skill");
283
+ async getSkillBundle(name) {
284
+ return this.get(`/skill${name ? `?name=${encodeURIComponent(name)}` : ""}`);
285
+ }
286
+ async listSkillBundles() {
287
+ return this.get("/skill?list=1");
137
288
  }
138
289
  // ── Schema ────────────────────────────────────────────────────────────────
139
290
  async getSchema() {
@@ -141,55 +292,46 @@ export class DayOfWeekClient {
141
292
  }
142
293
  // ── HTTP helpers ──────────────────────────────────────────────────────────
143
294
  async get(path) {
144
- const res = await fetch(`${this.baseUrl}${path}`, {
145
- headers: { Authorization: `Bearer ${this.token}` },
146
- });
147
- if (!res.ok) {
148
- const body = await res.text();
149
- try {
150
- const json = JSON.parse(body);
151
- throw new ApiError(res.status, json.error ?? body);
152
- }
153
- catch (e) {
154
- if (e instanceof ApiError)
155
- throw e;
156
- throw new ApiError(res.status, body);
157
- }
158
- }
159
- return res.json();
295
+ return this.request(path, { method: "GET" });
160
296
  }
161
297
  async post(path, body) {
298
+ return this.request(path, { method: "POST", body: JSON.stringify(body) });
299
+ }
300
+ async patch(path, body) {
301
+ return this.request(path, { method: "PATCH", body: JSON.stringify(body) });
302
+ }
303
+ async delete(path) {
304
+ return this.request(path, { method: "DELETE" });
305
+ }
306
+ async request(path, init) {
162
307
  const res = await fetch(`${this.baseUrl}${path}`, {
163
- method: "POST",
308
+ ...init,
164
309
  headers: {
165
310
  Authorization: `Bearer ${this.token}`,
166
- "Content-Type": "application/json",
311
+ ...(init.body ? { "Content-Type": "application/json" } : {}),
312
+ ...init.headers,
167
313
  },
168
- body: JSON.stringify(body),
169
314
  });
170
- if (!res.ok) {
171
- const text = await res.text();
172
- try {
173
- const json = JSON.parse(text);
174
- throw new ApiError(res.status, json.error ?? text);
175
- }
176
- catch (e) {
177
- if (e instanceof ApiError)
178
- throw e;
179
- throw new ApiError(res.status, text);
180
- }
181
- }
315
+ if (!res.ok)
316
+ throw await apiErrorFromResponse(res);
182
317
  return res.json();
183
318
  }
184
- async delete(path) {
185
- const res = await fetch(`${this.baseUrl}${path}`, {
186
- method: "DELETE",
187
- headers: { Authorization: `Bearer ${this.token}` },
188
- });
189
- if (!res.ok) {
190
- const text = await res.text();
191
- throw new ApiError(res.status, text);
192
- }
193
- return res.json();
319
+ }
320
+ async function hashFile(path) {
321
+ const hash = createHash("sha256");
322
+ for await (const chunk of createReadStream(path))
323
+ hash.update(chunk);
324
+ return hash.digest("hex");
325
+ }
326
+ async function apiErrorFromResponse(response) {
327
+ const body = await response.text();
328
+ try {
329
+ const json = JSON.parse(body);
330
+ if (typeof json.error === "string")
331
+ return new ApiError(response.status, json.error);
332
+ return new ApiError(response.status, json.error?.message ?? `Request failed (${response.status})`, json.error?.code, json.error?.requestId);
333
+ }
334
+ catch {
335
+ return new ApiError(response.status, `Request failed (${response.status})`);
194
336
  }
195
337
  }
package/dist/config.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type CredentialStore } from "./credentials.js";
1
2
  export interface DcliConfig {
2
3
  authToken?: string;
3
4
  apiUrl?: string;
@@ -13,5 +14,7 @@ export interface DcliConfig {
13
14
  }
14
15
  export declare function loadConfig(): DcliConfig;
15
16
  export declare function saveConfig(updates: Partial<DcliConfig>): void;
16
- export declare function getToken(): string;
17
+ export declare function getToken(store?: CredentialStore): string;
18
+ export declare function saveCredential(secret: string, store?: CredentialStore): void;
19
+ export declare function deleteCredential(store?: CredentialStore): void;
17
20
  export declare function getApiUrl(): string;
package/dist/config.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
+ import { defaultCredentialStore } from "./credentials.js";
4
5
  const CONFIG_DIR = join(homedir(), ".config", "dayofweek");
5
6
  const CONFIG_FILE = join(CONFIG_DIR, "dcli.json");
6
7
  export function loadConfig() {
@@ -15,22 +16,39 @@ export function loadConfig() {
15
16
  return {};
16
17
  }
17
18
  export function saveConfig(updates) {
18
- mkdirSync(CONFIG_DIR, { recursive: true });
19
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
19
20
  const existing = loadConfig();
20
21
  const merged = { ...existing, ...updates };
21
- writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), "utf-8");
22
+ writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), { encoding: "utf-8", mode: 0o600 });
22
23
  }
23
- export function getToken() {
24
+ export function getToken(store = defaultCredentialStore()) {
25
+ const config = loadConfig();
26
+ const storedToken = store.get();
24
27
  const token = process.env.DCLI_AUTH_TOKEN ??
25
28
  process.env.DCLI_TOKEN ??
26
- loadConfig().authToken;
29
+ storedToken ??
30
+ config.authToken;
31
+ // One-way migration of the old plaintext config. The legacy key is removed
32
+ // immediately after the protected store accepts it.
33
+ if (!storedToken && config.authToken && token === config.authToken) {
34
+ store.set(config.authToken);
35
+ saveConfig({ authToken: undefined });
36
+ }
27
37
  if (!token) {
28
- console.error("No auth token found.");
29
- console.error("Set DCLI_AUTH_TOKEN or run: dcli auth login");
30
- process.exit(1);
38
+ throw new Error("No credential found. Run: dcli auth login");
31
39
  }
32
40
  return token;
33
41
  }
42
+ export function saveCredential(secret, store = defaultCredentialStore()) {
43
+ store.set(secret);
44
+ if (loadConfig().authToken)
45
+ saveConfig({ authToken: undefined });
46
+ }
47
+ export function deleteCredential(store = defaultCredentialStore()) {
48
+ store.delete();
49
+ if (loadConfig().authToken)
50
+ saveConfig({ authToken: undefined });
51
+ }
34
52
  export function getApiUrl() {
35
53
  return (process.env.DCLI_API_URL ??
36
54
  loadConfig().apiUrl ??
@@ -0,0 +1,13 @@
1
+ export interface CredentialStore {
2
+ get(): string | undefined;
3
+ set(secret: string): void;
4
+ delete(): void;
5
+ }
6
+ export declare class ProtectedFileCredentialStore implements CredentialStore {
7
+ readonly path: string;
8
+ constructor(path?: string);
9
+ get(): string | undefined;
10
+ set(secret: string): void;
11
+ delete(): void;
12
+ }
13
+ export declare function defaultCredentialStore(): CredentialStore;
@@ -0,0 +1,113 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ import { homedir, platform, userInfo } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ const SERVICE = "com.dayofweek.dcli";
6
+ const ACCOUNT = "brain-device";
7
+ export class ProtectedFileCredentialStore {
8
+ path;
9
+ constructor(path = join(homedir(), ".config", "dayofweek", "credentials.json")) {
10
+ this.path = path;
11
+ }
12
+ get() {
13
+ if (!existsSync(this.path))
14
+ return undefined;
15
+ if (platform() !== "win32" && (statSync(this.path).mode & 0o077) !== 0) {
16
+ throw new Error(`Credential file permissions are unsafe: ${this.path}`);
17
+ }
18
+ const value = JSON.parse(readFileSync(this.path, "utf8"));
19
+ return typeof value.deviceSecret === "string" ? value.deviceSecret : undefined;
20
+ }
21
+ set(secret) {
22
+ if (!/^dsk_[A-Za-z0-9_-]{20,160}$/.test(secret))
23
+ throw new Error("Refusing to store an invalid credential");
24
+ const directory = dirname(this.path);
25
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
26
+ if (platform() !== "win32")
27
+ chmodSync(directory, 0o700);
28
+ writeFileSync(this.path, JSON.stringify({ deviceSecret: secret }), {
29
+ encoding: "utf8",
30
+ mode: 0o600,
31
+ flag: "w",
32
+ });
33
+ if (platform() !== "win32") {
34
+ chmodSync(this.path, 0o600);
35
+ if ((statSync(this.path).mode & 0o077) !== 0)
36
+ throw new Error("Could not secure credential file");
37
+ }
38
+ else {
39
+ const username = userInfo().username;
40
+ execFileSync("icacls", [this.path, "/inheritance:r", "/grant:r", `${username}:(R,W)`], {
41
+ stdio: "ignore",
42
+ });
43
+ }
44
+ }
45
+ delete() {
46
+ if (existsSync(this.path))
47
+ unlinkSync(this.path);
48
+ }
49
+ }
50
+ class MacKeychainCredentialStore {
51
+ get() {
52
+ try {
53
+ return execFileSync("security", ["find-generic-password", "-s", SERVICE, "-a", ACCOUNT, "-w"], {
54
+ encoding: "utf8",
55
+ stdio: ["ignore", "pipe", "ignore"],
56
+ }).trim() || undefined;
57
+ }
58
+ catch {
59
+ return undefined;
60
+ }
61
+ }
62
+ set(secret) {
63
+ execFileSync("security", ["add-generic-password", "-U", "-s", SERVICE, "-a", ACCOUNT, "-w", secret], { stdio: "ignore" });
64
+ }
65
+ delete() {
66
+ try {
67
+ execFileSync("security", ["delete-generic-password", "-s", SERVICE, "-a", ACCOUNT], {
68
+ stdio: "ignore",
69
+ });
70
+ }
71
+ catch {
72
+ // Missing credential is already logged out.
73
+ }
74
+ }
75
+ }
76
+ class LinuxSecretServiceCredentialStore {
77
+ get() {
78
+ try {
79
+ return execFileSync("secret-tool", ["lookup", "service", SERVICE, "account", ACCOUNT], {
80
+ encoding: "utf8",
81
+ stdio: ["ignore", "pipe", "ignore"],
82
+ }).trim() || undefined;
83
+ }
84
+ catch {
85
+ return undefined;
86
+ }
87
+ }
88
+ set(secret) {
89
+ execFileSync("secret-tool", ["store", "--label", "Day of Week device", "service", SERVICE, "account", ACCOUNT], { input: secret, stdio: ["pipe", "ignore", "ignore"] });
90
+ }
91
+ delete() {
92
+ try {
93
+ execFileSync("secret-tool", ["clear", "service", SERVICE, "account", ACCOUNT], { stdio: "ignore" });
94
+ }
95
+ catch {
96
+ // Missing credential is already logged out.
97
+ }
98
+ }
99
+ }
100
+ export function defaultCredentialStore() {
101
+ if (platform() === "darwin")
102
+ return new MacKeychainCredentialStore();
103
+ if (platform() === "linux") {
104
+ try {
105
+ execFileSync("secret-tool", ["--version"], { stdio: "ignore" });
106
+ return new LinuxSecretServiceCredentialStore();
107
+ }
108
+ catch {
109
+ return new ProtectedFileCredentialStore();
110
+ }
111
+ }
112
+ return new ProtectedFileCredentialStore();
113
+ }
@@ -0,0 +1,16 @@
1
+ export type SkillBundle = {
2
+ name: string;
3
+ version: string;
4
+ hash?: string;
5
+ files: Array<{
6
+ path: string;
7
+ content: string;
8
+ sha256?: string;
9
+ }>;
10
+ };
11
+ export declare function validateSkillBundle(bundle: SkillBundle): SkillBundle;
12
+ export declare function writeSkillBundle(input: SkillBundle, targetDir: string): {
13
+ written: number;
14
+ unchanged: number;
15
+ conflicts: string[];
16
+ };