@dayofweek/dcli 1.11.0 → 1.12.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 CHANGED
@@ -153,6 +153,35 @@ dcli data get <dataset> --limit 100 --json
153
153
  Responses are `{ dataset, total, truncated, rows }`. The CLI has no built-in
154
154
  dataset names; new datasets appear in the listing without a CLI update.
155
155
 
156
+ ## Time tracking (staff)
157
+
158
+ Hours registered against time projects. The server owns every rule — project
159
+ and participation periods, hour frames, self-financing, the reportable basis,
160
+ who may approve — and the CLI only carries JSON.
161
+
162
+ ```bash
163
+ dcli time projects list --json
164
+ dcli time projects show <projectId> --json
165
+ dcli time projects upsert --file project.json # create, or update with projectId
166
+
167
+ dcli time entries list --project <id> --from 2026-06-01 --to 2026-09-30 --json
168
+ dcli time entries import --project <id> --file entries.json --dry-run
169
+ dcli time entries import --project <id> --file entries.json --approved
170
+
171
+ dcli time months list --json
172
+ dcli time months submit --project <id> --month 2026-08
173
+ dcli time months approve --project <id> --month 2026-08 --user-email someone@example.com
174
+ dcli time months return --project <id> --month 2026-08 --user-email ... --comment "..."
175
+ ```
176
+
177
+ `entries import` is idempotent: each entry carries an import key (explicit, or
178
+ derived from person + date + activity + description), so re-running a file
179
+ updates rather than duplicates. Approved rows are locked and reported as
180
+ `skip_locked`; any invalid entry aborts the whole batch with nothing written.
181
+ Because real rows are written, the command refuses to run without `--dry-run`
182
+ first and `--approved` afterwards. Payload shapes are documented in the
183
+ `time-tracking` reference that `dcli skill install` fetches for staff.
184
+
156
185
  ## Feedback backlog
157
186
 
158
187
  The customer feedback backlog that humans and coding agents work together.
package/dist/bin/dcli.js CHANGED
@@ -1351,6 +1351,127 @@ feedback
1351
1351
  rejectedReason: opts.rejectedReason,
1352
1352
  }));
1353
1353
  });
1354
+ // ── Time tracking ────────────────────────────────────────────────────────────
1355
+ //
1356
+ // Hours per project. The server owns the rules (project and participation
1357
+ // periods, hour frames, self-financing, the reportable basis, approvals);
1358
+ // this group parses flags, reads JSON files and prints the response. The
1359
+ // only local rule is the irreversibility gate on import: real rows are
1360
+ // written, so --dry-run first, then --approved.
1361
+ const time = program.command("time").description("Hours per project: projects, entries, monthly sheets");
1362
+ const timeProjects = time.command("projects").description("Time projects: setup and overview");
1363
+ timeProjects
1364
+ .command("list")
1365
+ .description("List time projects with totals and frame usage")
1366
+ .option("--org <org>", "Organization slug or id (admin only)")
1367
+ .action(async (opts) => {
1368
+ output(await getClient().listTimeProjects(opts.org));
1369
+ });
1370
+ timeProjects
1371
+ .command("show <projectId>")
1372
+ .description("One project: setup, person×activity×month matrix, entries with computed split, monthly sheets")
1373
+ .option("--org <org>", "Organization slug or id (admin only)")
1374
+ .action(async (projectId, opts) => {
1375
+ output(await getClient().getTimeProject(projectId, opts.org));
1376
+ });
1377
+ timeProjects
1378
+ .command("upsert")
1379
+ .description("Create or update a time project from a JSON file (include projectId to update)")
1380
+ .requiredOption("--file <path>", "JSON payload with the project setup (- for stdin)")
1381
+ .option("--org <org>", "Organization slug or id (admin only)")
1382
+ .action(async (opts) => {
1383
+ const raw = opts.file === "-" ? readFileSync(0, "utf8") : readFileSync(opts.file, "utf8");
1384
+ const payload = JSON.parse(raw);
1385
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
1386
+ throw new Error("Payload must be a JSON object with the project setup");
1387
+ }
1388
+ output(await getClient().upsertTimeProject(payload, opts.org));
1389
+ });
1390
+ const timeEntries = time.command("entries").description("Registered hours");
1391
+ timeEntries
1392
+ .command("list")
1393
+ .description("Flat rows with the computed split (counted, self-financed, reportable, reasons)")
1394
+ .option("--project <projectId>", "Only this project")
1395
+ .option("--user <userId>", "Only this person (owners and admins)")
1396
+ .option("--from <YYYY-MM-DD>", "First work date")
1397
+ .option("--to <YYYY-MM-DD>", "Last work date")
1398
+ .option("--status <status>", "draft | submitted | approved")
1399
+ .option("--org <org>", "Organization slug or id (admin only)")
1400
+ .action(async (opts) => {
1401
+ output(await getClient().listTimeEntries({
1402
+ project: opts.project,
1403
+ user: opts.user,
1404
+ from: opts.from,
1405
+ to: opts.to,
1406
+ status: opts.status,
1407
+ org: opts.org,
1408
+ }));
1409
+ });
1410
+ timeEntries
1411
+ .command("import")
1412
+ .description("Idempotent batch import of hours into one project. Requires --dry-run or --approved")
1413
+ .requiredOption("--project <projectId>", "Target time project")
1414
+ .requiredOption("--file <path>", "JSON: { entries: [...] } or a bare array (- for stdin)")
1415
+ .option("--dry-run", "Validate and report what would happen, write nothing")
1416
+ .option("--approved", "The operator approved the dry-run result")
1417
+ .option("--org <org>", "Organization slug or id (admin only)")
1418
+ .action(async (opts) => {
1419
+ if (!opts.dryRun && !opts.approved) {
1420
+ throw new Error("Refusing to write: run with --dry-run first, show the operator the summary, " +
1421
+ "then repeat with --approved. This writes real hours, not proposals.");
1422
+ }
1423
+ const raw = opts.file === "-" ? readFileSync(0, "utf8") : readFileSync(opts.file, "utf8");
1424
+ const payload = JSON.parse(raw);
1425
+ const entries = Array.isArray(payload) ? payload : payload?.entries;
1426
+ if (!Array.isArray(entries) || entries.length === 0) {
1427
+ throw new Error("Payload needs { entries: [...] } with at least one entry");
1428
+ }
1429
+ output(await getClient().importTimeEntries({
1430
+ projectId: opts.project,
1431
+ entries,
1432
+ dryRun: Boolean(opts.dryRun),
1433
+ org: opts.org,
1434
+ }));
1435
+ });
1436
+ const timeMonths = time.command("months").description("Monthly sheets: submit, approve, return, reopen");
1437
+ timeMonths
1438
+ .command("list")
1439
+ .description("Months waiting for your approval (project owner), or every submitted month (admin)")
1440
+ .option("--org <org>", "Organization slug or id (admin only)")
1441
+ .action(async (opts) => {
1442
+ output(await getClient().listPendingTimeMonths(opts.org));
1443
+ });
1444
+ for (const action of ["submit", "approve", "return", "reopen"]) {
1445
+ const descriptions = {
1446
+ submit: "Submit a month for approval (your own; admins may pass --user-email)",
1447
+ approve: "Approve a submitted month — locks its rows (project owner or admin)",
1448
+ return: "Return a submitted month as drafts, --comment required (project owner or admin)",
1449
+ reopen: "Reopen an approved month for correction; it needs a new approval",
1450
+ };
1451
+ timeMonths
1452
+ .command(action)
1453
+ .description(descriptions[action])
1454
+ .requiredOption("--project <projectId>", "Time project")
1455
+ .requiredOption("--month <YYYY-MM>", "Month")
1456
+ .option("--user-email <email>", "Whose month (defaults to yourself)")
1457
+ .option("--user <userId>", "Whose month, by user id")
1458
+ .option("--comment <text>", "Comment (required for return)")
1459
+ .option("--org <org>", "Organization slug or id (admin only)")
1460
+ .action(async (opts) => {
1461
+ if (action === "return" && !opts.comment) {
1462
+ throw new Error("--comment is required when returning a month");
1463
+ }
1464
+ output(await getClient().actOnTimeMonth({
1465
+ projectId: opts.project,
1466
+ month: opts.month,
1467
+ action,
1468
+ userId: opts.user,
1469
+ userEmail: opts.userEmail,
1470
+ comment: opts.comment,
1471
+ org: opts.org,
1472
+ }));
1473
+ });
1474
+ }
1354
1475
  // ── Admin Commands ───────────────────────────────────────────────────────────
1355
1476
  //
1356
1477
  // These are admin-only. They're registered as hidden subcommands when the
@@ -4773,6 +4773,52 @@ var DayOfWeekClient = class {
4773
4773
  async archiveSharedSkill(skillId) {
4774
4774
  return this.delete(`/brain/skills/${encodeURIComponent(skillId)}`);
4775
4775
  }
4776
+ // ── Time tracking ─────────────────────────────────────────────────────────
4777
+ //
4778
+ // Hours registered against time projects. Every rule (periods, frames,
4779
+ // self-financing, the reportable basis, who may approve) is evaluated by
4780
+ // the server; the client only carries JSON and prints what comes back.
4781
+ async listTimeProjects(org) {
4782
+ const qs = org ? `?org=${encodeURIComponent(org)}` : "";
4783
+ return this.get(`/time/projects${qs}`);
4784
+ }
4785
+ async getTimeProject(projectId, org) {
4786
+ const params = new URLSearchParams({ id: projectId });
4787
+ if (org) params.set("org", org);
4788
+ return this.get(`/time/projects?${params.toString()}`);
4789
+ }
4790
+ async upsertTimeProject(payload, org) {
4791
+ const qs = org ? `?org=${encodeURIComponent(org)}` : "";
4792
+ return this.post(`/time/projects${qs}`, payload);
4793
+ }
4794
+ async listTimeEntries(opts) {
4795
+ const params = new URLSearchParams();
4796
+ if (opts?.project) params.set("project", opts.project);
4797
+ if (opts?.user) params.set("user", opts.user);
4798
+ if (opts?.from) params.set("from", opts.from);
4799
+ if (opts?.to) params.set("to", opts.to);
4800
+ if (opts?.status) params.set("status", opts.status);
4801
+ if (opts?.org) params.set("org", opts.org);
4802
+ const qs = params.toString();
4803
+ return this.get(`/time/entries${qs ? `?${qs}` : ""}`);
4804
+ }
4805
+ async importTimeEntries(input) {
4806
+ const qs = input.org ? `?org=${encodeURIComponent(input.org)}` : "";
4807
+ return this.post(`/time/entries${qs}`, {
4808
+ projectId: input.projectId,
4809
+ dryRun: input.dryRun,
4810
+ entries: input.entries
4811
+ });
4812
+ }
4813
+ async listPendingTimeMonths(org) {
4814
+ const qs = org ? `?org=${encodeURIComponent(org)}` : "";
4815
+ return this.get(`/time/months${qs}`);
4816
+ }
4817
+ async actOnTimeMonth(input) {
4818
+ const { org, ...body } = input;
4819
+ const qs = org ? `?org=${encodeURIComponent(org)}` : "";
4820
+ return this.post(`/time/months${qs}`, body);
4821
+ }
4776
4822
  // ── Schema ────────────────────────────────────────────────────────────────
4777
4823
  async getSchema() {
4778
4824
  return this.get("/schema");
@@ -5221,7 +5267,7 @@ var import_promises4 = require("node:readline/promises");
5221
5267
  // package.json
5222
5268
  var package_default = {
5223
5269
  name: "@dayofweek/dcli",
5224
- version: "1.11.0",
5270
+ version: "1.12.0",
5225
5271
  description: "CLI for the Day of Week AgTech platform \u2014 read data and submit proposals for review",
5226
5272
  license: "MIT",
5227
5273
  type: "module",
@@ -6219,6 +6265,84 @@ feedback.command("status <itemId>").description("Set status and/or priority on a
6219
6265
  rejectedReason: opts.rejectedReason
6220
6266
  }));
6221
6267
  });
6268
+ var time = program2.command("time").description("Hours per project: projects, entries, monthly sheets");
6269
+ var timeProjects = time.command("projects").description("Time projects: setup and overview");
6270
+ timeProjects.command("list").description("List time projects with totals and frame usage").option("--org <org>", "Organization slug or id (admin only)").action(async (opts) => {
6271
+ output(await getClient().listTimeProjects(opts.org));
6272
+ });
6273
+ timeProjects.command("show <projectId>").description("One project: setup, person\xD7activity\xD7month matrix, entries with computed split, monthly sheets").option("--org <org>", "Organization slug or id (admin only)").action(async (projectId, opts) => {
6274
+ output(await getClient().getTimeProject(projectId, opts.org));
6275
+ });
6276
+ timeProjects.command("upsert").description("Create or update a time project from a JSON file (include projectId to update)").requiredOption("--file <path>", "JSON payload with the project setup (- for stdin)").option("--org <org>", "Organization slug or id (admin only)").action(async (opts) => {
6277
+ const raw = opts.file === "-" ? (0, import_node_fs8.readFileSync)(0, "utf8") : (0, import_node_fs8.readFileSync)(opts.file, "utf8");
6278
+ const payload = JSON.parse(raw);
6279
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
6280
+ throw new Error("Payload must be a JSON object with the project setup");
6281
+ }
6282
+ output(await getClient().upsertTimeProject(payload, opts.org));
6283
+ });
6284
+ var timeEntries = time.command("entries").description("Registered hours");
6285
+ timeEntries.command("list").description("Flat rows with the computed split (counted, self-financed, reportable, reasons)").option("--project <projectId>", "Only this project").option("--user <userId>", "Only this person (owners and admins)").option("--from <YYYY-MM-DD>", "First work date").option("--to <YYYY-MM-DD>", "Last work date").option("--status <status>", "draft | submitted | approved").option("--org <org>", "Organization slug or id (admin only)").action(async (opts) => {
6286
+ output(
6287
+ await getClient().listTimeEntries({
6288
+ project: opts.project,
6289
+ user: opts.user,
6290
+ from: opts.from,
6291
+ to: opts.to,
6292
+ status: opts.status,
6293
+ org: opts.org
6294
+ })
6295
+ );
6296
+ });
6297
+ timeEntries.command("import").description("Idempotent batch import of hours into one project. Requires --dry-run or --approved").requiredOption("--project <projectId>", "Target time project").requiredOption("--file <path>", "JSON: { entries: [...] } or a bare array (- for stdin)").option("--dry-run", "Validate and report what would happen, write nothing").option("--approved", "The operator approved the dry-run result").option("--org <org>", "Organization slug or id (admin only)").action(async (opts) => {
6298
+ if (!opts.dryRun && !opts.approved) {
6299
+ throw new Error(
6300
+ "Refusing to write: run with --dry-run first, show the operator the summary, then repeat with --approved. This writes real hours, not proposals."
6301
+ );
6302
+ }
6303
+ const raw = opts.file === "-" ? (0, import_node_fs8.readFileSync)(0, "utf8") : (0, import_node_fs8.readFileSync)(opts.file, "utf8");
6304
+ const payload = JSON.parse(raw);
6305
+ const entries = Array.isArray(payload) ? payload : payload?.entries;
6306
+ if (!Array.isArray(entries) || entries.length === 0) {
6307
+ throw new Error("Payload needs { entries: [...] } with at least one entry");
6308
+ }
6309
+ output(
6310
+ await getClient().importTimeEntries({
6311
+ projectId: opts.project,
6312
+ entries,
6313
+ dryRun: Boolean(opts.dryRun),
6314
+ org: opts.org
6315
+ })
6316
+ );
6317
+ });
6318
+ var timeMonths = time.command("months").description("Monthly sheets: submit, approve, return, reopen");
6319
+ timeMonths.command("list").description("Months waiting for your approval (project owner), or every submitted month (admin)").option("--org <org>", "Organization slug or id (admin only)").action(async (opts) => {
6320
+ output(await getClient().listPendingTimeMonths(opts.org));
6321
+ });
6322
+ for (const action of ["submit", "approve", "return", "reopen"]) {
6323
+ const descriptions = {
6324
+ submit: "Submit a month for approval (your own; admins may pass --user-email)",
6325
+ approve: "Approve a submitted month \u2014 locks its rows (project owner or admin)",
6326
+ return: "Return a submitted month as drafts, --comment required (project owner or admin)",
6327
+ reopen: "Reopen an approved month for correction; it needs a new approval"
6328
+ };
6329
+ timeMonths.command(action).description(descriptions[action]).requiredOption("--project <projectId>", "Time project").requiredOption("--month <YYYY-MM>", "Month").option("--user-email <email>", "Whose month (defaults to yourself)").option("--user <userId>", "Whose month, by user id").option("--comment <text>", "Comment (required for return)").option("--org <org>", "Organization slug or id (admin only)").action(async (opts) => {
6330
+ if (action === "return" && !opts.comment) {
6331
+ throw new Error("--comment is required when returning a month");
6332
+ }
6333
+ output(
6334
+ await getClient().actOnTimeMonth({
6335
+ projectId: opts.project,
6336
+ month: opts.month,
6337
+ action,
6338
+ userId: opts.user,
6339
+ userEmail: opts.userEmail,
6340
+ comment: opts.comment,
6341
+ org: opts.org
6342
+ })
6343
+ );
6344
+ });
6345
+ }
6222
6346
  function registerAdminCommands() {
6223
6347
  const cfg = loadConfig();
6224
6348
  if (!cfg.isAdmin) return;
package/dist/client.d.ts CHANGED
@@ -474,6 +474,42 @@ export declare class DayOfWeekClient {
474
474
  }>;
475
475
  }): Promise<SharedSkillSummary>;
476
476
  archiveSharedSkill(skillId: string): Promise<SharedSkillSummary>;
477
+ listTimeProjects(org?: string): Promise<{
478
+ projects: any[];
479
+ }>;
480
+ getTimeProject(projectId: string, org?: string): Promise<any>;
481
+ upsertTimeProject(payload: Record<string, unknown>, org?: string): Promise<{
482
+ projectId: string;
483
+ }>;
484
+ listTimeEntries(opts?: {
485
+ project?: string;
486
+ user?: string;
487
+ from?: string;
488
+ to?: string;
489
+ status?: string;
490
+ org?: string;
491
+ }): Promise<{
492
+ total: number;
493
+ rows: any[];
494
+ }>;
495
+ importTimeEntries(input: {
496
+ projectId: string;
497
+ entries: unknown[];
498
+ dryRun: boolean;
499
+ org?: string;
500
+ }): Promise<any>;
501
+ listPendingTimeMonths(org?: string): Promise<{
502
+ approvals: any[];
503
+ }>;
504
+ actOnTimeMonth(input: {
505
+ projectId: string;
506
+ month: string;
507
+ action: "submit" | "approve" | "return" | "reopen";
508
+ userId?: string;
509
+ userEmail?: string;
510
+ comment?: string;
511
+ org?: string;
512
+ }): Promise<any>;
477
513
  getSchema(): Promise<any>;
478
514
  private get;
479
515
  private post;
package/dist/client.js CHANGED
@@ -678,6 +678,59 @@ export class DayOfWeekClient {
678
678
  async archiveSharedSkill(skillId) {
679
679
  return this.delete(`/brain/skills/${encodeURIComponent(skillId)}`);
680
680
  }
681
+ // ── Time tracking ─────────────────────────────────────────────────────────
682
+ //
683
+ // Hours registered against time projects. Every rule (periods, frames,
684
+ // self-financing, the reportable basis, who may approve) is evaluated by
685
+ // the server; the client only carries JSON and prints what comes back.
686
+ async listTimeProjects(org) {
687
+ const qs = org ? `?org=${encodeURIComponent(org)}` : "";
688
+ return this.get(`/time/projects${qs}`);
689
+ }
690
+ async getTimeProject(projectId, org) {
691
+ const params = new URLSearchParams({ id: projectId });
692
+ if (org)
693
+ params.set("org", org);
694
+ return this.get(`/time/projects?${params.toString()}`);
695
+ }
696
+ async upsertTimeProject(payload, org) {
697
+ const qs = org ? `?org=${encodeURIComponent(org)}` : "";
698
+ return this.post(`/time/projects${qs}`, payload);
699
+ }
700
+ async listTimeEntries(opts) {
701
+ const params = new URLSearchParams();
702
+ if (opts?.project)
703
+ params.set("project", opts.project);
704
+ if (opts?.user)
705
+ params.set("user", opts.user);
706
+ if (opts?.from)
707
+ params.set("from", opts.from);
708
+ if (opts?.to)
709
+ params.set("to", opts.to);
710
+ if (opts?.status)
711
+ params.set("status", opts.status);
712
+ if (opts?.org)
713
+ params.set("org", opts.org);
714
+ const qs = params.toString();
715
+ return this.get(`/time/entries${qs ? `?${qs}` : ""}`);
716
+ }
717
+ async importTimeEntries(input) {
718
+ const qs = input.org ? `?org=${encodeURIComponent(input.org)}` : "";
719
+ return this.post(`/time/entries${qs}`, {
720
+ projectId: input.projectId,
721
+ dryRun: input.dryRun,
722
+ entries: input.entries,
723
+ });
724
+ }
725
+ async listPendingTimeMonths(org) {
726
+ const qs = org ? `?org=${encodeURIComponent(org)}` : "";
727
+ return this.get(`/time/months${qs}`);
728
+ }
729
+ async actOnTimeMonth(input) {
730
+ const { org, ...body } = input;
731
+ const qs = org ? `?org=${encodeURIComponent(org)}` : "";
732
+ return this.post(`/time/months${qs}`, body);
733
+ }
681
734
  // ── Schema ────────────────────────────────────────────────────────────────
682
735
  async getSchema() {
683
736
  return this.get("/schema");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dayofweek/dcli",
3
- "version": "1.11.0",
3
+ "version": "1.12.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",