@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/bin/dcli.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
import { DayOfWeekClient } from "../client.js";
|
|
4
|
-
import { getToken, getApiUrl, saveConfig, loadConfig } from "../config.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { ApiError, DayOfWeekClient, toArrayBuffer } from "../client.js";
|
|
4
|
+
import { getToken, getApiUrl, saveConfig, loadConfig, saveCredential, deleteCredential } from "../config.js";
|
|
5
|
+
import { browserLogin } from "../auth/login.js";
|
|
6
|
+
import { parseBrainResource } from "../uri.js";
|
|
7
|
+
import { accessSync, constants, mkdirSync, readFileSync, existsSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join, basename, resolve, relative, sep } from "node:path";
|
|
7
9
|
import { homedir } from "node:os";
|
|
8
10
|
import { createInterface } from "node:readline/promises";
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
import pkg from "../../package.json" with { type: "json" };
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { validateSkillBundle, writeSkillBundle } from "../skills.js";
|
|
12
14
|
const program = new Command()
|
|
13
15
|
.name("dcli")
|
|
14
16
|
.description("CLI for the Day of Week AgTech platform")
|
|
@@ -30,25 +32,29 @@ const auth = program.command("auth").description("Authentication commands");
|
|
|
30
32
|
auth
|
|
31
33
|
.command("login")
|
|
32
34
|
.description("Authenticate via browser")
|
|
33
|
-
.
|
|
35
|
+
.option("--scopes <scopes>", "Comma-separated scopes", "brain:read,brain:write")
|
|
36
|
+
.option("--source-app <sourceApp>", "Authorization client", "dcli")
|
|
37
|
+
.action(async (opts) => {
|
|
34
38
|
const apiUrl = program.opts().apiUrl ?? getApiUrl();
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
console.
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
console.log(" export DCLI_AUTH_TOKEN=<your-token>");
|
|
43
|
-
console.log(" # or");
|
|
44
|
-
console.log(" dcli auth set-token <your-token>");
|
|
39
|
+
const scopes = opts.scopes.split(",").map((scope) => scope.trim()).filter(Boolean);
|
|
40
|
+
if (opts.sourceApp !== "dcli" && opts.sourceApp !== "dayofweek-desktop")
|
|
41
|
+
throw new Error("Invalid authorization client");
|
|
42
|
+
console.error("Opening Day of Week in your browser…");
|
|
43
|
+
const result = await browserLogin({ apiUrl, scopes, sourceApp: opts.sourceApp });
|
|
44
|
+
saveCredential(result.secret);
|
|
45
|
+
output({ authenticated: true, scopes: result.scopes, bootstrap: result.bootstrap });
|
|
45
46
|
});
|
|
46
47
|
auth
|
|
47
|
-
.command("
|
|
48
|
-
.description("
|
|
49
|
-
.action((
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
.command("logout")
|
|
49
|
+
.description("Revoke and remove the credential from this device")
|
|
50
|
+
.action(async () => {
|
|
51
|
+
if (program.opts().token || process.env.DCLI_AUTH_TOKEN || process.env.DCLI_TOKEN) {
|
|
52
|
+
throw new Error("Unset the token override before logging out this device");
|
|
53
|
+
}
|
|
54
|
+
const client = new DayOfWeekClient(getToken(), getApiUrl());
|
|
55
|
+
await client.revokeCurrentDevice();
|
|
56
|
+
deleteCredential();
|
|
57
|
+
output({ authenticated: false, loggedOut: true, revoked: true });
|
|
52
58
|
});
|
|
53
59
|
auth
|
|
54
60
|
.command("status")
|
|
@@ -56,12 +62,24 @@ auth
|
|
|
56
62
|
.action(async () => {
|
|
57
63
|
try {
|
|
58
64
|
const client = getClient();
|
|
59
|
-
|
|
65
|
+
let result;
|
|
66
|
+
try {
|
|
67
|
+
result = await client.brainBootstrap();
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (!(error instanceof ApiError) || error.code !== "scope_denied")
|
|
71
|
+
throw error;
|
|
72
|
+
result = await client.checkAuth();
|
|
73
|
+
}
|
|
60
74
|
// Persist isAdmin so the next CLI invocation can register admin
|
|
61
75
|
// subcommands in --help without a network round-trip. Stale cache
|
|
62
76
|
// is harmless: admin commands still reject non-admin tokens at the
|
|
63
77
|
// API boundary, and admin demotion is rare.
|
|
64
|
-
if (
|
|
78
|
+
if (typeof result === "object" &&
|
|
79
|
+
result !== null &&
|
|
80
|
+
"authenticated" in result &&
|
|
81
|
+
"isAdmin" in result &&
|
|
82
|
+
typeof result.isAdmin === "boolean") {
|
|
65
83
|
saveConfig({ isAdmin: result.isAdmin, roleCachedAt: Date.now() });
|
|
66
84
|
}
|
|
67
85
|
output(result);
|
|
@@ -71,6 +89,208 @@ auth
|
|
|
71
89
|
process.exit(1);
|
|
72
90
|
}
|
|
73
91
|
});
|
|
92
|
+
program
|
|
93
|
+
.command("doctor")
|
|
94
|
+
.description("Run machine-readable Day of Week health checks")
|
|
95
|
+
.action(async () => {
|
|
96
|
+
const project = inspectWikiProject(process.cwd());
|
|
97
|
+
const checks = [
|
|
98
|
+
{ name: "binary", ok: true, detail: pkg.version },
|
|
99
|
+
{ name: "credential", ok: false },
|
|
100
|
+
{ name: "api", ok: false },
|
|
101
|
+
{ name: "login", ok: false },
|
|
102
|
+
{ name: "areas", ok: false },
|
|
103
|
+
project.manifest,
|
|
104
|
+
project.launcher,
|
|
105
|
+
project.skills,
|
|
106
|
+
project.write,
|
|
107
|
+
{ name: "default_area", ok: false, detail: "manifest or API unavailable" },
|
|
108
|
+
];
|
|
109
|
+
try {
|
|
110
|
+
getToken();
|
|
111
|
+
checks[1] = { name: "credential", ok: true };
|
|
112
|
+
const bootstrap = await getClient().brainBootstrap();
|
|
113
|
+
checks[2] = { name: "api", ok: true };
|
|
114
|
+
checks[3] = { name: "login", ok: bootstrap.authenticated, detail: bootstrap.scopes.join(",") };
|
|
115
|
+
checks[4] = { name: "areas", ok: bootstrap.areas.length > 0, detail: String(bootstrap.areas.length) };
|
|
116
|
+
const defaultArea = checks.find((check) => check.name === "default_area");
|
|
117
|
+
defaultArea.ok = Boolean(project.defaultAreaId && bootstrap.areas.some((area) => area.id === project.defaultAreaId));
|
|
118
|
+
defaultArea.detail = defaultArea.ok ? project.defaultAreaId : "configured area is no longer accessible";
|
|
119
|
+
output({ ok: checks.every((check) => check.ok), checks, defaultAreaId: bootstrap.defaultAreaId });
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
output({ ok: false, checks, error: error instanceof Error ? error.message : "Health check failed" });
|
|
123
|
+
process.exitCode = 2;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
// ── Shared Brain Commands ───────────────────────────────────────────────────
|
|
127
|
+
const brain = program.command("brain").description("Work with shared Day of Week knowledge");
|
|
128
|
+
const brainCompany = brain.command("company").description("Connect the idempotent company knowledge space");
|
|
129
|
+
brainCompany
|
|
130
|
+
.command("status")
|
|
131
|
+
.description("Check whether the current hierarchy membership can connect a company space")
|
|
132
|
+
.action(async () => output(await getClient().companyBrainStatus()));
|
|
133
|
+
brainCompany
|
|
134
|
+
.command("ensure")
|
|
135
|
+
.description("Create or connect the one company brain for the current entity")
|
|
136
|
+
.action(async () => output(await getClient().ensureCompanyBrain()));
|
|
137
|
+
brain
|
|
138
|
+
.command("list")
|
|
139
|
+
.description("List accessible company and project spaces")
|
|
140
|
+
.action(async () => output(await getClient().listBrainAreas()));
|
|
141
|
+
brain
|
|
142
|
+
.command("search <query>")
|
|
143
|
+
.description("Search accessible shared knowledge")
|
|
144
|
+
.option("--area <areaId>", "Restrict to one accessible area")
|
|
145
|
+
.option("--limit <count>", "Maximum results", (value) => Number.parseInt(value, 10), 10)
|
|
146
|
+
.action(async (query, opts) => {
|
|
147
|
+
output(await getClient().searchBrain(query, { areaId: opts.area, limit: opts.limit }));
|
|
148
|
+
});
|
|
149
|
+
brain
|
|
150
|
+
.command("get <uri>")
|
|
151
|
+
.description("Resolve a Day of Week brain URI")
|
|
152
|
+
.option("--markdown", "Print note markdown only")
|
|
153
|
+
.action(async (uri, opts) => {
|
|
154
|
+
parseBrainResource(uri);
|
|
155
|
+
const result = await getClient().resolveBrain(uri);
|
|
156
|
+
if (opts.markdown) {
|
|
157
|
+
if (result.resourceType !== "note")
|
|
158
|
+
throw new Error("--markdown requires a note URI");
|
|
159
|
+
process.stdout.write(result.note.markdown);
|
|
160
|
+
if (!result.note.markdown.endsWith("\n"))
|
|
161
|
+
process.stdout.write("\n");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
output(result);
|
|
165
|
+
});
|
|
166
|
+
brain
|
|
167
|
+
.command("share")
|
|
168
|
+
.description("Explicitly share a markdown note")
|
|
169
|
+
.requiredOption("--area <areaId>", "Destination area")
|
|
170
|
+
.requiredOption("--title <title>", "Shared note title")
|
|
171
|
+
.option("--file <path>", "Markdown file")
|
|
172
|
+
.option("--stdin", "Read markdown from standard input")
|
|
173
|
+
.requiredOption("--intent <intent>", "interactive or autonomous")
|
|
174
|
+
.action(async (opts) => {
|
|
175
|
+
if (Boolean(opts.file) === Boolean(opts.stdin))
|
|
176
|
+
throw new Error("Use exactly one of --file or --stdin");
|
|
177
|
+
if (opts.intent !== "interactive" && opts.intent !== "autonomous")
|
|
178
|
+
throw new Error("Invalid --intent");
|
|
179
|
+
const markdown = opts.stdin ? await readStdin() : readFileSync(opts.file, "utf8");
|
|
180
|
+
output(await getClient().shareBrainNote({
|
|
181
|
+
areaId: opts.area,
|
|
182
|
+
title: opts.title,
|
|
183
|
+
markdown,
|
|
184
|
+
sourceName: opts.file ? basename(opts.file) : undefined,
|
|
185
|
+
intent: opts.intent,
|
|
186
|
+
}));
|
|
187
|
+
});
|
|
188
|
+
brain
|
|
189
|
+
.command("update <uri>")
|
|
190
|
+
.description("Update a shared note with optimistic concurrency")
|
|
191
|
+
.requiredOption("--file <path>", "Markdown file")
|
|
192
|
+
.requiredOption("--if-version <version>", "Expected current version", (value) => Number.parseInt(value, 10))
|
|
193
|
+
.option("--title <title>", "Updated title")
|
|
194
|
+
.action(async (uri, opts) => {
|
|
195
|
+
const parsed = parseBrainResource(uri);
|
|
196
|
+
if (parsed.resourceType !== "note" || !parsed.resourceId)
|
|
197
|
+
throw new Error("A note URI is required");
|
|
198
|
+
const note = await getClient().updateBrainNote(parsed.resourceId, {
|
|
199
|
+
title: opts.title,
|
|
200
|
+
markdown: readFileSync(opts.file, "utf8"),
|
|
201
|
+
expectedVersion: opts.ifVersion,
|
|
202
|
+
});
|
|
203
|
+
if (note.areaId !== parsed.areaId)
|
|
204
|
+
throw new Error("Server returned a mismatched area");
|
|
205
|
+
output(note);
|
|
206
|
+
});
|
|
207
|
+
brain
|
|
208
|
+
.command("archive <uri>")
|
|
209
|
+
.description("Soft-archive a shared note")
|
|
210
|
+
.requiredOption("--if-version <version>", "Expected current version", (value) => Number.parseInt(value, 10))
|
|
211
|
+
.action(async (uri, opts) => {
|
|
212
|
+
const parsed = parseBrainResource(uri);
|
|
213
|
+
if (parsed.resourceType !== "note" || !parsed.resourceId)
|
|
214
|
+
throw new Error("A note URI is required");
|
|
215
|
+
const note = await getClient().archiveBrainNote(parsed.resourceId, opts.ifVersion);
|
|
216
|
+
if (note.areaId !== parsed.areaId)
|
|
217
|
+
throw new Error("Server returned a mismatched area");
|
|
218
|
+
output(note);
|
|
219
|
+
});
|
|
220
|
+
brain
|
|
221
|
+
.command("restore <uri>")
|
|
222
|
+
.description("Restore an archived shared note")
|
|
223
|
+
.requiredOption("--if-version <version>", "Expected current version", (value) => Number.parseInt(value, 10))
|
|
224
|
+
.action(async (uri, opts) => {
|
|
225
|
+
const parsed = parseBrainResource(uri);
|
|
226
|
+
if (parsed.resourceType !== "note" || !parsed.resourceId)
|
|
227
|
+
throw new Error("A note URI is required");
|
|
228
|
+
const note = await getClient().restoreBrainNote(parsed.resourceId, opts.ifVersion);
|
|
229
|
+
if (note.areaId !== parsed.areaId)
|
|
230
|
+
throw new Error("Server returned a mismatched area");
|
|
231
|
+
output(note);
|
|
232
|
+
});
|
|
233
|
+
brain
|
|
234
|
+
.command("audit")
|
|
235
|
+
.description("List bounded audit metadata (area owner and brain:manage scope required)")
|
|
236
|
+
.requiredOption("--area <areaId>", "Area to inspect")
|
|
237
|
+
.option("--cursor <cursor>", "Pagination cursor")
|
|
238
|
+
.option("--limit <count>", "Maximum events", (value) => Number.parseInt(value, 10), 50)
|
|
239
|
+
.action(async (opts) => {
|
|
240
|
+
output(await getClient().listBrainAudit(opts.area, { cursor: opts.cursor, limit: opts.limit }));
|
|
241
|
+
});
|
|
242
|
+
const brainSource = brain.command("source").description("Work with original shared files and recordings");
|
|
243
|
+
brainSource
|
|
244
|
+
.command("get <uri>")
|
|
245
|
+
.description("Read source metadata and derived text")
|
|
246
|
+
.action(async (uri) => {
|
|
247
|
+
const parsed = parseBrainResource(uri);
|
|
248
|
+
if (parsed.resourceType !== "source" || !parsed.resourceId)
|
|
249
|
+
throw new Error("A source URI is required");
|
|
250
|
+
const source = await getClient().getBrainSource(parsed.resourceId);
|
|
251
|
+
if (source.areaId !== parsed.areaId)
|
|
252
|
+
throw new Error("Server returned a mismatched area");
|
|
253
|
+
output(source);
|
|
254
|
+
});
|
|
255
|
+
brainSource
|
|
256
|
+
.command("upload")
|
|
257
|
+
.description("Upload an original file or meeting recording")
|
|
258
|
+
.requiredOption("--area <areaId>", "Destination area")
|
|
259
|
+
.requiredOption("--file <path>", "File to upload")
|
|
260
|
+
.option("--mime <mimeType>", "MIME type; inferred from extension when omitted")
|
|
261
|
+
.option("--meeting", "Mark this source as a recorded meeting")
|
|
262
|
+
.option("--consent-ack", "Confirm recorded participants were informed and consented")
|
|
263
|
+
.action(async (opts) => {
|
|
264
|
+
if (opts.meeting && !opts.consentAck) {
|
|
265
|
+
throw new Error("--consent-ack is required: you are attesting that recorded participants were informed and consented");
|
|
266
|
+
}
|
|
267
|
+
output(await getClient().uploadBrainSource({
|
|
268
|
+
areaId: opts.area,
|
|
269
|
+
path: opts.file,
|
|
270
|
+
mimeType: opts.mime ?? inferMimeType(opts.file),
|
|
271
|
+
isMeeting: opts.meeting ?? false,
|
|
272
|
+
consentAcknowledged: opts.consentAck ?? false,
|
|
273
|
+
}));
|
|
274
|
+
});
|
|
275
|
+
brainSource
|
|
276
|
+
.command("download <uri>")
|
|
277
|
+
.description("Download exact original bytes to an explicit path")
|
|
278
|
+
.requiredOption("--output <path>", "Explicit output file")
|
|
279
|
+
.option("--overwrite", "Replace an existing output file")
|
|
280
|
+
.action(async (uri, opts) => {
|
|
281
|
+
const parsed = parseBrainResource(uri);
|
|
282
|
+
if (parsed.resourceType !== "source" || !parsed.resourceId)
|
|
283
|
+
throw new Error("A source URI is required");
|
|
284
|
+
const source = await getClient().getBrainSource(parsed.resourceId);
|
|
285
|
+
if (source.areaId !== parsed.areaId)
|
|
286
|
+
throw new Error("Server returned a mismatched area");
|
|
287
|
+
output(await getClient().downloadBrainSource({
|
|
288
|
+
sourceId: source.id,
|
|
289
|
+
outputPath: opts.output,
|
|
290
|
+
expectedSha256: source.sha256,
|
|
291
|
+
overwrite: opts.overwrite,
|
|
292
|
+
}));
|
|
293
|
+
});
|
|
74
294
|
auth
|
|
75
295
|
.command("devices")
|
|
76
296
|
.description("List your agent tokens")
|
|
@@ -174,6 +394,7 @@ agent
|
|
|
174
394
|
.option("--parent <entityId>", "Parent entity ID")
|
|
175
395
|
.option("--entity-type <type>", "Entity type (Farm, Producer, ...)")
|
|
176
396
|
.option("--target-id <id>", "Target record ID (for update/delete)")
|
|
397
|
+
.option("--org <slug>", "File against another org (admin tokens only)")
|
|
177
398
|
.option("--file <path>", "Read payload from JSON file (- for stdin)")
|
|
178
399
|
.option("--payload <json>", "Inline JSON payload")
|
|
179
400
|
.action(async (opts) => {
|
|
@@ -186,6 +407,7 @@ agent
|
|
|
186
407
|
}
|
|
187
408
|
const client = getClient();
|
|
188
409
|
const result = await client.submitProposal({
|
|
410
|
+
org: opts.org,
|
|
189
411
|
operation: opts.op,
|
|
190
412
|
targetTable: opts.table,
|
|
191
413
|
title: opts.title,
|
|
@@ -205,6 +427,7 @@ agent
|
|
|
205
427
|
.description("Submit multiple proposals")
|
|
206
428
|
.requiredOption("--label <label>", "Batch label")
|
|
207
429
|
.option("--source <agent>", "Agent identifier")
|
|
430
|
+
.option("--org <slug>", "File against another org (admin tokens only)")
|
|
208
431
|
.requiredOption("--file <path>", "JSON file with proposals array (- for stdin)")
|
|
209
432
|
.action(async (opts) => {
|
|
210
433
|
const content = opts.file === "-"
|
|
@@ -213,6 +436,7 @@ agent
|
|
|
213
436
|
const proposals = JSON.parse(content);
|
|
214
437
|
const client = getClient();
|
|
215
438
|
const result = await client.submitBatch({
|
|
439
|
+
org: opts.org,
|
|
216
440
|
batchLabel: opts.label,
|
|
217
441
|
sourceAgent: opts.source,
|
|
218
442
|
proposals: Array.isArray(proposals) ? proposals : [proposals],
|
|
@@ -238,6 +462,191 @@ agent
|
|
|
238
462
|
const result = await client.getProposal(proposalId);
|
|
239
463
|
output(result);
|
|
240
464
|
});
|
|
465
|
+
// ── Knowledge Commands ───────────────────────────────────────────────────────
|
|
466
|
+
const MIME_BY_EXT = {
|
|
467
|
+
".pdf": "application/pdf",
|
|
468
|
+
".md": "text/markdown",
|
|
469
|
+
".txt": "text/plain",
|
|
470
|
+
".csv": "text/csv",
|
|
471
|
+
".json": "application/json",
|
|
472
|
+
".html": "text/html",
|
|
473
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
474
|
+
".doc": "application/msword",
|
|
475
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
476
|
+
".xls": "application/vnd.ms-excel",
|
|
477
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
478
|
+
};
|
|
479
|
+
function guessMimeType(path) {
|
|
480
|
+
const dot = path.lastIndexOf(".");
|
|
481
|
+
const ext = dot === -1 ? "" : path.slice(dot).toLowerCase();
|
|
482
|
+
return MIME_BY_EXT[ext] ?? "application/octet-stream";
|
|
483
|
+
}
|
|
484
|
+
/** Filesystem-safe name for an exported document, keeping it recognisable. */
|
|
485
|
+
function exportFileName(doc) {
|
|
486
|
+
const base = (doc.fileName || doc.title || doc.id || "document").trim();
|
|
487
|
+
const stripped = base.replace(/\.(md|txt)$/i, "");
|
|
488
|
+
const safe = stripped.replace(/[/\\:*?"<>|]+/g, "-").replace(/\s+/g, " ").slice(0, 120);
|
|
489
|
+
return `${safe || doc.id || "document"}.md`;
|
|
490
|
+
}
|
|
491
|
+
const knowledge = program
|
|
492
|
+
.command("knowledge")
|
|
493
|
+
.description("Read, add and export entity knowledge documents");
|
|
494
|
+
knowledge
|
|
495
|
+
.command("list")
|
|
496
|
+
.description("List knowledge documents on an entity")
|
|
497
|
+
.requiredOption("--entity <entityId>", "Target entity id")
|
|
498
|
+
.option("--full", "Include each document's full content, not an excerpt")
|
|
499
|
+
.option("--org <org>", "Organization (admin only)")
|
|
500
|
+
.action(async (opts) => {
|
|
501
|
+
const client = getClient();
|
|
502
|
+
output(await client.listKnowledge({ entity: opts.entity, full: opts.full, org: opts.org }));
|
|
503
|
+
});
|
|
504
|
+
knowledge
|
|
505
|
+
.command("get <documentId>")
|
|
506
|
+
.description("Show one knowledge document with full content")
|
|
507
|
+
.option("--org <org>", "Organization (admin only)")
|
|
508
|
+
.action(async (documentId, opts) => {
|
|
509
|
+
const client = getClient();
|
|
510
|
+
output(await client.getKnowledge(documentId, opts.org));
|
|
511
|
+
});
|
|
512
|
+
knowledge
|
|
513
|
+
.command("search <query>")
|
|
514
|
+
.description("Semantic search across knowledge documents")
|
|
515
|
+
.option("--entity <entityId>", "Scope to one entity's org tree")
|
|
516
|
+
.option("--types <types>", "Comma-separated sourceType filter")
|
|
517
|
+
.option("--limit <count>", "Max hits", parseInt)
|
|
518
|
+
.option("--all-orgs", "Search every org (admin only)")
|
|
519
|
+
.option("--org <org>", "Organization (admin only)")
|
|
520
|
+
.action(async (query, opts) => {
|
|
521
|
+
const client = getClient();
|
|
522
|
+
output(await client.searchKnowledge({
|
|
523
|
+
query,
|
|
524
|
+
entity: opts.entity,
|
|
525
|
+
types: opts.types
|
|
526
|
+
? String(opts.types).split(",").map((t) => t.trim()).filter(Boolean)
|
|
527
|
+
: undefined,
|
|
528
|
+
limit: opts.limit,
|
|
529
|
+
allOrgs: opts.allOrgs,
|
|
530
|
+
org: opts.org,
|
|
531
|
+
}));
|
|
532
|
+
});
|
|
533
|
+
knowledge
|
|
534
|
+
.command("add")
|
|
535
|
+
.description("Add a markdown knowledge note (proposal by default)")
|
|
536
|
+
.requiredOption("--entity <entityId>", "Target entity id")
|
|
537
|
+
.requiredOption("--title <title>", "Document title")
|
|
538
|
+
.option("--file <path>", "Markdown file to read (- for stdin)")
|
|
539
|
+
.option("--content <text>", "Inline content instead of --file")
|
|
540
|
+
.option("--source-type <type>", "research | website | competitive_intel | …", "research")
|
|
541
|
+
.option("--source-url <url>", "Where the content came from")
|
|
542
|
+
.option("--source-description <text>", "Short provenance note")
|
|
543
|
+
.option("--confidence <n>", "0-1, helps reviewers prioritize", parseFloat)
|
|
544
|
+
.option("--source-agent <name>", "Attribution for the submitting agent")
|
|
545
|
+
.option("--direct", "Write immediately instead of proposing. Admin only — ask the operator first")
|
|
546
|
+
.option("--org <org>", "Organization (admin only)")
|
|
547
|
+
.action(async (opts) => {
|
|
548
|
+
let content;
|
|
549
|
+
if (opts.content) {
|
|
550
|
+
content = String(opts.content);
|
|
551
|
+
}
|
|
552
|
+
else if (opts.file) {
|
|
553
|
+
content = opts.file === "-" ? readFileSync(0, "utf8") : readFileSync(opts.file, "utf8");
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
throw new Error("Provide --file or --content");
|
|
557
|
+
}
|
|
558
|
+
if (!content.trim())
|
|
559
|
+
throw new Error("Content is empty");
|
|
560
|
+
const client = getClient();
|
|
561
|
+
output(await client.addKnowledge({
|
|
562
|
+
entityId: opts.entity,
|
|
563
|
+
title: opts.title,
|
|
564
|
+
content,
|
|
565
|
+
sourceType: opts.sourceType,
|
|
566
|
+
sourceUrl: opts.sourceUrl,
|
|
567
|
+
sourceDescription: opts.sourceDescription,
|
|
568
|
+
confidence: opts.confidence,
|
|
569
|
+
sourceAgent: opts.sourceAgent,
|
|
570
|
+
direct: Boolean(opts.direct),
|
|
571
|
+
org: opts.org,
|
|
572
|
+
}));
|
|
573
|
+
});
|
|
574
|
+
knowledge
|
|
575
|
+
.command("attach")
|
|
576
|
+
.description("Attach a file (PDF, DOCX, XLSX …) as a source. Admin only")
|
|
577
|
+
.requiredOption("--entity <entityId>", "Target entity id")
|
|
578
|
+
.requiredOption("--file <path>", "File to upload")
|
|
579
|
+
.option("--name <fileName>", "Stored file name (defaults to the file's own)")
|
|
580
|
+
.option("--mime <mimeType>", "Content type (guessed from the extension)")
|
|
581
|
+
.option("--source-type <type>", "research | website | contract | …", "other")
|
|
582
|
+
.option("--source-url <url>", "Where the file came from")
|
|
583
|
+
.option("--source-description <text>", "Short provenance note")
|
|
584
|
+
.option("--org <org>", "Organization (admin only)")
|
|
585
|
+
.action(async (opts) => {
|
|
586
|
+
if (!existsSync(opts.file))
|
|
587
|
+
throw new Error(`File not found: ${opts.file}`);
|
|
588
|
+
const data = readFileSync(opts.file);
|
|
589
|
+
if (data.byteLength === 0)
|
|
590
|
+
throw new Error("File is empty");
|
|
591
|
+
const client = getClient();
|
|
592
|
+
output(await client.attachKnowledgeFile({
|
|
593
|
+
entityId: opts.entity,
|
|
594
|
+
data: toArrayBuffer(data),
|
|
595
|
+
fileName: opts.name ?? basename(opts.file),
|
|
596
|
+
mimeType: opts.mime ?? guessMimeType(opts.file),
|
|
597
|
+
sourceType: opts.sourceType,
|
|
598
|
+
sourceUrl: opts.sourceUrl,
|
|
599
|
+
sourceDescription: opts.sourceDescription,
|
|
600
|
+
org: opts.org,
|
|
601
|
+
}));
|
|
602
|
+
});
|
|
603
|
+
knowledge
|
|
604
|
+
.command("export")
|
|
605
|
+
.description("Write an entity's knowledge documents to disk as markdown")
|
|
606
|
+
.requiredOption("--entity <entityId>", "Source entity id")
|
|
607
|
+
.requiredOption("--out <dir>", "Directory to write into (created if missing)")
|
|
608
|
+
.option("--org <org>", "Organization (admin only)")
|
|
609
|
+
.action(async (opts) => {
|
|
610
|
+
const client = getClient();
|
|
611
|
+
const docs = await client.listKnowledge({ entity: opts.entity, full: true, org: opts.org });
|
|
612
|
+
mkdirSync(opts.out, { recursive: true });
|
|
613
|
+
const written = [];
|
|
614
|
+
const skipped = [];
|
|
615
|
+
for (const doc of docs) {
|
|
616
|
+
const body = typeof doc.content === "string" ? doc.content : "";
|
|
617
|
+
if (!body) {
|
|
618
|
+
// A binary source whose text extraction hasn't finished has nothing to
|
|
619
|
+
// mirror yet. Report it rather than writing an empty file.
|
|
620
|
+
skipped.push({
|
|
621
|
+
id: doc.id,
|
|
622
|
+
title: doc.title,
|
|
623
|
+
reason: doc.processingStatus && doc.processingStatus !== "completed"
|
|
624
|
+
? `processingStatus=${doc.processingStatus}`
|
|
625
|
+
: "no extracted content",
|
|
626
|
+
});
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
const frontMatter = [
|
|
630
|
+
"---",
|
|
631
|
+
`document_id: ${doc.id}`,
|
|
632
|
+
`title: ${JSON.stringify(doc.title ?? "")}`,
|
|
633
|
+
`source_type: ${doc.sourceType ?? "other"}`,
|
|
634
|
+
...(doc.sourceUrl ? [`source_url: ${doc.sourceUrl}`] : []),
|
|
635
|
+
...(doc.sourceDescription
|
|
636
|
+
? [`source_description: ${JSON.stringify(doc.sourceDescription)}`]
|
|
637
|
+
: []),
|
|
638
|
+
`entity_id: ${opts.entity}`,
|
|
639
|
+
...(doc.createdAt ? [`created_at: ${new Date(doc.createdAt).toISOString()}`] : []),
|
|
640
|
+
"exported_from: dayofweek-platform",
|
|
641
|
+
"---",
|
|
642
|
+
"",
|
|
643
|
+
].join("\n");
|
|
644
|
+
const target = join(opts.out, exportFileName(doc));
|
|
645
|
+
writeFileSync(target, `${frontMatter}${body}\n`, "utf8");
|
|
646
|
+
written.push({ id: doc.id, file: target, bytes: Buffer.byteLength(body, "utf8") });
|
|
647
|
+
}
|
|
648
|
+
output({ entity: opts.entity, out: opts.out, total: docs.length, written, skipped });
|
|
649
|
+
});
|
|
241
650
|
// ── Skill Commands ───────────────────────────────────────────────────────────
|
|
242
651
|
const skill = program.command("skill").description("Manage the Day of Week agent skill");
|
|
243
652
|
function resolveTargetDirs(target, bundleName, customDir) {
|
|
@@ -255,15 +664,88 @@ function resolveTargetDirs(target, bundleName, customDir) {
|
|
|
255
664
|
return claudeRootExists ? [agentsDir, claudeDir] : [agentsDir];
|
|
256
665
|
}
|
|
257
666
|
}
|
|
258
|
-
function
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
667
|
+
function sha256(value) {
|
|
668
|
+
return createHash("sha256").update(value).digest("hex");
|
|
669
|
+
}
|
|
670
|
+
function inspectWikiProject(directory) {
|
|
671
|
+
const root = resolve(directory);
|
|
672
|
+
const manifestPath = join(root, ".dayofweek", "manifest.json");
|
|
673
|
+
const missing = {
|
|
674
|
+
manifest: { name: "project_manifest", ok: false, detail: "not found in current directory" },
|
|
675
|
+
launcher: { name: "project_launcher", ok: false, detail: "manifest unavailable" },
|
|
676
|
+
skills: { name: "project_skills", ok: false, detail: "manifest unavailable" },
|
|
677
|
+
write: { name: "project_write", ok: false, detail: "manifest unavailable" },
|
|
678
|
+
};
|
|
679
|
+
if (!existsSync(manifestPath))
|
|
680
|
+
return missing;
|
|
681
|
+
try {
|
|
682
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
683
|
+
if (manifest.schemaVersion !== 1 ||
|
|
684
|
+
typeof manifest.dcliRuntimePath !== "string" ||
|
|
685
|
+
!manifest.managedFiles ||
|
|
686
|
+
typeof manifest.managedFiles !== "object") {
|
|
687
|
+
return {
|
|
688
|
+
manifest: { name: "project_manifest", ok: false, detail: "invalid schema" },
|
|
689
|
+
launcher: missing.launcher,
|
|
690
|
+
skills: missing.skills,
|
|
691
|
+
write: missing.write,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const runtime = resolve(manifest.dcliRuntimePath);
|
|
695
|
+
const runtimeOk = existsSync(runtime) && statSync(runtime).isFile();
|
|
696
|
+
const launcherPaths = process.platform === "win32"
|
|
697
|
+
? [".dayofweek/bin/dcli.cmd"]
|
|
698
|
+
: [".dayofweek/bin/dcli"];
|
|
699
|
+
const launcherOk = runtimeOk && launcherPaths.every((path) => managedFileMatches(root, path, manifest.managedFiles));
|
|
700
|
+
const skillNames = ["personal-llm-wiki", "dayofweek-brain"];
|
|
701
|
+
const skillPaths = skillNames.flatMap((name) => [
|
|
702
|
+
`.agents/skills/${name}/SKILL.md`,
|
|
703
|
+
`.claude/skills/${name}/SKILL.md`,
|
|
704
|
+
]);
|
|
705
|
+
const skillsOk = skillPaths.every((path) => managedFileMatches(root, path, manifest.managedFiles));
|
|
706
|
+
let writeOk = false;
|
|
707
|
+
try {
|
|
708
|
+
accessSync(root, constants.W_OK);
|
|
709
|
+
writeOk = true;
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
writeOk = false;
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
manifest: {
|
|
716
|
+
name: "project_manifest",
|
|
717
|
+
ok: true,
|
|
718
|
+
detail: `schema=1 dcli=${manifest.dcliVersion ?? "unknown"}`,
|
|
719
|
+
},
|
|
720
|
+
launcher: {
|
|
721
|
+
name: "project_launcher",
|
|
722
|
+
ok: launcherOk,
|
|
723
|
+
detail: runtimeOk ? (launcherOk ? "managed launcher verified" : "launcher modified or missing") : "managed runtime missing",
|
|
724
|
+
},
|
|
725
|
+
skills: {
|
|
726
|
+
name: "project_skills",
|
|
727
|
+
ok: skillsOk,
|
|
728
|
+
detail: skillsOk ? Object.entries(manifest.skillVersions ?? {}).map(([name, version]) => `${name}@${version}`).join(",") : "managed skill modified or missing",
|
|
729
|
+
},
|
|
730
|
+
write: { name: "project_write", ok: writeOk, detail: writeOk ? "folder is writable" : "folder is not writable" },
|
|
731
|
+
defaultAreaId: manifest.defaultAreaId,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
return {
|
|
736
|
+
manifest: { name: "project_manifest", ok: false, detail: "unreadable or malformed" },
|
|
737
|
+
launcher: missing.launcher,
|
|
738
|
+
skills: missing.skills,
|
|
739
|
+
write: missing.write,
|
|
740
|
+
};
|
|
265
741
|
}
|
|
266
|
-
|
|
742
|
+
}
|
|
743
|
+
function managedFileMatches(root, relativePath, managedFiles) {
|
|
744
|
+
const expected = managedFiles[relativePath];
|
|
745
|
+
const target = resolve(root, relativePath);
|
|
746
|
+
const pathFromRoot = relative(root, target);
|
|
747
|
+
const insideRoot = pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${sep}`) && !pathFromRoot.startsWith(sep);
|
|
748
|
+
return Boolean(expected && insideRoot && existsSync(target) && sha256(readFileSync(target)) === expected);
|
|
267
749
|
}
|
|
268
750
|
function parseTarget(value) {
|
|
269
751
|
const v = (value ?? "all").toLowerCase();
|
|
@@ -274,67 +756,75 @@ function parseTarget(value) {
|
|
|
274
756
|
return v;
|
|
275
757
|
}
|
|
276
758
|
skill
|
|
277
|
-
.command("
|
|
759
|
+
.command("list")
|
|
760
|
+
.description("List authenticated named skill bundles")
|
|
761
|
+
.action(async () => output(await getClient().listSkillBundles()));
|
|
762
|
+
skill
|
|
763
|
+
.command("bundle <name>")
|
|
764
|
+
.description("Fetch and verify a named skill bundle for a managed installer")
|
|
765
|
+
.action(async (name) => output(validateSkillBundle(await getClient().getSkillBundle(name))));
|
|
766
|
+
skill
|
|
767
|
+
.command("install [name]")
|
|
278
768
|
.description("Install the agent skill (requires valid auth)")
|
|
279
769
|
.option("--dir <path>", "Custom install directory (overrides --target)")
|
|
280
770
|
.option("--target <target>", "Install target: agents, claude, or all (default: all)")
|
|
281
|
-
.action(async (opts) => {
|
|
771
|
+
.action(async (name, opts) => {
|
|
282
772
|
const client = getClient();
|
|
283
|
-
const bundle = await client.getSkillBundle();
|
|
773
|
+
const bundle = await client.getSkillBundle(name);
|
|
284
774
|
const target = parseTarget(opts.target);
|
|
285
775
|
const dirs = resolveTargetDirs(target, bundle.name, opts.dir);
|
|
776
|
+
const installations = [];
|
|
286
777
|
for (const dir of dirs) {
|
|
287
|
-
const
|
|
288
|
-
|
|
778
|
+
const result = writeSkillBundle(bundle, dir);
|
|
779
|
+
installations.push({ directory: dir, ...result });
|
|
289
780
|
}
|
|
290
|
-
|
|
781
|
+
output({ action: "install", bundle: bundle.name, version: bundle.version, hash: bundle.hash, installations });
|
|
291
782
|
});
|
|
292
783
|
skill
|
|
293
|
-
.command("update")
|
|
784
|
+
.command("update [name]")
|
|
294
785
|
.description("Update the skill to the latest version")
|
|
295
786
|
.option("--dir <path>", "Custom install directory (overrides --target)")
|
|
296
787
|
.option("--target <target>", "Install target: agents, claude, or all (default: all)")
|
|
297
|
-
.action(async (opts) => {
|
|
788
|
+
.action(async (name, opts) => {
|
|
298
789
|
const client = getClient();
|
|
299
|
-
const bundle = await client.getSkillBundle();
|
|
790
|
+
const bundle = await client.getSkillBundle(name);
|
|
300
791
|
const target = parseTarget(opts.target);
|
|
301
792
|
const dirs = resolveTargetDirs(target, bundle.name, opts.dir);
|
|
793
|
+
const installations = [];
|
|
302
794
|
for (const dir of dirs) {
|
|
303
|
-
const
|
|
304
|
-
|
|
795
|
+
const result = writeSkillBundle(bundle, dir);
|
|
796
|
+
installations.push({ directory: dir, ...result });
|
|
305
797
|
}
|
|
798
|
+
output({ action: "update", bundle: bundle.name, version: bundle.version, hash: bundle.hash, installations });
|
|
306
799
|
});
|
|
307
800
|
skill
|
|
308
|
-
.command("status")
|
|
801
|
+
.command("status [name]")
|
|
309
802
|
.description("Check if the skill is installed")
|
|
310
803
|
.option("--dir <path>", "Custom install directory (overrides --target)")
|
|
311
804
|
.option("--target <target>", "Check target: agents, claude, or all (default: all)")
|
|
312
|
-
.action(async (opts) => {
|
|
313
|
-
const bundleName = "dayofweek-platform";
|
|
805
|
+
.action(async (name, opts) => {
|
|
806
|
+
const bundleName = name ?? "dayofweek-platform";
|
|
314
807
|
const target = parseTarget(opts.target);
|
|
315
808
|
const dirs = opts.dir
|
|
316
809
|
? [opts.dir]
|
|
317
810
|
: target === "all"
|
|
318
811
|
? [join(homedir(), ".agents", "skills", bundleName), join(homedir(), ".claude", "skills", bundleName)]
|
|
319
812
|
: resolveTargetDirs(target, bundleName);
|
|
320
|
-
|
|
813
|
+
const installations = [];
|
|
321
814
|
for (const dir of dirs) {
|
|
322
815
|
const skillPath = join(dir, "SKILL.md");
|
|
323
816
|
if (!existsSync(skillPath)) {
|
|
324
|
-
|
|
817
|
+
installations.push({ installed: false, directory: dir, name: bundleName });
|
|
325
818
|
continue;
|
|
326
819
|
}
|
|
327
|
-
anyInstalled = true;
|
|
328
820
|
const content = readFileSync(skillPath, "utf-8");
|
|
329
821
|
const versionMatch = content.match(/version:\s*"([^"]+)"/);
|
|
330
|
-
|
|
331
|
-
console.log(` Version: ${versionMatch?.[1] ?? "unknown"}`);
|
|
822
|
+
installations.push({ installed: true, directory: dir, name: bundleName, version: versionMatch?.[1] ?? "unknown", sha256: sha256(content) });
|
|
332
823
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
console.log("\nTo update: dcli skill update");
|
|
824
|
+
const installed = installations.some((entry) => entry.installed);
|
|
825
|
+
output({ installed, bundle: bundleName, installations });
|
|
826
|
+
if (!installed)
|
|
827
|
+
process.exitCode = 2;
|
|
338
828
|
});
|
|
339
829
|
// ── Admin Commands ───────────────────────────────────────────────────────────
|
|
340
830
|
//
|
|
@@ -398,8 +888,36 @@ async function readStdin() {
|
|
|
398
888
|
}
|
|
399
889
|
return chunks.join("\n");
|
|
400
890
|
}
|
|
891
|
+
function inferMimeType(path) {
|
|
892
|
+
const extension = path.toLowerCase().split(".").at(-1);
|
|
893
|
+
const types = {
|
|
894
|
+
pdf: "application/pdf",
|
|
895
|
+
md: "text/markdown",
|
|
896
|
+
txt: "text/plain",
|
|
897
|
+
csv: "text/csv",
|
|
898
|
+
json: "application/json",
|
|
899
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
900
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
901
|
+
mp3: "audio/mpeg",
|
|
902
|
+
m4a: "audio/mp4",
|
|
903
|
+
wav: "audio/wav",
|
|
904
|
+
mp4: "video/mp4",
|
|
905
|
+
};
|
|
906
|
+
const mime = extension ? types[extension] : undefined;
|
|
907
|
+
if (!mime)
|
|
908
|
+
throw new Error("Could not infer MIME type; pass --mime");
|
|
909
|
+
return mime;
|
|
910
|
+
}
|
|
401
911
|
// ── Run ──────────────────────────────────────────────────────────────────────
|
|
402
912
|
program.parseAsync(process.argv).catch((err) => {
|
|
403
913
|
console.error(err.message ?? err);
|
|
404
|
-
|
|
914
|
+
if (err instanceof ApiError) {
|
|
915
|
+
const code = err.code;
|
|
916
|
+
process.exit(code === "unauthenticated" ? 2 :
|
|
917
|
+
code === "scope_denied" || code === "not_found" ? 3 :
|
|
918
|
+
code === "conflict" ? 4 :
|
|
919
|
+
code === "quarantined" ? 7 :
|
|
920
|
+
err.status >= 500 ? 6 : 5);
|
|
921
|
+
}
|
|
922
|
+
process.exit(5);
|
|
405
923
|
});
|