@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/README.md +82 -55
- 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 +450 -57
- package/dist/client.d.ts +162 -2
- package/dist/client.js +214 -44
- package/dist/config.d.ts +13 -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 } from "../config.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { ApiError, DayOfWeekClient } 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, readFileSync, existsSync, statSync } 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,7 +62,26 @@ 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
|
+
}
|
|
74
|
+
// Persist isAdmin so the next CLI invocation can register admin
|
|
75
|
+
// subcommands in --help without a network round-trip. Stale cache
|
|
76
|
+
// is harmless: admin commands still reject non-admin tokens at the
|
|
77
|
+
// API boundary, and admin demotion is rare.
|
|
78
|
+
if (typeof result === "object" &&
|
|
79
|
+
result !== null &&
|
|
80
|
+
"authenticated" in result &&
|
|
81
|
+
"isAdmin" in result &&
|
|
82
|
+
typeof result.isAdmin === "boolean") {
|
|
83
|
+
saveConfig({ isAdmin: result.isAdmin, roleCachedAt: Date.now() });
|
|
84
|
+
}
|
|
60
85
|
output(result);
|
|
61
86
|
}
|
|
62
87
|
catch (err) {
|
|
@@ -64,6 +89,208 @@ auth
|
|
|
64
89
|
process.exit(1);
|
|
65
90
|
}
|
|
66
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
|
+
});
|
|
67
294
|
auth
|
|
68
295
|
.command("devices")
|
|
69
296
|
.description("List your agent tokens")
|
|
@@ -167,6 +394,7 @@ agent
|
|
|
167
394
|
.option("--parent <entityId>", "Parent entity ID")
|
|
168
395
|
.option("--entity-type <type>", "Entity type (Farm, Producer, ...)")
|
|
169
396
|
.option("--target-id <id>", "Target record ID (for update/delete)")
|
|
397
|
+
.option("--org <slug>", "File against another org (admin tokens only)")
|
|
170
398
|
.option("--file <path>", "Read payload from JSON file (- for stdin)")
|
|
171
399
|
.option("--payload <json>", "Inline JSON payload")
|
|
172
400
|
.action(async (opts) => {
|
|
@@ -179,6 +407,7 @@ agent
|
|
|
179
407
|
}
|
|
180
408
|
const client = getClient();
|
|
181
409
|
const result = await client.submitProposal({
|
|
410
|
+
org: opts.org,
|
|
182
411
|
operation: opts.op,
|
|
183
412
|
targetTable: opts.table,
|
|
184
413
|
title: opts.title,
|
|
@@ -198,6 +427,7 @@ agent
|
|
|
198
427
|
.description("Submit multiple proposals")
|
|
199
428
|
.requiredOption("--label <label>", "Batch label")
|
|
200
429
|
.option("--source <agent>", "Agent identifier")
|
|
430
|
+
.option("--org <slug>", "File against another org (admin tokens only)")
|
|
201
431
|
.requiredOption("--file <path>", "JSON file with proposals array (- for stdin)")
|
|
202
432
|
.action(async (opts) => {
|
|
203
433
|
const content = opts.file === "-"
|
|
@@ -206,6 +436,7 @@ agent
|
|
|
206
436
|
const proposals = JSON.parse(content);
|
|
207
437
|
const client = getClient();
|
|
208
438
|
const result = await client.submitBatch({
|
|
439
|
+
org: opts.org,
|
|
209
440
|
batchLabel: opts.label,
|
|
210
441
|
sourceAgent: opts.source,
|
|
211
442
|
proposals: Array.isArray(proposals) ? proposals : [proposals],
|
|
@@ -248,15 +479,88 @@ function resolveTargetDirs(target, bundleName, customDir) {
|
|
|
248
479
|
return claudeRootExists ? [agentsDir, claudeDir] : [agentsDir];
|
|
249
480
|
}
|
|
250
481
|
}
|
|
251
|
-
function
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
482
|
+
function sha256(value) {
|
|
483
|
+
return createHash("sha256").update(value).digest("hex");
|
|
484
|
+
}
|
|
485
|
+
function inspectWikiProject(directory) {
|
|
486
|
+
const root = resolve(directory);
|
|
487
|
+
const manifestPath = join(root, ".dayofweek", "manifest.json");
|
|
488
|
+
const missing = {
|
|
489
|
+
manifest: { name: "project_manifest", ok: false, detail: "not found in current directory" },
|
|
490
|
+
launcher: { name: "project_launcher", ok: false, detail: "manifest unavailable" },
|
|
491
|
+
skills: { name: "project_skills", ok: false, detail: "manifest unavailable" },
|
|
492
|
+
write: { name: "project_write", ok: false, detail: "manifest unavailable" },
|
|
493
|
+
};
|
|
494
|
+
if (!existsSync(manifestPath))
|
|
495
|
+
return missing;
|
|
496
|
+
try {
|
|
497
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
498
|
+
if (manifest.schemaVersion !== 1 ||
|
|
499
|
+
typeof manifest.dcliRuntimePath !== "string" ||
|
|
500
|
+
!manifest.managedFiles ||
|
|
501
|
+
typeof manifest.managedFiles !== "object") {
|
|
502
|
+
return {
|
|
503
|
+
manifest: { name: "project_manifest", ok: false, detail: "invalid schema" },
|
|
504
|
+
launcher: missing.launcher,
|
|
505
|
+
skills: missing.skills,
|
|
506
|
+
write: missing.write,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
const runtime = resolve(manifest.dcliRuntimePath);
|
|
510
|
+
const runtimeOk = existsSync(runtime) && statSync(runtime).isFile();
|
|
511
|
+
const launcherPaths = process.platform === "win32"
|
|
512
|
+
? [".dayofweek/bin/dcli.cmd"]
|
|
513
|
+
: [".dayofweek/bin/dcli"];
|
|
514
|
+
const launcherOk = runtimeOk && launcherPaths.every((path) => managedFileMatches(root, path, manifest.managedFiles));
|
|
515
|
+
const skillNames = ["personal-llm-wiki", "dayofweek-brain"];
|
|
516
|
+
const skillPaths = skillNames.flatMap((name) => [
|
|
517
|
+
`.agents/skills/${name}/SKILL.md`,
|
|
518
|
+
`.claude/skills/${name}/SKILL.md`,
|
|
519
|
+
]);
|
|
520
|
+
const skillsOk = skillPaths.every((path) => managedFileMatches(root, path, manifest.managedFiles));
|
|
521
|
+
let writeOk = false;
|
|
522
|
+
try {
|
|
523
|
+
accessSync(root, constants.W_OK);
|
|
524
|
+
writeOk = true;
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
writeOk = false;
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
manifest: {
|
|
531
|
+
name: "project_manifest",
|
|
532
|
+
ok: true,
|
|
533
|
+
detail: `schema=1 dcli=${manifest.dcliVersion ?? "unknown"}`,
|
|
534
|
+
},
|
|
535
|
+
launcher: {
|
|
536
|
+
name: "project_launcher",
|
|
537
|
+
ok: launcherOk,
|
|
538
|
+
detail: runtimeOk ? (launcherOk ? "managed launcher verified" : "launcher modified or missing") : "managed runtime missing",
|
|
539
|
+
},
|
|
540
|
+
skills: {
|
|
541
|
+
name: "project_skills",
|
|
542
|
+
ok: skillsOk,
|
|
543
|
+
detail: skillsOk ? Object.entries(manifest.skillVersions ?? {}).map(([name, version]) => `${name}@${version}`).join(",") : "managed skill modified or missing",
|
|
544
|
+
},
|
|
545
|
+
write: { name: "project_write", ok: writeOk, detail: writeOk ? "folder is writable" : "folder is not writable" },
|
|
546
|
+
defaultAreaId: manifest.defaultAreaId,
|
|
547
|
+
};
|
|
258
548
|
}
|
|
259
|
-
|
|
549
|
+
catch {
|
|
550
|
+
return {
|
|
551
|
+
manifest: { name: "project_manifest", ok: false, detail: "unreadable or malformed" },
|
|
552
|
+
launcher: missing.launcher,
|
|
553
|
+
skills: missing.skills,
|
|
554
|
+
write: missing.write,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function managedFileMatches(root, relativePath, managedFiles) {
|
|
559
|
+
const expected = managedFiles[relativePath];
|
|
560
|
+
const target = resolve(root, relativePath);
|
|
561
|
+
const pathFromRoot = relative(root, target);
|
|
562
|
+
const insideRoot = pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${sep}`) && !pathFromRoot.startsWith(sep);
|
|
563
|
+
return Boolean(expected && insideRoot && existsSync(target) && sha256(readFileSync(target)) === expected);
|
|
260
564
|
}
|
|
261
565
|
function parseTarget(value) {
|
|
262
566
|
const v = (value ?? "all").toLowerCase();
|
|
@@ -267,68 +571,129 @@ function parseTarget(value) {
|
|
|
267
571
|
return v;
|
|
268
572
|
}
|
|
269
573
|
skill
|
|
270
|
-
.command("
|
|
574
|
+
.command("list")
|
|
575
|
+
.description("List authenticated named skill bundles")
|
|
576
|
+
.action(async () => output(await getClient().listSkillBundles()));
|
|
577
|
+
skill
|
|
578
|
+
.command("bundle <name>")
|
|
579
|
+
.description("Fetch and verify a named skill bundle for a managed installer")
|
|
580
|
+
.action(async (name) => output(validateSkillBundle(await getClient().getSkillBundle(name))));
|
|
581
|
+
skill
|
|
582
|
+
.command("install [name]")
|
|
271
583
|
.description("Install the agent skill (requires valid auth)")
|
|
272
584
|
.option("--dir <path>", "Custom install directory (overrides --target)")
|
|
273
585
|
.option("--target <target>", "Install target: agents, claude, or all (default: all)")
|
|
274
|
-
.action(async (opts) => {
|
|
586
|
+
.action(async (name, opts) => {
|
|
275
587
|
const client = getClient();
|
|
276
|
-
const bundle = await client.getSkillBundle();
|
|
588
|
+
const bundle = await client.getSkillBundle(name);
|
|
277
589
|
const target = parseTarget(opts.target);
|
|
278
590
|
const dirs = resolveTargetDirs(target, bundle.name, opts.dir);
|
|
591
|
+
const installations = [];
|
|
279
592
|
for (const dir of dirs) {
|
|
280
|
-
const
|
|
281
|
-
|
|
593
|
+
const result = writeSkillBundle(bundle, dir);
|
|
594
|
+
installations.push({ directory: dir, ...result });
|
|
282
595
|
}
|
|
283
|
-
|
|
596
|
+
output({ action: "install", bundle: bundle.name, version: bundle.version, hash: bundle.hash, installations });
|
|
284
597
|
});
|
|
285
598
|
skill
|
|
286
|
-
.command("update")
|
|
599
|
+
.command("update [name]")
|
|
287
600
|
.description("Update the skill to the latest version")
|
|
288
601
|
.option("--dir <path>", "Custom install directory (overrides --target)")
|
|
289
602
|
.option("--target <target>", "Install target: agents, claude, or all (default: all)")
|
|
290
|
-
.action(async (opts) => {
|
|
603
|
+
.action(async (name, opts) => {
|
|
291
604
|
const client = getClient();
|
|
292
|
-
const bundle = await client.getSkillBundle();
|
|
605
|
+
const bundle = await client.getSkillBundle(name);
|
|
293
606
|
const target = parseTarget(opts.target);
|
|
294
607
|
const dirs = resolveTargetDirs(target, bundle.name, opts.dir);
|
|
608
|
+
const installations = [];
|
|
295
609
|
for (const dir of dirs) {
|
|
296
|
-
const
|
|
297
|
-
|
|
610
|
+
const result = writeSkillBundle(bundle, dir);
|
|
611
|
+
installations.push({ directory: dir, ...result });
|
|
298
612
|
}
|
|
613
|
+
output({ action: "update", bundle: bundle.name, version: bundle.version, hash: bundle.hash, installations });
|
|
299
614
|
});
|
|
300
615
|
skill
|
|
301
|
-
.command("status")
|
|
616
|
+
.command("status [name]")
|
|
302
617
|
.description("Check if the skill is installed")
|
|
303
618
|
.option("--dir <path>", "Custom install directory (overrides --target)")
|
|
304
619
|
.option("--target <target>", "Check target: agents, claude, or all (default: all)")
|
|
305
|
-
.action(async (opts) => {
|
|
306
|
-
const bundleName = "dayofweek-platform";
|
|
620
|
+
.action(async (name, opts) => {
|
|
621
|
+
const bundleName = name ?? "dayofweek-platform";
|
|
307
622
|
const target = parseTarget(opts.target);
|
|
308
623
|
const dirs = opts.dir
|
|
309
624
|
? [opts.dir]
|
|
310
625
|
: target === "all"
|
|
311
626
|
? [join(homedir(), ".agents", "skills", bundleName), join(homedir(), ".claude", "skills", bundleName)]
|
|
312
627
|
: resolveTargetDirs(target, bundleName);
|
|
313
|
-
|
|
628
|
+
const installations = [];
|
|
314
629
|
for (const dir of dirs) {
|
|
315
630
|
const skillPath = join(dir, "SKILL.md");
|
|
316
631
|
if (!existsSync(skillPath)) {
|
|
317
|
-
|
|
632
|
+
installations.push({ installed: false, directory: dir, name: bundleName });
|
|
318
633
|
continue;
|
|
319
634
|
}
|
|
320
|
-
anyInstalled = true;
|
|
321
635
|
const content = readFileSync(skillPath, "utf-8");
|
|
322
636
|
const versionMatch = content.match(/version:\s*"([^"]+)"/);
|
|
323
|
-
|
|
324
|
-
console.log(` Version: ${versionMatch?.[1] ?? "unknown"}`);
|
|
637
|
+
installations.push({ installed: true, directory: dir, name: bundleName, version: versionMatch?.[1] ?? "unknown", sha256: sha256(content) });
|
|
325
638
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
console.log("\nTo update: dcli skill update");
|
|
639
|
+
const installed = installations.some((entry) => entry.installed);
|
|
640
|
+
output({ installed, bundle: bundleName, installations });
|
|
641
|
+
if (!installed)
|
|
642
|
+
process.exitCode = 2;
|
|
331
643
|
});
|
|
644
|
+
// ── Admin Commands ───────────────────────────────────────────────────────────
|
|
645
|
+
//
|
|
646
|
+
// These are admin-only. They're registered as hidden subcommands when the
|
|
647
|
+
// cached role from the last `dcli auth status` says the caller is admin —
|
|
648
|
+
// otherwise they're not advertised in --help at all and customers never
|
|
649
|
+
// learn the commands exist. The endpoints themselves enforce admin auth
|
|
650
|
+
// independently, so stale or absent cache can't grant access.
|
|
651
|
+
//
|
|
652
|
+
// New admin agents should run `dcli auth status` once after install to
|
|
653
|
+
// populate the cache; the admin skill bundle (ADMIN_MD) documents this.
|
|
654
|
+
function registerAdminCommands() {
|
|
655
|
+
const cfg = loadConfig();
|
|
656
|
+
if (!cfg.isAdmin)
|
|
657
|
+
return;
|
|
658
|
+
const admin = program
|
|
659
|
+
.command("admin", { hidden: true })
|
|
660
|
+
.description("Admin-only cross-org operations (DoW staff)");
|
|
661
|
+
admin
|
|
662
|
+
.command("entities")
|
|
663
|
+
.description("List entities across all orgs with admin filters")
|
|
664
|
+
.option("--type <entityType>", "Filter by entity type")
|
|
665
|
+
.option("--missing-location", "Only entities lacking metadata.places[].lat/lng")
|
|
666
|
+
.option("--search <query>", "Substring match on name")
|
|
667
|
+
.option("--org <slug>", "Restrict to a single org slug or ID")
|
|
668
|
+
.option("--limit <count>", "Max results (default 200)", parseInt)
|
|
669
|
+
.action(async (opts) => {
|
|
670
|
+
const client = getClient();
|
|
671
|
+
const result = await client.adminListEntities({
|
|
672
|
+
org: opts.org,
|
|
673
|
+
type: opts.type,
|
|
674
|
+
missingLocation: opts.missingLocation,
|
|
675
|
+
search: opts.search,
|
|
676
|
+
limit: opts.limit,
|
|
677
|
+
});
|
|
678
|
+
output(result);
|
|
679
|
+
});
|
|
680
|
+
admin
|
|
681
|
+
.command("proposals")
|
|
682
|
+
.description("List agent proposals across all orgs")
|
|
683
|
+
.option("--status <status>", "Filter: pending, approved, rejected, failed")
|
|
684
|
+
.option("--source-agent <name>", "Filter by sourceAgent identifier")
|
|
685
|
+
.option("--limit <count>", "Max results (default 100)", parseInt)
|
|
686
|
+
.action(async (opts) => {
|
|
687
|
+
const client = getClient();
|
|
688
|
+
const result = await client.adminListProposals({
|
|
689
|
+
status: opts.status,
|
|
690
|
+
sourceAgent: opts.sourceAgent,
|
|
691
|
+
limit: opts.limit,
|
|
692
|
+
});
|
|
693
|
+
output(result);
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
registerAdminCommands();
|
|
332
697
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
333
698
|
async function readStdin() {
|
|
334
699
|
const chunks = [];
|
|
@@ -338,8 +703,36 @@ async function readStdin() {
|
|
|
338
703
|
}
|
|
339
704
|
return chunks.join("\n");
|
|
340
705
|
}
|
|
706
|
+
function inferMimeType(path) {
|
|
707
|
+
const extension = path.toLowerCase().split(".").at(-1);
|
|
708
|
+
const types = {
|
|
709
|
+
pdf: "application/pdf",
|
|
710
|
+
md: "text/markdown",
|
|
711
|
+
txt: "text/plain",
|
|
712
|
+
csv: "text/csv",
|
|
713
|
+
json: "application/json",
|
|
714
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
715
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
716
|
+
mp3: "audio/mpeg",
|
|
717
|
+
m4a: "audio/mp4",
|
|
718
|
+
wav: "audio/wav",
|
|
719
|
+
mp4: "video/mp4",
|
|
720
|
+
};
|
|
721
|
+
const mime = extension ? types[extension] : undefined;
|
|
722
|
+
if (!mime)
|
|
723
|
+
throw new Error("Could not infer MIME type; pass --mime");
|
|
724
|
+
return mime;
|
|
725
|
+
}
|
|
341
726
|
// ── Run ──────────────────────────────────────────────────────────────────────
|
|
342
727
|
program.parseAsync(process.argv).catch((err) => {
|
|
343
728
|
console.error(err.message ?? err);
|
|
344
|
-
|
|
729
|
+
if (err instanceof ApiError) {
|
|
730
|
+
const code = err.code;
|
|
731
|
+
process.exit(code === "unauthenticated" ? 2 :
|
|
732
|
+
code === "scope_denied" || code === "not_found" ? 3 :
|
|
733
|
+
code === "conflict" ? 4 :
|
|
734
|
+
code === "quarantined" ? 7 :
|
|
735
|
+
err.status >= 500 ? 6 : 5);
|
|
736
|
+
}
|
|
737
|
+
process.exit(5);
|
|
345
738
|
});
|