@dayofweek/dcli 1.1.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/dcli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import { DayOfWeekClient } from "../client.js";
4
- import { getToken, getApiUrl, saveConfig } from "../config.js";
4
+ import { getToken, getApiUrl, saveConfig, loadConfig } from "../config.js";
5
5
  import { readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
6
6
  import { join, dirname } from "node:path";
7
7
  import { homedir } from "node:os";
@@ -57,6 +57,13 @@ auth
57
57
  try {
58
58
  const client = getClient();
59
59
  const result = await client.checkAuth();
60
+ // Persist isAdmin so the next CLI invocation can register admin
61
+ // subcommands in --help without a network round-trip. Stale cache
62
+ // is harmless: admin commands still reject non-admin tokens at the
63
+ // API boundary, and admin demotion is rare.
64
+ if (result.authenticated && typeof result.isAdmin === "boolean") {
65
+ saveConfig({ isAdmin: result.isAdmin, roleCachedAt: Date.now() });
66
+ }
60
67
  output(result);
61
68
  }
62
69
  catch (err) {
@@ -233,14 +240,22 @@ agent
233
240
  });
234
241
  // ── Skill Commands ───────────────────────────────────────────────────────────
235
242
  const skill = program.command("skill").description("Manage the Day of Week agent skill");
236
- skill
237
- .command("install")
238
- .description("Install the agent skill (requires valid auth)")
239
- .option("--dir <path>", "Custom install directory")
240
- .action(async (opts) => {
241
- const client = getClient();
242
- const bundle = await client.getSkillBundle();
243
- const targetDir = opts.dir ?? join(homedir(), ".agents", "skills", bundle.name);
243
+ function resolveTargetDirs(target, bundleName, customDir) {
244
+ if (customDir)
245
+ return [customDir];
246
+ const agentsDir = join(homedir(), ".agents", "skills", bundleName);
247
+ const claudeDir = join(homedir(), ".claude", "skills", bundleName);
248
+ const claudeRootExists = existsSync(join(homedir(), ".claude"));
249
+ switch (target) {
250
+ case "agents":
251
+ return [agentsDir];
252
+ case "claude":
253
+ return [claudeDir];
254
+ case "all":
255
+ return claudeRootExists ? [agentsDir, claudeDir] : [agentsDir];
256
+ }
257
+ }
258
+ function writeBundle(bundle, targetDir) {
244
259
  let filesWritten = 0;
245
260
  for (const file of bundle.files) {
246
261
  const filePath = join(targetDir, file.path);
@@ -248,44 +263,132 @@ skill
248
263
  writeFileSync(filePath, file.content, "utf-8");
249
264
  filesWritten++;
250
265
  }
251
- console.log(`Installed ${filesWritten} files to ${targetDir}`);
266
+ return filesWritten;
267
+ }
268
+ function parseTarget(value) {
269
+ const v = (value ?? "all").toLowerCase();
270
+ if (v !== "agents" && v !== "claude" && v !== "all") {
271
+ console.error(`Invalid --target: ${value}. Use agents, claude, or all.`);
272
+ process.exit(1);
273
+ }
274
+ return v;
275
+ }
276
+ skill
277
+ .command("install")
278
+ .description("Install the agent skill (requires valid auth)")
279
+ .option("--dir <path>", "Custom install directory (overrides --target)")
280
+ .option("--target <target>", "Install target: agents, claude, or all (default: all)")
281
+ .action(async (opts) => {
282
+ const client = getClient();
283
+ const bundle = await client.getSkillBundle();
284
+ const target = parseTarget(opts.target);
285
+ const dirs = resolveTargetDirs(target, bundle.name, opts.dir);
286
+ for (const dir of dirs) {
287
+ const count = writeBundle(bundle, dir);
288
+ console.log(`Installed ${count} files to ${dir}`);
289
+ }
252
290
  console.log("Any compatible agent will discover the skill automatically.");
253
291
  });
254
292
  skill
255
293
  .command("update")
256
294
  .description("Update the skill to the latest version")
257
- .option("--dir <path>", "Custom install directory")
295
+ .option("--dir <path>", "Custom install directory (overrides --target)")
296
+ .option("--target <target>", "Install target: agents, claude, or all (default: all)")
258
297
  .action(async (opts) => {
259
- // Same as install — overwrites
260
298
  const client = getClient();
261
299
  const bundle = await client.getSkillBundle();
262
- const targetDir = opts.dir ?? join(homedir(), ".agents", "skills", bundle.name);
263
- let filesWritten = 0;
264
- for (const file of bundle.files) {
265
- const filePath = join(targetDir, file.path);
266
- mkdirSync(dirname(filePath), { recursive: true });
267
- writeFileSync(filePath, file.content, "utf-8");
268
- filesWritten++;
300
+ const target = parseTarget(opts.target);
301
+ const dirs = resolveTargetDirs(target, bundle.name, opts.dir);
302
+ for (const dir of dirs) {
303
+ const count = writeBundle(bundle, dir);
304
+ console.log(`Updated ${count} files in ${dir}`);
269
305
  }
270
- console.log(`Updated ${filesWritten} files in ${targetDir}`);
271
306
  });
272
307
  skill
273
308
  .command("status")
274
309
  .description("Check if the skill is installed")
275
- .option("--dir <path>", "Custom install directory")
310
+ .option("--dir <path>", "Custom install directory (overrides --target)")
311
+ .option("--target <target>", "Check target: agents, claude, or all (default: all)")
276
312
  .action(async (opts) => {
277
- const dir = opts.dir ?? join(homedir(), ".agents", "skills", "dayofweek-platform");
278
- const skillPath = join(dir, "SKILL.md");
279
- if (!existsSync(skillPath)) {
280
- console.log("Not installed. Run: dcli skill install");
313
+ const bundleName = "dayofweek-platform";
314
+ const target = parseTarget(opts.target);
315
+ const dirs = opts.dir
316
+ ? [opts.dir]
317
+ : target === "all"
318
+ ? [join(homedir(), ".agents", "skills", bundleName), join(homedir(), ".claude", "skills", bundleName)]
319
+ : resolveTargetDirs(target, bundleName);
320
+ let anyInstalled = false;
321
+ for (const dir of dirs) {
322
+ const skillPath = join(dir, "SKILL.md");
323
+ if (!existsSync(skillPath)) {
324
+ console.log(`Not installed at: ${dir}`);
325
+ continue;
326
+ }
327
+ anyInstalled = true;
328
+ const content = readFileSync(skillPath, "utf-8");
329
+ const versionMatch = content.match(/version:\s*"([^"]+)"/);
330
+ console.log(`Installed at: ${dir}`);
331
+ console.log(` Version: ${versionMatch?.[1] ?? "unknown"}`);
332
+ }
333
+ if (!anyInstalled) {
334
+ console.log("\nRun: dcli skill install");
281
335
  process.exit(1);
282
336
  }
283
- const content = readFileSync(skillPath, "utf-8");
284
- const versionMatch = content.match(/version:\s*"([^"]+)"/);
285
- console.log(`Installed at: ${dir}`);
286
- console.log(`Version: ${versionMatch?.[1] ?? "unknown"}`);
287
337
  console.log("\nTo update: dcli skill update");
288
338
  });
339
+ // ── Admin Commands ───────────────────────────────────────────────────────────
340
+ //
341
+ // These are admin-only. They're registered as hidden subcommands when the
342
+ // cached role from the last `dcli auth status` says the caller is admin —
343
+ // otherwise they're not advertised in --help at all and customers never
344
+ // learn the commands exist. The endpoints themselves enforce admin auth
345
+ // independently, so stale or absent cache can't grant access.
346
+ //
347
+ // New admin agents should run `dcli auth status` once after install to
348
+ // populate the cache; the admin skill bundle (ADMIN_MD) documents this.
349
+ function registerAdminCommands() {
350
+ const cfg = loadConfig();
351
+ if (!cfg.isAdmin)
352
+ return;
353
+ const admin = program
354
+ .command("admin", { hidden: true })
355
+ .description("Admin-only cross-org operations (DoW staff)");
356
+ admin
357
+ .command("entities")
358
+ .description("List entities across all orgs with admin filters")
359
+ .option("--type <entityType>", "Filter by entity type")
360
+ .option("--missing-location", "Only entities lacking metadata.places[].lat/lng")
361
+ .option("--search <query>", "Substring match on name")
362
+ .option("--org <slug>", "Restrict to a single org slug or ID")
363
+ .option("--limit <count>", "Max results (default 200)", parseInt)
364
+ .action(async (opts) => {
365
+ const client = getClient();
366
+ const result = await client.adminListEntities({
367
+ org: opts.org,
368
+ type: opts.type,
369
+ missingLocation: opts.missingLocation,
370
+ search: opts.search,
371
+ limit: opts.limit,
372
+ });
373
+ output(result);
374
+ });
375
+ admin
376
+ .command("proposals")
377
+ .description("List agent proposals across all orgs")
378
+ .option("--status <status>", "Filter: pending, approved, rejected, failed")
379
+ .option("--source-agent <name>", "Filter by sourceAgent identifier")
380
+ .option("--limit <count>", "Max results (default 100)", parseInt)
381
+ .action(async (opts) => {
382
+ const client = getClient();
383
+ const result = await client.adminListProposals({
384
+ status: opts.status,
385
+ sourceAgent: opts.sourceAgent,
386
+ limit: opts.limit,
387
+ });
388
+ output(result);
389
+ });
390
+ }
391
+ registerAdminCommands();
289
392
  // ── Helpers ──────────────────────────────────────────────────────────────────
290
393
  async function readStdin() {
291
394
  const chunks = [];
package/dist/client.d.ts CHANGED
@@ -13,6 +13,7 @@ export declare class DayOfWeekClient {
13
13
  checkAuth(): Promise<{
14
14
  status: string;
15
15
  authenticated: boolean;
16
+ isAdmin?: boolean;
16
17
  }>;
17
18
  listDevices(): Promise<Array<{
18
19
  _id: string;
@@ -61,6 +62,26 @@ export declare class DayOfWeekClient {
61
62
  sourceAgent?: string;
62
63
  proposals: any[];
63
64
  }): Promise<any>;
65
+ adminListEntities(opts?: {
66
+ org?: string;
67
+ type?: string;
68
+ missingLocation?: boolean;
69
+ search?: string;
70
+ limit?: number;
71
+ }): Promise<{
72
+ results: any[];
73
+ truncated: boolean;
74
+ total: number;
75
+ }>;
76
+ adminListProposals(opts?: {
77
+ status?: string;
78
+ sourceAgent?: string;
79
+ limit?: number;
80
+ }): Promise<{
81
+ results: any[];
82
+ truncated: boolean;
83
+ total: number;
84
+ }>;
64
85
  getSkillBundle(): Promise<{
65
86
  name: string;
66
87
  version: string;
package/dist/client.js CHANGED
@@ -103,6 +103,34 @@ export class DayOfWeekClient {
103
103
  async submitBatch(batch) {
104
104
  return this.post("/proposals/batch", batch);
105
105
  }
106
+ // ── Admin (cross-org) ─────────────────────────────────────────────────────
107
+ // These endpoints reject non-admin tokens with 401 "Admin access required".
108
+ async adminListEntities(opts) {
109
+ const params = new URLSearchParams();
110
+ if (opts?.org)
111
+ params.set("org", opts.org);
112
+ if (opts?.type)
113
+ params.set("type", opts.type);
114
+ if (opts?.missingLocation)
115
+ params.set("missing-location", "1");
116
+ if (opts?.search)
117
+ params.set("search", opts.search);
118
+ if (opts?.limit)
119
+ params.set("limit", String(opts.limit));
120
+ const qs = params.toString();
121
+ return this.get(`/admin/entities${qs ? `?${qs}` : ""}`);
122
+ }
123
+ async adminListProposals(opts) {
124
+ const params = new URLSearchParams();
125
+ if (opts?.status)
126
+ params.set("status", opts.status);
127
+ if (opts?.sourceAgent)
128
+ params.set("source-agent", opts.sourceAgent);
129
+ if (opts?.limit)
130
+ params.set("limit", String(opts.limit));
131
+ const qs = params.toString();
132
+ return this.get(`/admin/proposals${qs ? `?${qs}` : ""}`);
133
+ }
106
134
  // ── Skill ─────────────────────────────────────────────────────────────────
107
135
  async getSkillBundle() {
108
136
  return this.get("/skill");
package/dist/config.d.ts CHANGED
@@ -1,6 +1,15 @@
1
1
  export interface DcliConfig {
2
2
  authToken?: string;
3
3
  apiUrl?: string;
4
+ /**
5
+ * Cached admin status from the last `dcli auth status` call. Used to
6
+ * decide locally whether to expose admin-only subcommands in `--help`.
7
+ * Refreshed automatically when `dcli auth status` or `dcli auth login`
8
+ * runs; admin commands are hidden when this is unset or false.
9
+ */
10
+ isAdmin?: boolean;
11
+ /** Unix-ms timestamp of the last isAdmin refresh. */
12
+ roleCachedAt?: number;
4
13
  }
5
14
  export declare function loadConfig(): DcliConfig;
6
15
  export declare function saveConfig(updates: Partial<DcliConfig>): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dayofweek/dcli",
3
- "version": "1.1.3",
3
+ "version": "1.3.0",
4
4
  "description": "CLI for the Day of Week AgTech platform — read data and submit proposals for review",
5
5
  "license": "MIT",
6
6
  "type": "module",