@dayofweek/dcli 1.2.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;
@@ -13,6 +15,7 @@ export declare class DayOfWeekClient {
13
15
  checkAuth(): Promise<{
14
16
  status: string;
15
17
  authenticated: boolean;
18
+ isAdmin?: boolean;
16
19
  }>;
17
20
  listDevices(): Promise<Array<{
18
21
  _id: string;
@@ -27,6 +30,75 @@ export declare class DayOfWeekClient {
27
30
  revokeDevice(deviceId: string): Promise<{
28
31
  success: boolean;
29
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>;
30
102
  listEntities(opts?: {
31
103
  type?: string;
32
104
  parent?: string;
@@ -57,20 +129,108 @@ export declare class DayOfWeekClient {
57
129
  getProposal(proposalId: string): Promise<any>;
58
130
  submitProposal(proposal: Record<string, unknown>): Promise<any>;
59
131
  submitBatch(batch: {
132
+ org?: string;
60
133
  batchLabel: string;
61
134
  sourceAgent?: string;
62
135
  proposals: any[];
63
136
  }): Promise<any>;
64
- getSkillBundle(): Promise<{
137
+ adminListEntities(opts?: {
138
+ org?: string;
139
+ type?: string;
140
+ missingLocation?: boolean;
141
+ search?: string;
142
+ limit?: number;
143
+ }): Promise<{
144
+ results: any[];
145
+ truncated: boolean;
146
+ total: number;
147
+ }>;
148
+ adminListProposals(opts?: {
149
+ status?: string;
150
+ sourceAgent?: string;
151
+ limit?: number;
152
+ }): Promise<{
153
+ results: any[];
154
+ truncated: boolean;
155
+ total: number;
156
+ }>;
157
+ getSkillBundle(name?: string): Promise<{
65
158
  name: string;
66
159
  version: string;
160
+ hash?: string;
67
161
  files: Array<{
68
162
  path: string;
69
163
  content: string;
164
+ sha256?: string;
70
165
  }>;
71
166
  }>;
167
+ listSkillBundles(): Promise<Array<{
168
+ name: string;
169
+ version: string;
170
+ hash: string;
171
+ }>>;
72
172
  getSchema(): Promise<any>;
73
173
  private get;
74
174
  private post;
175
+ private patch;
75
176
  private delete;
177
+ private request;
76
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();
@@ -103,9 +251,40 @@ export class DayOfWeekClient {
103
251
  async submitBatch(batch) {
104
252
  return this.post("/proposals/batch", batch);
105
253
  }
254
+ // ── Admin (cross-org) ─────────────────────────────────────────────────────
255
+ // These endpoints reject non-admin tokens with 401 "Admin access required".
256
+ async adminListEntities(opts) {
257
+ const params = new URLSearchParams();
258
+ if (opts?.org)
259
+ params.set("org", opts.org);
260
+ if (opts?.type)
261
+ params.set("type", opts.type);
262
+ if (opts?.missingLocation)
263
+ params.set("missing-location", "1");
264
+ if (opts?.search)
265
+ params.set("search", opts.search);
266
+ if (opts?.limit)
267
+ params.set("limit", String(opts.limit));
268
+ const qs = params.toString();
269
+ return this.get(`/admin/entities${qs ? `?${qs}` : ""}`);
270
+ }
271
+ async adminListProposals(opts) {
272
+ const params = new URLSearchParams();
273
+ if (opts?.status)
274
+ params.set("status", opts.status);
275
+ if (opts?.sourceAgent)
276
+ params.set("source-agent", opts.sourceAgent);
277
+ if (opts?.limit)
278
+ params.set("limit", String(opts.limit));
279
+ const qs = params.toString();
280
+ return this.get(`/admin/proposals${qs ? `?${qs}` : ""}`);
281
+ }
106
282
  // ── Skill ─────────────────────────────────────────────────────────────────
107
- async getSkillBundle() {
108
- 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");
109
288
  }
110
289
  // ── Schema ────────────────────────────────────────────────────────────────
111
290
  async getSchema() {
@@ -113,55 +292,46 @@ export class DayOfWeekClient {
113
292
  }
114
293
  // ── HTTP helpers ──────────────────────────────────────────────────────────
115
294
  async get(path) {
116
- const res = await fetch(`${this.baseUrl}${path}`, {
117
- headers: { Authorization: `Bearer ${this.token}` },
118
- });
119
- if (!res.ok) {
120
- const body = await res.text();
121
- try {
122
- const json = JSON.parse(body);
123
- throw new ApiError(res.status, json.error ?? body);
124
- }
125
- catch (e) {
126
- if (e instanceof ApiError)
127
- throw e;
128
- throw new ApiError(res.status, body);
129
- }
130
- }
131
- return res.json();
295
+ return this.request(path, { method: "GET" });
132
296
  }
133
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) {
134
307
  const res = await fetch(`${this.baseUrl}${path}`, {
135
- method: "POST",
308
+ ...init,
136
309
  headers: {
137
310
  Authorization: `Bearer ${this.token}`,
138
- "Content-Type": "application/json",
311
+ ...(init.body ? { "Content-Type": "application/json" } : {}),
312
+ ...init.headers,
139
313
  },
140
- body: JSON.stringify(body),
141
314
  });
142
- if (!res.ok) {
143
- const text = await res.text();
144
- try {
145
- const json = JSON.parse(text);
146
- throw new ApiError(res.status, json.error ?? text);
147
- }
148
- catch (e) {
149
- if (e instanceof ApiError)
150
- throw e;
151
- throw new ApiError(res.status, text);
152
- }
153
- }
315
+ if (!res.ok)
316
+ throw await apiErrorFromResponse(res);
154
317
  return res.json();
155
318
  }
156
- async delete(path) {
157
- const res = await fetch(`${this.baseUrl}${path}`, {
158
- method: "DELETE",
159
- headers: { Authorization: `Bearer ${this.token}` },
160
- });
161
- if (!res.ok) {
162
- const text = await res.text();
163
- throw new ApiError(res.status, text);
164
- }
165
- 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})`);
166
336
  }
167
337
  }
package/dist/config.d.ts CHANGED
@@ -1,8 +1,20 @@
1
+ import { type CredentialStore } from "./credentials.js";
1
2
  export interface DcliConfig {
2
3
  authToken?: string;
3
4
  apiUrl?: string;
5
+ /**
6
+ * Cached admin status from the last `dcli auth status` call. Used to
7
+ * decide locally whether to expose admin-only subcommands in `--help`.
8
+ * Refreshed automatically when `dcli auth status` or `dcli auth login`
9
+ * runs; admin commands are hidden when this is unset or false.
10
+ */
11
+ isAdmin?: boolean;
12
+ /** Unix-ms timestamp of the last isAdmin refresh. */
13
+ roleCachedAt?: number;
4
14
  }
5
15
  export declare function loadConfig(): DcliConfig;
6
16
  export declare function saveConfig(updates: Partial<DcliConfig>): void;
7
- 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;
8
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;