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 +177 -8
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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(
|
|
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
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
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";
|