@dayofweek/dcli 1.3.0 → 1.5.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 +107 -54
- package/dist/auth/login.d.ts +18 -0
- package/dist/auth/login.js +47 -0
- package/dist/auth/loopback.d.ts +6 -0
- package/dist/auth/loopback.js +59 -0
- package/dist/auth/pkce.d.ts +14 -0
- package/dist/auth/pkce.js +87 -0
- package/dist/bin/dcli.js +576 -58
- package/dist/client.d.ts +205 -2
- package/dist/client.js +280 -44
- package/dist/config.d.ts +4 -1
- package/dist/config.js +25 -7
- package/dist/credentials.d.ts +13 -0
- package/dist/credentials.js +113 -0
- package/dist/skills.d.ts +16 -0
- package/dist/skills.js +77 -0
- package/dist/uri.d.ts +9 -0
- package/dist/uri.js +72 -0
- package/package.json +9 -2
package/dist/client.d.ts
CHANGED
|
@@ -2,9 +2,16 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* Node's Buffer is a Uint8Array view over a possibly-larger, possibly-shared
|
|
7
|
+
* backing store, which is not assignable to BodyInit. Copy out the exact bytes.
|
|
8
|
+
*/
|
|
9
|
+
export declare function toArrayBuffer(view: Uint8Array): ArrayBuffer;
|
|
5
10
|
export declare class ApiError extends Error {
|
|
6
11
|
status: number;
|
|
7
|
-
|
|
12
|
+
code?: string | undefined;
|
|
13
|
+
requestId?: string | undefined;
|
|
14
|
+
constructor(status: number, message: string, code?: string | undefined, requestId?: string | undefined);
|
|
8
15
|
}
|
|
9
16
|
export declare class DayOfWeekClient {
|
|
10
17
|
private baseUrl;
|
|
@@ -28,6 +35,75 @@ export declare class DayOfWeekClient {
|
|
|
28
35
|
revokeDevice(deviceId: string): Promise<{
|
|
29
36
|
success: boolean;
|
|
30
37
|
}>;
|
|
38
|
+
revokeCurrentDevice(): Promise<{
|
|
39
|
+
revoked: true;
|
|
40
|
+
}>;
|
|
41
|
+
brainBootstrap(): Promise<{
|
|
42
|
+
authenticated: true;
|
|
43
|
+
scopes: string[];
|
|
44
|
+
entitled: boolean;
|
|
45
|
+
areas: BrainArea[];
|
|
46
|
+
defaultAreaId?: string;
|
|
47
|
+
}>;
|
|
48
|
+
listBrainAreas(): Promise<BrainArea[]>;
|
|
49
|
+
companyBrainStatus(): Promise<{
|
|
50
|
+
eligible: boolean;
|
|
51
|
+
canEnsure: boolean;
|
|
52
|
+
entityName?: string;
|
|
53
|
+
existingArea: BrainArea | null;
|
|
54
|
+
}>;
|
|
55
|
+
ensureCompanyBrain(): Promise<BrainArea>;
|
|
56
|
+
resolveBrain(uri: string): Promise<BrainResolvedResource>;
|
|
57
|
+
getBrainNote(id: string): Promise<BrainNote>;
|
|
58
|
+
getBrainSource(id: string): Promise<BrainSource>;
|
|
59
|
+
uploadBrainSource(input: {
|
|
60
|
+
areaId: string;
|
|
61
|
+
path: string;
|
|
62
|
+
mimeType: string;
|
|
63
|
+
isMeeting: boolean;
|
|
64
|
+
consentAcknowledged: boolean;
|
|
65
|
+
}): Promise<{
|
|
66
|
+
sourceId: string;
|
|
67
|
+
uri: string;
|
|
68
|
+
httpsUrl: string;
|
|
69
|
+
filename: string;
|
|
70
|
+
byteSize: number;
|
|
71
|
+
sha256: string;
|
|
72
|
+
scanState: "pending" | "clean";
|
|
73
|
+
processingState: string;
|
|
74
|
+
}>;
|
|
75
|
+
downloadBrainSource(input: {
|
|
76
|
+
sourceId: string;
|
|
77
|
+
outputPath: string;
|
|
78
|
+
expectedSha256?: string;
|
|
79
|
+
overwrite?: boolean;
|
|
80
|
+
}): Promise<{
|
|
81
|
+
outputPath: string;
|
|
82
|
+
byteSize: number;
|
|
83
|
+
sha256: string;
|
|
84
|
+
}>;
|
|
85
|
+
searchBrain(query: string, options?: {
|
|
86
|
+
areaId?: string;
|
|
87
|
+
limit?: number;
|
|
88
|
+
}): Promise<unknown>;
|
|
89
|
+
shareBrainNote(input: {
|
|
90
|
+
areaId: string;
|
|
91
|
+
title: string;
|
|
92
|
+
markdown: string;
|
|
93
|
+
sourceName?: string;
|
|
94
|
+
intent: "interactive" | "autonomous";
|
|
95
|
+
}): Promise<BrainNote>;
|
|
96
|
+
updateBrainNote(id: string, input: {
|
|
97
|
+
title?: string;
|
|
98
|
+
markdown: string;
|
|
99
|
+
expectedVersion: number;
|
|
100
|
+
}): Promise<BrainNote>;
|
|
101
|
+
archiveBrainNote(id: string, expectedVersion: number): Promise<BrainNote>;
|
|
102
|
+
restoreBrainNote(id: string, expectedVersion: number): Promise<BrainNote>;
|
|
103
|
+
listBrainAudit(areaId: string, options?: {
|
|
104
|
+
cursor?: string;
|
|
105
|
+
limit?: number;
|
|
106
|
+
}): Promise<unknown>;
|
|
31
107
|
listEntities(opts?: {
|
|
32
108
|
type?: string;
|
|
33
109
|
parent?: string;
|
|
@@ -58,6 +134,7 @@ export declare class DayOfWeekClient {
|
|
|
58
134
|
getProposal(proposalId: string): Promise<any>;
|
|
59
135
|
submitProposal(proposal: Record<string, unknown>): Promise<any>;
|
|
60
136
|
submitBatch(batch: {
|
|
137
|
+
org?: string;
|
|
61
138
|
batchLabel: string;
|
|
62
139
|
sourceAgent?: string;
|
|
63
140
|
proposals: any[];
|
|
@@ -82,16 +159,142 @@ export declare class DayOfWeekClient {
|
|
|
82
159
|
truncated: boolean;
|
|
83
160
|
total: number;
|
|
84
161
|
}>;
|
|
85
|
-
|
|
162
|
+
/**
|
|
163
|
+
* List the knowledge documents attached to an entity. `full` returns each
|
|
164
|
+
* document's whole content instead of an excerpt, which is what you want when
|
|
165
|
+
* mirroring an entity's sources into an external knowledge base.
|
|
166
|
+
*/
|
|
167
|
+
listKnowledge(opts: {
|
|
168
|
+
entity: string;
|
|
169
|
+
full?: boolean;
|
|
170
|
+
org?: string;
|
|
171
|
+
}): Promise<any[]>;
|
|
172
|
+
getKnowledge(documentId: string, org?: string): Promise<any>;
|
|
173
|
+
/** Semantic search across knowledge documents. */
|
|
174
|
+
searchKnowledge(opts: {
|
|
175
|
+
query: string;
|
|
176
|
+
entity?: string;
|
|
177
|
+
types?: string[];
|
|
178
|
+
limit?: number;
|
|
179
|
+
allOrgs?: boolean;
|
|
180
|
+
org?: string;
|
|
181
|
+
}): Promise<any>;
|
|
182
|
+
/**
|
|
183
|
+
* Add a markdown knowledge note.
|
|
184
|
+
*
|
|
185
|
+
* Without `direct` this submits a proposal for human review — the default,
|
|
186
|
+
* and the only option non-admin tokens have. With `direct: true` an admin
|
|
187
|
+
* token writes the document immediately, skipping review. Ask the operator
|
|
188
|
+
* before doing that; see references/admin.md in the served skill.
|
|
189
|
+
*/
|
|
190
|
+
addKnowledge(input: {
|
|
191
|
+
entityId: string;
|
|
192
|
+
title: string;
|
|
193
|
+
content: string;
|
|
194
|
+
sourceType?: string;
|
|
195
|
+
sourceUrl?: string;
|
|
196
|
+
sourceDescription?: string;
|
|
197
|
+
confidence?: number;
|
|
198
|
+
sourceAgent?: string;
|
|
199
|
+
direct?: boolean;
|
|
200
|
+
org?: string;
|
|
201
|
+
}): Promise<any>;
|
|
202
|
+
/**
|
|
203
|
+
* Attach a binary source (PDF, DOCX, XLSX, …) to an entity. Admin only.
|
|
204
|
+
*
|
|
205
|
+
* Three hops: ask for an upload URL, send the bytes straight to Convex
|
|
206
|
+
* storage, then register the resulting storageId. The bytes never pass
|
|
207
|
+
* through function arguments, so file size isn't bounded by an arg limit.
|
|
208
|
+
*/
|
|
209
|
+
attachKnowledgeFile(input: {
|
|
210
|
+
entityId: string;
|
|
211
|
+
/** Raw file bytes. Buffer callers: pass `toArrayBuffer(buf)` below. */
|
|
212
|
+
data: ArrayBuffer;
|
|
213
|
+
fileName: string;
|
|
214
|
+
mimeType: string;
|
|
215
|
+
sourceType?: string;
|
|
216
|
+
sourceUrl?: string;
|
|
217
|
+
sourceDescription?: string;
|
|
218
|
+
org?: string;
|
|
219
|
+
}): Promise<any>;
|
|
220
|
+
getSkillBundle(name?: string): Promise<{
|
|
86
221
|
name: string;
|
|
87
222
|
version: string;
|
|
223
|
+
hash?: string;
|
|
88
224
|
files: Array<{
|
|
89
225
|
path: string;
|
|
90
226
|
content: string;
|
|
227
|
+
sha256?: string;
|
|
91
228
|
}>;
|
|
92
229
|
}>;
|
|
230
|
+
listSkillBundles(): Promise<Array<{
|
|
231
|
+
name: string;
|
|
232
|
+
version: string;
|
|
233
|
+
hash: string;
|
|
234
|
+
}>>;
|
|
93
235
|
getSchema(): Promise<any>;
|
|
94
236
|
private get;
|
|
95
237
|
private post;
|
|
238
|
+
private put;
|
|
239
|
+
private patch;
|
|
96
240
|
private delete;
|
|
241
|
+
private request;
|
|
97
242
|
}
|
|
243
|
+
export type BrainArea = {
|
|
244
|
+
id: string;
|
|
245
|
+
name: string;
|
|
246
|
+
kind: "company" | "project";
|
|
247
|
+
role: "owner" | "editor" | "viewer";
|
|
248
|
+
canWrite: boolean;
|
|
249
|
+
canManage: boolean;
|
|
250
|
+
uri: string;
|
|
251
|
+
httpsUrl: string;
|
|
252
|
+
updatedAt: number;
|
|
253
|
+
};
|
|
254
|
+
export type BrainNote = {
|
|
255
|
+
id: string;
|
|
256
|
+
areaId: string;
|
|
257
|
+
areaName: string;
|
|
258
|
+
title: string;
|
|
259
|
+
markdown: string;
|
|
260
|
+
status: string;
|
|
261
|
+
version: number;
|
|
262
|
+
hash?: string;
|
|
263
|
+
sourceId?: string;
|
|
264
|
+
sourceUri?: string;
|
|
265
|
+
uri: string;
|
|
266
|
+
httpsUrl: string;
|
|
267
|
+
createdAt: number;
|
|
268
|
+
updatedAt: number;
|
|
269
|
+
};
|
|
270
|
+
export type BrainSource = {
|
|
271
|
+
id: string;
|
|
272
|
+
areaId: string;
|
|
273
|
+
areaName: string;
|
|
274
|
+
kind: "text" | "audio" | "file";
|
|
275
|
+
filename?: string;
|
|
276
|
+
mimeType?: string;
|
|
277
|
+
byteSize?: number;
|
|
278
|
+
sha256?: string;
|
|
279
|
+
scanState?: "pending" | "clean" | "quarantined" | "failed";
|
|
280
|
+
processingState: string;
|
|
281
|
+
rawText?: string;
|
|
282
|
+
extractedText?: string;
|
|
283
|
+
transcript?: string;
|
|
284
|
+
isMeeting: boolean;
|
|
285
|
+
derivedNoteUri?: string;
|
|
286
|
+
canDownload: boolean;
|
|
287
|
+
uri: string;
|
|
288
|
+
httpsUrl: string;
|
|
289
|
+
uploadedAt: number;
|
|
290
|
+
};
|
|
291
|
+
export type BrainResolvedResource = {
|
|
292
|
+
resourceType: "area";
|
|
293
|
+
area: BrainArea;
|
|
294
|
+
} | {
|
|
295
|
+
resourceType: "note";
|
|
296
|
+
note: BrainNote;
|
|
297
|
+
} | {
|
|
298
|
+
resourceType: "source";
|
|
299
|
+
source: BrainSource;
|
|
300
|
+
};
|
package/dist/client.js
CHANGED
|
@@ -2,12 +2,30 @@
|
|
|
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";
|
|
11
|
+
/**
|
|
12
|
+
* Node's Buffer is a Uint8Array view over a possibly-larger, possibly-shared
|
|
13
|
+
* backing store, which is not assignable to BodyInit. Copy out the exact bytes.
|
|
14
|
+
*/
|
|
15
|
+
export function toArrayBuffer(view) {
|
|
16
|
+
const out = new ArrayBuffer(view.byteLength);
|
|
17
|
+
new Uint8Array(out).set(view);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
6
20
|
export class ApiError extends Error {
|
|
7
21
|
status;
|
|
8
|
-
|
|
22
|
+
code;
|
|
23
|
+
requestId;
|
|
24
|
+
constructor(status, message, code, requestId) {
|
|
9
25
|
super(message);
|
|
10
26
|
this.status = status;
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.requestId = requestId;
|
|
11
29
|
this.name = "ApiError";
|
|
12
30
|
}
|
|
13
31
|
}
|
|
@@ -31,6 +49,145 @@ export class DayOfWeekClient {
|
|
|
31
49
|
async revokeDevice(deviceId) {
|
|
32
50
|
return this.delete(`/auth/devices/${deviceId}`);
|
|
33
51
|
}
|
|
52
|
+
async revokeCurrentDevice() {
|
|
53
|
+
return this.delete("/auth/current-device");
|
|
54
|
+
}
|
|
55
|
+
// ── Shared brain ─────────────────────────────────────────────────────────
|
|
56
|
+
async brainBootstrap() {
|
|
57
|
+
return this.get("/brain/bootstrap");
|
|
58
|
+
}
|
|
59
|
+
async listBrainAreas() {
|
|
60
|
+
return this.get("/brain/areas");
|
|
61
|
+
}
|
|
62
|
+
async companyBrainStatus() {
|
|
63
|
+
return this.get("/brain/company-area");
|
|
64
|
+
}
|
|
65
|
+
async ensureCompanyBrain() {
|
|
66
|
+
return this.post("/brain/company-area", {});
|
|
67
|
+
}
|
|
68
|
+
async resolveBrain(uri) {
|
|
69
|
+
return this.get(`/brain/resolve?uri=${encodeURIComponent(uri)}`);
|
|
70
|
+
}
|
|
71
|
+
async getBrainNote(id) {
|
|
72
|
+
return this.get(`/brain/notes/${encodeURIComponent(id)}`);
|
|
73
|
+
}
|
|
74
|
+
async getBrainSource(id) {
|
|
75
|
+
return this.get(`/brain/sources/${encodeURIComponent(id)}`);
|
|
76
|
+
}
|
|
77
|
+
async uploadBrainSource(input) {
|
|
78
|
+
const info = statSync(input.path);
|
|
79
|
+
if (!info.isFile())
|
|
80
|
+
throw new Error("Upload path is not a regular file");
|
|
81
|
+
const sha256 = await hashFile(input.path);
|
|
82
|
+
const idempotencyKey = `up_${createHash("sha256")
|
|
83
|
+
.update(`${input.areaId}:${sha256}:${input.isMeeting}`)
|
|
84
|
+
.digest("base64url")}`;
|
|
85
|
+
const session = await this.post("/brain/source-uploads", {
|
|
86
|
+
areaId: input.areaId,
|
|
87
|
+
filename: basename(input.path),
|
|
88
|
+
mimeType: input.mimeType,
|
|
89
|
+
byteSize: info.size,
|
|
90
|
+
sha256,
|
|
91
|
+
isMeeting: input.isMeeting,
|
|
92
|
+
consentAcknowledged: input.consentAcknowledged,
|
|
93
|
+
idempotencyKey,
|
|
94
|
+
});
|
|
95
|
+
const upload = await fetch(session.uploadUrl, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: { "Content-Type": input.mimeType },
|
|
98
|
+
body: createReadStream(input.path),
|
|
99
|
+
duplex: "half",
|
|
100
|
+
});
|
|
101
|
+
if (!upload.ok)
|
|
102
|
+
throw new ApiError(upload.status, "Source byte upload failed", "network_error");
|
|
103
|
+
const stored = await upload.json();
|
|
104
|
+
if (!stored.storageId)
|
|
105
|
+
throw new Error("Upload did not return a storage ID");
|
|
106
|
+
const completed = await this.post(`/brain/source-uploads/${encodeURIComponent(session.uploadId)}/complete`, {
|
|
107
|
+
storageId: stored.storageId,
|
|
108
|
+
});
|
|
109
|
+
if (completed.sha256 !== sha256)
|
|
110
|
+
throw new Error("Server checksum does not match the uploaded file");
|
|
111
|
+
return completed;
|
|
112
|
+
}
|
|
113
|
+
async downloadBrainSource(input) {
|
|
114
|
+
if (!input.overwrite && existsSync(input.outputPath))
|
|
115
|
+
throw new Error("Output already exists; use --overwrite");
|
|
116
|
+
const response = await fetch(`${this.baseUrl}/brain/sources/${encodeURIComponent(input.sourceId)}/download`, {
|
|
117
|
+
headers: { Authorization: `Bearer ${this.token}` },
|
|
118
|
+
});
|
|
119
|
+
if (!response.ok)
|
|
120
|
+
throw await apiErrorFromResponse(response);
|
|
121
|
+
if (!response.body)
|
|
122
|
+
throw new Error("Download response had no body");
|
|
123
|
+
const expected = response.headers.get("x-dayofweek-sha256") ?? input.expectedSha256;
|
|
124
|
+
if (!expected || !/^[a-f0-9]{64}$/.test(expected))
|
|
125
|
+
throw new Error("Download response omitted its checksum");
|
|
126
|
+
const tempPath = join(dirname(input.outputPath), `.${basename(input.outputPath)}.dcli-${randomUUID()}.tmp`);
|
|
127
|
+
const hash = createHash("sha256");
|
|
128
|
+
let byteSize = 0;
|
|
129
|
+
const hasher = new Transform({
|
|
130
|
+
transform(chunk, _encoding, callback) {
|
|
131
|
+
hash.update(chunk);
|
|
132
|
+
byteSize += chunk.length;
|
|
133
|
+
callback(null, chunk);
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
try {
|
|
137
|
+
await pipeline(Readable.fromWeb(response.body), hasher, createWriteStream(tempPath, { flags: "wx", mode: 0o600 }));
|
|
138
|
+
const descriptor = openSync(tempPath, "r");
|
|
139
|
+
try {
|
|
140
|
+
fsyncSync(descriptor);
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
closeSync(descriptor);
|
|
144
|
+
}
|
|
145
|
+
const actual = hash.digest("hex");
|
|
146
|
+
if (actual !== expected || (input.expectedSha256 && actual !== input.expectedSha256)) {
|
|
147
|
+
throw new Error("Downloaded source checksum mismatch");
|
|
148
|
+
}
|
|
149
|
+
if (input.overwrite) {
|
|
150
|
+
renameSync(tempPath, input.outputPath);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
linkSync(tempPath, input.outputPath);
|
|
154
|
+
unlinkSync(tempPath);
|
|
155
|
+
}
|
|
156
|
+
return { outputPath: input.outputPath, byteSize, sha256: actual };
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (existsSync(tempPath))
|
|
160
|
+
unlinkSync(tempPath);
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async searchBrain(query, options) {
|
|
165
|
+
return this.post("/brain/search", {
|
|
166
|
+
query,
|
|
167
|
+
areaId: options?.areaId,
|
|
168
|
+
limit: options?.limit,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
async shareBrainNote(input) {
|
|
172
|
+
return this.post("/brain/notes", input);
|
|
173
|
+
}
|
|
174
|
+
async updateBrainNote(id, input) {
|
|
175
|
+
return this.patch(`/brain/notes/${encodeURIComponent(id)}`, input);
|
|
176
|
+
}
|
|
177
|
+
async archiveBrainNote(id, expectedVersion) {
|
|
178
|
+
return this.delete(`/brain/notes/${encodeURIComponent(id)}?expectedVersion=${expectedVersion}`);
|
|
179
|
+
}
|
|
180
|
+
async restoreBrainNote(id, expectedVersion) {
|
|
181
|
+
return this.post(`/brain/notes/${encodeURIComponent(id)}/restore`, { expectedVersion });
|
|
182
|
+
}
|
|
183
|
+
async listBrainAudit(areaId, options) {
|
|
184
|
+
const query = new URLSearchParams({ areaId });
|
|
185
|
+
if (options?.cursor)
|
|
186
|
+
query.set("cursor", options.cursor);
|
|
187
|
+
if (options?.limit)
|
|
188
|
+
query.set("limit", String(options.limit));
|
|
189
|
+
return this.get(`/brain/audit?${query}`);
|
|
190
|
+
}
|
|
34
191
|
// ── Read ──────────────────────────────────────────────────────────────────
|
|
35
192
|
async listEntities(opts) {
|
|
36
193
|
const params = new URLSearchParams();
|
|
@@ -131,9 +288,94 @@ export class DayOfWeekClient {
|
|
|
131
288
|
const qs = params.toString();
|
|
132
289
|
return this.get(`/admin/proposals${qs ? `?${qs}` : ""}`);
|
|
133
290
|
}
|
|
291
|
+
// ── Knowledge ─────────────────────────────────────────────────────────────
|
|
292
|
+
/**
|
|
293
|
+
* List the knowledge documents attached to an entity. `full` returns each
|
|
294
|
+
* document's whole content instead of an excerpt, which is what you want when
|
|
295
|
+
* mirroring an entity's sources into an external knowledge base.
|
|
296
|
+
*/
|
|
297
|
+
async listKnowledge(opts) {
|
|
298
|
+
const params = new URLSearchParams({ entity: opts.entity });
|
|
299
|
+
if (opts.full)
|
|
300
|
+
params.set("full", "1");
|
|
301
|
+
if (opts.org)
|
|
302
|
+
params.set("org", opts.org);
|
|
303
|
+
return this.get(`/knowledge?${params.toString()}`);
|
|
304
|
+
}
|
|
305
|
+
async getKnowledge(documentId, org) {
|
|
306
|
+
const params = new URLSearchParams({ document: documentId });
|
|
307
|
+
if (org)
|
|
308
|
+
params.set("org", org);
|
|
309
|
+
return this.get(`/knowledge?${params.toString()}`);
|
|
310
|
+
}
|
|
311
|
+
/** Semantic search across knowledge documents. */
|
|
312
|
+
async searchKnowledge(opts) {
|
|
313
|
+
const params = new URLSearchParams({ q: opts.query });
|
|
314
|
+
if (opts.entity)
|
|
315
|
+
params.set("entity", opts.entity);
|
|
316
|
+
if (opts.types?.length)
|
|
317
|
+
params.set("types", opts.types.join(","));
|
|
318
|
+
if (opts.limit)
|
|
319
|
+
params.set("limit", String(opts.limit));
|
|
320
|
+
if (opts.allOrgs)
|
|
321
|
+
params.set("allOrgs", "1");
|
|
322
|
+
if (opts.org)
|
|
323
|
+
params.set("org", opts.org);
|
|
324
|
+
return this.get(`/knowledge?${params.toString()}`);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Add a markdown knowledge note.
|
|
328
|
+
*
|
|
329
|
+
* Without `direct` this submits a proposal for human review — the default,
|
|
330
|
+
* and the only option non-admin tokens have. With `direct: true` an admin
|
|
331
|
+
* token writes the document immediately, skipping review. Ask the operator
|
|
332
|
+
* before doing that; see references/admin.md in the served skill.
|
|
333
|
+
*/
|
|
334
|
+
async addKnowledge(input) {
|
|
335
|
+
return this.post("/knowledge", input);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Attach a binary source (PDF, DOCX, XLSX, …) to an entity. Admin only.
|
|
339
|
+
*
|
|
340
|
+
* Three hops: ask for an upload URL, send the bytes straight to Convex
|
|
341
|
+
* storage, then register the resulting storageId. The bytes never pass
|
|
342
|
+
* through function arguments, so file size isn't bounded by an arg limit.
|
|
343
|
+
*/
|
|
344
|
+
async attachKnowledgeFile(input) {
|
|
345
|
+
const { uploadUrl } = await this.post("/knowledge/file", {
|
|
346
|
+
entityId: input.entityId,
|
|
347
|
+
org: input.org,
|
|
348
|
+
});
|
|
349
|
+
const byteSize = input.data.byteLength;
|
|
350
|
+
const uploadRes = await fetch(uploadUrl, {
|
|
351
|
+
method: "POST",
|
|
352
|
+
headers: { "Content-Type": input.mimeType },
|
|
353
|
+
body: input.data,
|
|
354
|
+
});
|
|
355
|
+
if (!uploadRes.ok) {
|
|
356
|
+
throw new ApiError(uploadRes.status, `Upload to storage failed: ${uploadRes.status} ${uploadRes.statusText}`);
|
|
357
|
+
}
|
|
358
|
+
const uploaded = (await uploadRes.json());
|
|
359
|
+
if (!uploaded.storageId)
|
|
360
|
+
throw new Error("Storage upload returned no storageId");
|
|
361
|
+
return this.put("/knowledge/file", {
|
|
362
|
+
entityId: input.entityId,
|
|
363
|
+
org: input.org,
|
|
364
|
+
storageId: uploaded.storageId,
|
|
365
|
+
fileName: input.fileName,
|
|
366
|
+
mimeType: input.mimeType,
|
|
367
|
+
byteSize,
|
|
368
|
+
sourceType: input.sourceType,
|
|
369
|
+
sourceUrl: input.sourceUrl,
|
|
370
|
+
sourceDescription: input.sourceDescription,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
134
373
|
// ── Skill ─────────────────────────────────────────────────────────────────
|
|
135
|
-
async getSkillBundle() {
|
|
136
|
-
return this.get(
|
|
374
|
+
async getSkillBundle(name) {
|
|
375
|
+
return this.get(`/skill${name ? `?name=${encodeURIComponent(name)}` : ""}`);
|
|
376
|
+
}
|
|
377
|
+
async listSkillBundles() {
|
|
378
|
+
return this.get("/skill?list=1");
|
|
137
379
|
}
|
|
138
380
|
// ── Schema ────────────────────────────────────────────────────────────────
|
|
139
381
|
async getSchema() {
|
|
@@ -141,55 +383,49 @@ export class DayOfWeekClient {
|
|
|
141
383
|
}
|
|
142
384
|
// ── HTTP helpers ──────────────────────────────────────────────────────────
|
|
143
385
|
async get(path) {
|
|
144
|
-
|
|
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();
|
|
386
|
+
return this.request(path, { method: "GET" });
|
|
160
387
|
}
|
|
161
388
|
async post(path, body) {
|
|
389
|
+
return this.request(path, { method: "POST", body: JSON.stringify(body) });
|
|
390
|
+
}
|
|
391
|
+
async put(path, body) {
|
|
392
|
+
return this.request(path, { method: "PUT", body: JSON.stringify(body) });
|
|
393
|
+
}
|
|
394
|
+
async patch(path, body) {
|
|
395
|
+
return this.request(path, { method: "PATCH", body: JSON.stringify(body) });
|
|
396
|
+
}
|
|
397
|
+
async delete(path) {
|
|
398
|
+
return this.request(path, { method: "DELETE" });
|
|
399
|
+
}
|
|
400
|
+
async request(path, init) {
|
|
162
401
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
163
|
-
|
|
402
|
+
...init,
|
|
164
403
|
headers: {
|
|
165
404
|
Authorization: `Bearer ${this.token}`,
|
|
166
|
-
"Content-Type": "application/json",
|
|
405
|
+
...(init.body ? { "Content-Type": "application/json" } : {}),
|
|
406
|
+
...init.headers,
|
|
167
407
|
},
|
|
168
|
-
body: JSON.stringify(body),
|
|
169
408
|
});
|
|
170
|
-
if (!res.ok)
|
|
171
|
-
|
|
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
|
-
}
|
|
409
|
+
if (!res.ok)
|
|
410
|
+
throw await apiErrorFromResponse(res);
|
|
182
411
|
return res.json();
|
|
183
412
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
413
|
+
}
|
|
414
|
+
async function hashFile(path) {
|
|
415
|
+
const hash = createHash("sha256");
|
|
416
|
+
for await (const chunk of createReadStream(path))
|
|
417
|
+
hash.update(chunk);
|
|
418
|
+
return hash.digest("hex");
|
|
419
|
+
}
|
|
420
|
+
async function apiErrorFromResponse(response) {
|
|
421
|
+
const body = await response.text();
|
|
422
|
+
try {
|
|
423
|
+
const json = JSON.parse(body);
|
|
424
|
+
if (typeof json.error === "string")
|
|
425
|
+
return new ApiError(response.status, json.error);
|
|
426
|
+
return new ApiError(response.status, json.error?.message ?? `Request failed (${response.status})`, json.error?.code, json.error?.requestId);
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
return new ApiError(response.status, `Request failed (${response.status})`);
|
|
194
430
|
}
|
|
195
431
|
}
|
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
|
-
|
|
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
|
-
|
|
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;
|