exponential-cli 1.16.0 → 1.18.1

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/index.js CHANGED
@@ -2298,17 +2298,49 @@ function statusBadge(status) {
2298
2298
  }
2299
2299
  function outputTimeLogJson(result) {
2300
2300
  console.log(
2301
- JSON.stringify({ outcome: result.outcome, entry: transformTimeEntry(result.entry) }, null, 2)
2301
+ JSON.stringify(
2302
+ {
2303
+ outcome: result.outcome,
2304
+ entry: result.entry ? transformTimeEntry(result.entry) : null,
2305
+ pieces: (result.pieces ?? []).map(transformTimeEntry),
2306
+ mergedInto: result.mergedInto ?? []
2307
+ },
2308
+ null,
2309
+ 2
2310
+ )
2302
2311
  );
2303
2312
  }
2304
2313
  function outputTimeLogPretty(result) {
2305
- const e = result.entry;
2306
- const verb = { created: "Logged", updated: "Updated", left: "Left untouched (already confirmed)" }[result.outcome];
2307
- console.log(
2308
- `${chalk3.green("\u2713")} ${verb}: ${clock(e.startedAt)}\u2013${e.endedAt ? clock(e.endedAt) : "now"} ${chalk3.bold(formatMinutes(entryMinutes(e)))} ${statusBadge(e.status)} ${e.action?.name ?? e.actionId}`
2309
- );
2310
- if (e.note) console.log(chalk3.gray(` ${e.note}`));
2311
- console.log(chalk3.gray(` ID: ${e.id}${e.sourceRef ? ` ref: ${e.sourceRef}` : ""} owner: ${e.userId}`));
2314
+ if (result.outcome === "merged") {
2315
+ console.log(
2316
+ `${chalk3.green("\u2713")} Merged into your manual time (${(result.mergedInto ?? []).length} entr${(result.mergedInto ?? []).length === 1 ? "y" : "ies"} annotated); nothing new written.`
2317
+ );
2318
+ return;
2319
+ }
2320
+ if (result.outcome === "dropped" || !result.entry) {
2321
+ console.log(`${chalk3.yellow("\u2013")} Dropped: your manual time already covers every minute of it.`);
2322
+ return;
2323
+ }
2324
+ const pieces = result.pieces && result.pieces.length > 0 ? result.pieces : [result.entry];
2325
+ const verb = { created: "Logged", updated: "Updated", left: "Left untouched (already confirmed)" }[result.outcome] ?? result.outcome;
2326
+ for (const e of pieces) {
2327
+ console.log(
2328
+ `${chalk3.green("\u2713")} ${verb}: ${clock(e.startedAt)}\u2013${e.endedAt ? clock(e.endedAt) : "now"} ${chalk3.bold(formatMinutes(entryMinutes(e)))} ${statusBadge(e.status)} ${e.action?.name ?? e.actionId}`
2329
+ );
2330
+ if (e.note) console.log(chalk3.gray(` ${e.note}`));
2331
+ console.log(chalk3.gray(` ID: ${e.id}${e.sourceRef ? ` ref: ${e.sourceRef}` : ""} owner: ${e.userId}`));
2332
+ }
2333
+ if (pieces.length > 1) console.log(chalk3.gray(` split into ${pieces.length} pieces around your manual time`));
2334
+ }
2335
+ function outputTimeConfirmJson(result, date) {
2336
+ console.log(JSON.stringify({ date, confirmed: result.confirmed }, null, 2));
2337
+ }
2338
+ function outputTimeConfirmPretty(result, date) {
2339
+ if (result.confirmed === 0) {
2340
+ console.log(chalk3.gray(`Nothing proposed on ${date}; nothing to confirm.`));
2341
+ return;
2342
+ }
2343
+ console.log(`${chalk3.green("\u2713")} Confirmed ${result.confirmed} ${result.confirmed === 1 ? "entry" : "entries"} on ${date}.`);
2312
2344
  }
2313
2345
  function outputTimeEntriesJson(entries, date) {
2314
2346
  const rows = entries.map(transformTimeEntry);
@@ -2386,6 +2418,59 @@ function outputTimeBatchPretty(results) {
2386
2418
  console.log(chalk3.bold(`
2387
2419
  ${ok}/${results.length} entries logged`));
2388
2420
  }
2421
+ function outputDayReportJson(report, date) {
2422
+ console.log(
2423
+ JSON.stringify(
2424
+ {
2425
+ date,
2426
+ attentionMinutes: report.attentionMinutes,
2427
+ sessionMinutes: report.sessionMinutes,
2428
+ agentRunMinutes: report.agentRunMinutes,
2429
+ unassignedCount: report.unassignedCount,
2430
+ proposedCount: report.proposedCount,
2431
+ byProduct: report.byProduct,
2432
+ byAction: report.byAction,
2433
+ flags: report.flags,
2434
+ entries: report.entries.map((e) => ({
2435
+ ...transformTimeEntry(e),
2436
+ productId: e.productId,
2437
+ productName: e.productName,
2438
+ isAgentRun: e.isAgentRun,
2439
+ flags: e.flags,
2440
+ ticket: e.action.ticket ? { id: e.action.ticket.id, number: e.action.ticket.number, shortId: e.action.ticket.shortId } : null
2441
+ }))
2442
+ },
2443
+ null,
2444
+ 2
2445
+ )
2446
+ );
2447
+ }
2448
+ function outputDayReportPretty(report, date) {
2449
+ console.log(chalk3.bold(`
2450
+ ${date}`));
2451
+ console.log(
2452
+ ` ${chalk3.bold(formatMinutes(report.attentionMinutes))} attention \xB7 ${formatMinutes(report.sessionMinutes)} session \xB7 ${formatMinutes(report.agentRunMinutes)} agent-run` + (report.proposedCount > 0 ? chalk3.yellow(` \xB7 ${report.proposedCount} proposed`) : "") + (report.unassignedCount > 0 ? chalk3.yellow(` \xB7 ${report.unassignedCount} unassigned`) : "")
2453
+ );
2454
+ if (report.entries.length === 0) {
2455
+ console.log(chalk3.gray(" No time recorded."));
2456
+ return;
2457
+ }
2458
+ console.log(chalk3.bold("\nBy product"));
2459
+ for (const row of report.byProduct) {
2460
+ console.log(` ${formatMinutes(row.minutes).padStart(7)} ${row.productId ? row.name : chalk3.yellow(row.name)}`);
2461
+ }
2462
+ console.log(chalk3.bold("\nBy action"));
2463
+ for (const row of report.byAction) {
2464
+ const ticket = row.ticket ? chalk3.gray(` ${row.ticket.shortId ?? `#${row.ticket.number}`}`) : row.projectId ? chalk3.gray(` \xB7 ${row.projectName ?? "project"}`) : chalk3.yellow(" [unassigned]");
2465
+ const agent = row.agentRunMinutes > 0 ? chalk3.gray(` (+${formatMinutes(row.agentRunMinutes)} agent-run)`) : "";
2466
+ const proposed = row.proposedCount > 0 ? chalk3.yellow(` ${row.proposedCount} proposed`) : "";
2467
+ console.log(` ${formatMinutes(row.minutes).padStart(7)} ${row.name}${ticket}${agent}${proposed}`);
2468
+ }
2469
+ if (report.flags.length > 0) {
2470
+ console.log(chalk3.yellow(`
2471
+ ${report.flags.length} manual entr${report.flags.length === 1 ? "y looks" : "ies look"} like a forgotten timer`));
2472
+ }
2473
+ }
2389
2474
 
2390
2475
  // src/commands/actions.ts
2391
2476
  var VALID_PRIORITIES = [
@@ -6240,6 +6325,7 @@ function createTicketsCommand() {
6240
6325
 
6241
6326
  // src/commands/time.ts
6242
6327
  import { Command as Command22 } from "commander";
6328
+ import { segmentConversation } from "exponential-sdk";
6243
6329
  var SOURCES2 = ["manual", "claude-desktop", "agent-run"];
6244
6330
  var STATUSES2 = ["PROPOSED", "CONFIRMED"];
6245
6331
  var OWNERSHIP_NOTE = "Run with an external-agent key, the entry belongs to the agent's OWNER and is always PROPOSED; a personal token logs your own time, CONFIRMED by default.";
@@ -6411,8 +6497,91 @@ function createTimeCommand() {
6411
6497
  handleError(error);
6412
6498
  }
6413
6499
  });
6500
+ time.command("confirm").description(
6501
+ "Confirm a day: every PROPOSED entry of yours that starts that day becomes CONFIRMED and the Actions' spent time moves. Human only \u2014 run it with your personal credentials; an agent key is refused."
6502
+ ).requiredOption("-d, --date <YYYY-MM-DD>", "The day (local time)").option("-w, --workspace <slug|id>", "Only entries in this workspace (default: all)").action(async (options, cmd) => {
6503
+ const globalOpts = cmd.optsWithGlobals();
6504
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
6505
+ try {
6506
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(options.date)) {
6507
+ throw new Error(`--date must be YYYY-MM-DD, got "${options.date}".`);
6508
+ }
6509
+ const client = getClient();
6510
+ const workspaceId = options.workspace ? await resolveWorkspaceId(client, options.workspace) : void 0;
6511
+ const result = await client.time.confirmDay(options.date, workspaceId);
6512
+ if (useJson) outputTimeConfirmJson(result, options.date);
6513
+ else outputTimeConfirmPretty(result, options.date);
6514
+ } catch (error) {
6515
+ handleError(error);
6516
+ }
6517
+ });
6518
+ time.command("report").description(
6519
+ "One day's report \u2014 the numbers the /time Day tab shows: attention hours (each covered minute once), session hours (the plain sum), agent-run time on its own, hours by Product and by Action with overlapping minutes split evenly, and the unassigned and proposed counts. JSON when piped or with --json."
6520
+ ).requiredOption("-d, --date <YYYY-MM-DD>", "The day (local time)").option("-w, --workspace <slug|id>", "Only entries in this workspace (default: all)").action(async (options, cmd) => {
6521
+ const globalOpts = cmd.optsWithGlobals();
6522
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
6523
+ try {
6524
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(options.date)) {
6525
+ throw new Error(`--date must be YYYY-MM-DD, got "${options.date}".`);
6526
+ }
6527
+ const client = getClient();
6528
+ const workspaceId = options.workspace ? await resolveWorkspaceId(client, options.workspace) : void 0;
6529
+ const report = await client.time.dayReport(options.date, workspaceId);
6530
+ if (useJson) outputDayReportJson(report, options.date);
6531
+ else outputDayReportPretty(report, options.date);
6532
+ } catch (error) {
6533
+ handleError(error);
6534
+ }
6535
+ });
6536
+ time.command("segment").description(
6537
+ [
6538
+ "Cut one conversation's messages into Worklog segments and print the batch `time log --from-file -` consumes.",
6539
+ 'Input: a JSON array of {at, role} (role user|assistant), or {"messages": [...]}.',
6540
+ "A gap over 30 minutes between the person's messages ends a segment; bounds round outward to 5 minutes; assistant-only runs longer than the gap become agent-run entries."
6541
+ ].join(" ")
6542
+ ).requiredOption("--from-file <path>", 'Messages JSON ("-" = stdin)').requiredOption("-a, --action <id>", "Action CUID every entry is logged on").requiredOption("--ref-prefix <prefix>", "sourceRef prefix, e.g. claude-session:<sessionId>; segments get #0, #1, \u2026").option("--gap <minutes>", "Silence between the person's messages that ends a segment", "30").option("--note <text>", "Note copied onto every entry").action(
6543
+ async (options) => {
6544
+ try {
6545
+ const gap = Number.parseInt(options.gap, 10);
6546
+ if (Number.isNaN(gap) || gap <= 0) throw new Error(`--gap must be a positive integer, got "${options.gap}".`);
6547
+ const messages = parseMessagesFile(readText(void 0, options.fromFile));
6548
+ const segments = segmentConversation(messages, gap);
6549
+ const entries = segments.map((segment, index) => ({
6550
+ actionId: options.action,
6551
+ startedAt: segment.start.toISOString(),
6552
+ endedAt: segment.end.toISOString(),
6553
+ source: segment.humanTurns === 0 ? "agent-run" : "claude-desktop",
6554
+ sourceRef: `${options.refPrefix}#${index}`,
6555
+ ...options.note ? { note: options.note } : {}
6556
+ }));
6557
+ console.log(JSON.stringify(entries, null, 2));
6558
+ } catch (error) {
6559
+ handleError(error);
6560
+ }
6561
+ }
6562
+ );
6414
6563
  return time;
6415
6564
  }
6565
+ function parseMessagesFile(raw) {
6566
+ let parsed;
6567
+ try {
6568
+ parsed = JSON.parse(raw);
6569
+ } catch (error) {
6570
+ throw new Error(
6571
+ `--from-file must be JSON: ${error instanceof Error ? error.message : String(error)}`
6572
+ );
6573
+ }
6574
+ const items = Array.isArray(parsed) ? parsed : typeof parsed === "object" && parsed !== null && Array.isArray(parsed.messages) ? parsed.messages : null;
6575
+ if (!items) throw new Error('--from-file must contain a JSON array of {at, role} messages, or {"messages": [...]}.');
6576
+ return items.map((item, i) => {
6577
+ if (typeof item !== "object" || item === null) throw new Error(`Message[${i}] is not an object.`);
6578
+ const { at, role } = item;
6579
+ if (typeof at !== "string" && !(at instanceof Date)) throw new Error(`Message[${i}].at must be an ISO timestamp.`);
6580
+ if (Number.isNaN(new Date(at).getTime())) throw new Error(`Message[${i}].at is not a valid timestamp: "${String(at)}".`);
6581
+ if (role !== "user" && role !== "assistant") throw new Error(`Message[${i}].role must be "user" or "assistant".`);
6582
+ return { at, role };
6583
+ });
6584
+ }
6416
6585
 
6417
6586
  // src/commands/workspaces.ts
6418
6587
  import { Command as Command23 } from "commander";