logwork-cli 1.1.1 → 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.
Files changed (70) hide show
  1. package/README.md +53 -25
  2. package/RELEASE.md +4 -2
  3. package/bin/logwork.mjs +2 -2
  4. package/commands/commits.mjs +128 -13
  5. package/commands/history.mjs +0 -4
  6. package/commands/mcp.mjs +266 -0
  7. package/commands/quick.mjs +38 -23
  8. package/commands/reminder.mjs +41 -2
  9. package/commands/setup.mjs +197 -42
  10. package/commands/tui.mjs +1 -0
  11. package/docs/advanced.md +18 -7
  12. package/docs/mcp-setup.md +120 -26
  13. package/docs/operations-guide.md +50 -2
  14. package/docs/recipes.md +40 -0
  15. package/docs/security.md +23 -2
  16. package/examples/mcp/antigravity.mcp_config.json +2 -2
  17. package/examples/mcp/codex.config.toml +2 -2
  18. package/examples/mcp/copilot.mcp.json +2 -2
  19. package/lib/api-auth-forms.mjs +393 -0
  20. package/lib/api-auth-http.mjs +184 -0
  21. package/lib/api-auth-support.mjs +101 -0
  22. package/lib/api-auth-tokens.mjs +129 -0
  23. package/lib/api-auth.mjs +39 -813
  24. package/lib/api.mjs +38 -1422
  25. package/lib/apply-ledger.mjs +1 -1
  26. package/lib/batch-workflow.mjs +187 -2
  27. package/lib/commit-workflow.mjs +345 -23
  28. package/lib/config.mjs +7 -0
  29. package/lib/diagnostics.mjs +116 -4
  30. package/lib/jira-api.mjs +25 -3
  31. package/lib/jira-workflow.mjs +36 -6
  32. package/lib/launcher.mjs +303 -0
  33. package/lib/logwork-config.mjs +64 -13
  34. package/lib/manual-apply-state.mjs +15 -0
  35. package/lib/manual-commands.mjs +2 -2
  36. package/lib/manual-ink-app.mjs +142 -1282
  37. package/lib/manual-ink-draft-actions.mjs +227 -0
  38. package/lib/manual-ink-interaction.mjs +318 -0
  39. package/lib/manual-ink-session.mjs +41 -0
  40. package/lib/manual-ink-task-editor.mjs +338 -0
  41. package/lib/manual-ink-ui.mjs +38 -0
  42. package/lib/manual-ink-wizard.mjs +520 -0
  43. package/lib/manual-mcp.mjs +78 -20
  44. package/lib/manual-repl.mjs +171 -22
  45. package/lib/mcp-install.mjs +930 -0
  46. package/lib/mcp-server.mjs +51 -20
  47. package/lib/node-runtime.mjs +155 -0
  48. package/lib/paths.mjs +7 -0
  49. package/lib/project-mapping-workflow.mjs +32 -27
  50. package/lib/query-workflow.mjs +19 -1
  51. package/lib/reconciliation-workflow.mjs +1 -1
  52. package/lib/reminder-config.mjs +38 -2
  53. package/lib/reminder-scheduler.mjs +85 -18
  54. package/lib/reminder-service.mjs +42 -3
  55. package/lib/reminder-workflow.mjs +118 -22
  56. package/lib/repo-project-suggestions.mjs +285 -0
  57. package/lib/ro-api-diagnostics.mjs +109 -0
  58. package/lib/ro-api-entries.mjs +359 -0
  59. package/lib/ro-api-fields.mjs +290 -0
  60. package/lib/ro-api-http.mjs +68 -0
  61. package/lib/ro-api-projects.mjs +137 -0
  62. package/lib/ro-api-tasks.mjs +50 -0
  63. package/lib/ro-api-timesheet.mjs +413 -0
  64. package/lib/ro-delete-workflow.mjs +2 -1
  65. package/lib/ro-entry-reader.mjs +11 -6
  66. package/lib/ro-resubmit-workflow.mjs +2 -1
  67. package/lib/terminal-auth-prompts.mjs +7 -4
  68. package/lib/ui.mjs +121 -56
  69. package/package.json +11 -4
  70. package/skills/logwork/SKILL.md +10 -4
@@ -0,0 +1,520 @@
1
+ // Logwork wizard flow for the Ink app: target, date and project pickers, live
2
+ // preview, apply and cancel. Called from ManualApp with its state and setters.
3
+ import { useCallback } from 'react';
4
+ import {
5
+ buildCurrentWeekDateOptions,
6
+ buildLogworkText,
7
+ buildProjectOptions,
8
+ buildProjectOverrides,
9
+ buildWeekDateOptions,
10
+ canApplyPreview,
11
+ formatDraftPreview
12
+ } from './manual-logwork-wizard.mjs';
13
+ import {
14
+ BOTH_TARGET,
15
+ JIRA_TARGET,
16
+ previewTargets,
17
+ RO_TARGET,
18
+ targetLabel
19
+ } from './manual-targets.mjs';
20
+ import { loadManualDrafts } from './manual-drafts.mjs';
21
+ import { enrichRoPreviewWithJiraProjects } from './project-mapping-workflow.mjs';
22
+ import {
23
+ clearSessionPreviews,
24
+ hasSessionPreview,
25
+ sessionPreview,
26
+ setSessionPreview
27
+ } from './manual-ink-session.mjs';
28
+
29
+ export function useLogworkWizard(ctx) {
30
+ const {
31
+ cwd,
32
+ sessionRef,
33
+ wizard,
34
+ setWizard,
35
+ setActivePanel,
36
+ setLoading,
37
+ setInputValue,
38
+ setSelectedIndex,
39
+ inkWorkflows,
40
+ prompts,
41
+ runWithInlineAuth,
42
+ runWithJiraAuthInstruction,
43
+ executeCommand
44
+ } = ctx;
45
+
46
+ const startNewLogworkWizard = useCallback(async (target = RO_TARGET) => {
47
+ setInputValue('');
48
+ setSelectedIndex(0);
49
+ setLoading(target === JIRA_TARGET ? 'Building current week...' : 'Fetching current week...');
50
+ setActivePanel({
51
+ kind: 'logwork',
52
+ title: 'Logwork',
53
+ text: target === JIRA_TARGET
54
+ ? 'Preparing Jira worklog dates...'
55
+ : 'Loading this week booking/logwork data...'
56
+ });
57
+ try {
58
+ const weekly = target === JIRA_TARGET
59
+ ? null
60
+ : await runWithInlineAuth(() => inkWorkflows.queryLogwork({
61
+ period: 'this_week',
62
+ cwd,
63
+ includeEntries: false
64
+ }));
65
+ const dateOptions = weekly ? buildWeekDateOptions(weekly) : buildCurrentWeekDateOptions();
66
+ if (!dateOptions.length) {
67
+ throw new Error('Unable to build week date options.');
68
+ }
69
+ const todayIndex = dateOptions.findIndex((option) => option.isToday);
70
+ setWizard({
71
+ step: 'select_date',
72
+ target,
73
+ weekly,
74
+ dateOptions,
75
+ dateIndex: todayIndex >= 0 ? todayIndex : 0,
76
+ projectsResult: null,
77
+ projectOptions: [],
78
+ projectIndex: 0,
79
+ selectedDate: null,
80
+ selectedProject: null,
81
+ tasks: [],
82
+ preview: null,
83
+ activeDraftId: null
84
+ });
85
+ setActivePanel({
86
+ kind: 'logwork',
87
+ title: 'Select Date',
88
+ text: `Choose a day for this ${targetLabel(target)} logwork session.`
89
+ });
90
+ } catch (error) {
91
+ setActivePanel({
92
+ kind: 'error',
93
+ title: 'Logwork Error',
94
+ text: error.message
95
+ });
96
+ } finally {
97
+ setLoading('');
98
+ }
99
+ }, [cwd, inkWorkflows, runWithInlineAuth, runWithJiraAuthInstruction]);
100
+
101
+ const startLogworkTargetPicker = useCallback(() => {
102
+ setInputValue('');
103
+ setSelectedIndex(0);
104
+ setWizard({
105
+ step: 'select_logwork_target',
106
+ targetIndex: 0
107
+ });
108
+ setActivePanel({
109
+ kind: 'logwork',
110
+ title: 'Select Logwork Target',
111
+ text: 'Choose where this logwork session should write.'
112
+ });
113
+ }, []);
114
+
115
+ const startAuthTargetPicker = useCallback(() => {
116
+ setInputValue('');
117
+ setSelectedIndex(0);
118
+ setWizard({
119
+ step: 'select_auth_target',
120
+ targetIndex: 0
121
+ });
122
+ setActivePanel({
123
+ kind: 'auth',
124
+ title: 'Select Auth Target',
125
+ text: 'Choose which service to authenticate.'
126
+ });
127
+ }, []);
128
+
129
+ const startReconciliationPeriodPicker = useCallback(() => {
130
+ setInputValue('');
131
+ setSelectedIndex(0);
132
+ setWizard({
133
+ step: 'select_reconciliation_period',
134
+ periodIndex: 2
135
+ });
136
+ setActivePanel({
137
+ kind: 'output',
138
+ title: 'RO / Jira Reconciliation',
139
+ text: 'Choose a preset period to compare.'
140
+ });
141
+ }, []);
142
+
143
+ const startLogworkWizard = useCallback(async (target) => {
144
+ if (!target) {
145
+ startLogworkTargetPicker();
146
+ return;
147
+ }
148
+
149
+ if (target !== RO_TARGET) {
150
+ await startNewLogworkWizard(target);
151
+ return;
152
+ }
153
+
154
+ setInputValue('');
155
+ setSelectedIndex(0);
156
+ setLoading('Loading saved drafts...');
157
+ try {
158
+ const drafts = await loadManualDrafts({ cwd });
159
+ if (drafts.length) {
160
+ setWizard({
161
+ step: 'select_draft',
162
+ target: RO_TARGET,
163
+ drafts,
164
+ draftIndex: 0,
165
+ draftSource: 'start',
166
+ weekly: null,
167
+ dateOptions: [],
168
+ dateIndex: 0,
169
+ projectsResult: null,
170
+ projectOptions: [],
171
+ projectIndex: 0,
172
+ selectedDate: null,
173
+ selectedProject: null,
174
+ tasks: [],
175
+ preview: null,
176
+ activeDraftId: null
177
+ });
178
+ setActivePanel({
179
+ kind: 'logwork',
180
+ title: 'Saved Drafts',
181
+ text: 'Resume a saved draft, delete one, or start a new logwork session.'
182
+ });
183
+ return;
184
+ }
185
+ await startNewLogworkWizard(RO_TARGET);
186
+ } catch (error) {
187
+ setActivePanel({
188
+ kind: 'error',
189
+ title: 'Draft Error',
190
+ text: error.message
191
+ });
192
+ } finally {
193
+ setLoading('');
194
+ }
195
+ }, [cwd, startLogworkTargetPicker, startNewLogworkWizard]);
196
+
197
+ const startMcpClientPicker = useCallback(() => {
198
+ setInputValue('');
199
+ setSelectedIndex(0);
200
+ setWizard({
201
+ step: 'select_mcp_client',
202
+ mcpClientIndex: 0
203
+ });
204
+ setActivePanel({
205
+ kind: 'output',
206
+ title: 'MCP Setup',
207
+ text: 'Choose an MCP client to print copy-ready setup.'
208
+ });
209
+ }, []);
210
+
211
+ const cancelMcpClientPicker = useCallback(() => {
212
+ setWizard(null);
213
+ setActivePanel({
214
+ kind: 'idle',
215
+ title: 'Ready',
216
+ text: 'Type / for commands.'
217
+ });
218
+ }, []);
219
+
220
+ const selectWizardDate = useCallback(async (dateOption) => {
221
+ const target = wizard?.target || RO_TARGET;
222
+ if (target === JIRA_TARGET) {
223
+ const draft = {
224
+ target,
225
+ date: dateOption.date,
226
+ project: null,
227
+ tasks: [],
228
+ preview: null
229
+ };
230
+ sessionRef.current.logworkDraft = draft;
231
+ clearSessionPreviews(sessionRef.current);
232
+ sessionRef.current.activeTarget = target;
233
+ setWizard((current) => ({
234
+ ...current,
235
+ step: 'edit_tasks',
236
+ selectedDate: dateOption.date,
237
+ selectedProject: null,
238
+ tasks: [],
239
+ preview: null,
240
+ previews: {}
241
+ }));
242
+ setActivePanel({
243
+ kind: 'logwork',
244
+ title: 'Jira Worklog Draft',
245
+ text: [
246
+ formatDraftPreview(draft),
247
+ '',
248
+ 'Enter tasks one per line: +2 Feature Development | check ui/ux (SCB-213)',
249
+ 'Empty Enter applies after preview is ready.',
250
+ 'Type / for task commands. Esc cancels.'
251
+ ].join('\n')
252
+ });
253
+ return;
254
+ }
255
+
256
+ setLoading('Fetching project memberships...');
257
+ setActivePanel({
258
+ kind: 'logwork',
259
+ title: 'Select Project',
260
+ text: `Loading projects for ${dateOption.date}...`
261
+ });
262
+ try {
263
+ const projectsResult = await runWithInlineAuth(() => inkWorkflows.listLogworkProjects({ cwd }));
264
+ const projectOptions = buildProjectOptions({
265
+ projectsResult,
266
+ weekly: wizard.weekly,
267
+ date: dateOption.date
268
+ });
269
+ if (!projectOptions.length) {
270
+ throw new Error('No Resource Optimiser project memberships found.');
271
+ }
272
+ setWizard((current) => ({
273
+ ...current,
274
+ step: 'select_project',
275
+ target,
276
+ selectedDate: dateOption.date,
277
+ projectsResult,
278
+ projectOptions,
279
+ projectIndex: 0,
280
+ selectedProject: null,
281
+ tasks: [],
282
+ preview: null
283
+ }));
284
+ setActivePanel({
285
+ kind: 'logwork',
286
+ title: 'Select Project',
287
+ text: `Choose a project for ${dateOption.date}.`
288
+ });
289
+ } catch (error) {
290
+ setActivePanel({
291
+ kind: 'error',
292
+ title: 'Project Error',
293
+ text: error.message
294
+ });
295
+ } finally {
296
+ setLoading('');
297
+ }
298
+ }, [cwd, inkWorkflows, runWithInlineAuth, wizard]);
299
+
300
+ const selectWizardProject = useCallback((project) => {
301
+ const target = wizard?.target || RO_TARGET;
302
+ const draft = {
303
+ target,
304
+ date: wizard.selectedDate,
305
+ project,
306
+ tasks: [],
307
+ preview: null
308
+ };
309
+ sessionRef.current.logworkDraft = draft;
310
+ clearSessionPreviews(sessionRef.current);
311
+ sessionRef.current.activeTarget = target;
312
+ setWizard((current) => ({
313
+ ...current,
314
+ step: 'edit_tasks',
315
+ target,
316
+ selectedProject: project,
317
+ tasks: [],
318
+ preview: null,
319
+ previews: {}
320
+ }));
321
+ setActivePanel({
322
+ kind: 'logwork',
323
+ title: target === BOTH_TARGET ? 'RO + Jira Draft' : 'Logwork Draft',
324
+ text: [
325
+ formatDraftPreview(draft),
326
+ '',
327
+ target === BOTH_TARGET ? 'Enter tasks one per line: +2 Feature Development | check ui/ux (SCB-213)' : 'Enter tasks one per line: +2 Feature Development | check ui/ux',
328
+ 'Empty Enter applies after preview is ready.',
329
+ 'Type / for task commands. Esc cancels.'
330
+ ].join('\n')
331
+ });
332
+ }, [wizard]);
333
+
334
+ const previewDraft = useCallback(async (nextWizard) => {
335
+ const target = nextWizard.target || RO_TARGET;
336
+ const draft = {
337
+ target,
338
+ date: nextWizard.selectedDate,
339
+ project: nextWizard.selectedProject,
340
+ tasks: nextWizard.tasks
341
+ };
342
+
343
+ if (!draft.tasks.length) {
344
+ sessionRef.current.logworkDraft = draft;
345
+ clearSessionPreviews(sessionRef.current);
346
+ sessionRef.current.activeTarget = target;
347
+ setWizard(nextWizard);
348
+ setActivePanel({
349
+ kind: 'logwork',
350
+ title: target === JIRA_TARGET ? 'Jira Worklog Draft' : target === BOTH_TARGET ? 'RO + Jira Draft' : 'Logwork Draft',
351
+ text: [
352
+ formatDraftPreview(draft),
353
+ '',
354
+ target === RO_TARGET ? 'Enter tasks one per line: +2 Feature Development | check ui/ux' : 'Enter tasks one per line: +2 Feature Development | check ui/ux (SCB-213)',
355
+ 'Empty Enter applies after preview is ready.'
356
+ ].join('\n')
357
+ });
358
+ return;
359
+ }
360
+
361
+ setLoading('Building live preview...');
362
+ try {
363
+ const text = buildLogworkText({
364
+ date: draft.date,
365
+ tasks: draft.tasks
366
+ });
367
+ const previews = {};
368
+ if (previewTargets(target).includes(RO_TARGET)) {
369
+ const projectOverrides = buildProjectOverrides({
370
+ date: draft.date,
371
+ tasks: draft.tasks,
372
+ projectMemberId: draft.project.projectMemberId
373
+ });
374
+ previews[RO_TARGET] = await runWithInlineAuth(() => inkWorkflows.previewLogworkBatch({
375
+ text,
376
+ projectOverrides,
377
+ cwd
378
+ }));
379
+ }
380
+ if (previewTargets(target).includes(JIRA_TARGET)) {
381
+ previews[JIRA_TARGET] = await runWithJiraAuthInstruction(() => inkWorkflows.previewJiraWorklogBatch({
382
+ text
383
+ }));
384
+ }
385
+ if (target === BOTH_TARGET && previews[RO_TARGET] && previews[JIRA_TARGET]) {
386
+ previews[RO_TARGET] = enrichRoPreviewWithJiraProjects(previews[RO_TARGET], previews[JIRA_TARGET]);
387
+ }
388
+ const preview = target === JIRA_TARGET ? previews[JIRA_TARGET] : previews[RO_TARGET];
389
+ const withPreview = {
390
+ ...draft,
391
+ preview,
392
+ previews
393
+ };
394
+ sessionRef.current.logworkDraft = withPreview;
395
+ sessionRef.current.activeTarget = target;
396
+ for (const previewTarget of previewTargets(target)) {
397
+ setSessionPreview(sessionRef.current, previewTarget, previews[previewTarget] || null);
398
+ }
399
+ setWizard({
400
+ ...nextWizard,
401
+ target,
402
+ preview,
403
+ previews
404
+ });
405
+ const hasBlockedJira = previews[JIRA_TARGET]?.entries?.some((entry) => entry.status !== 'ready');
406
+ setActivePanel({
407
+ kind: preview?.unresolvedEntries?.length || preview?.errors?.length || hasBlockedJira ? 'error' : 'logwork',
408
+ title: target === JIRA_TARGET ? 'Jira Worklog Preview' : target === BOTH_TARGET ? 'RO + Jira Preview' : 'Logwork Preview',
409
+ text: [
410
+ formatDraftPreview(withPreview),
411
+ '',
412
+ 'Empty Enter applies this preview.',
413
+ 'Type another +hours task to add more.',
414
+ '/remove selects tasks to delete.'
415
+ ].join('\n')
416
+ });
417
+ return preview;
418
+ } catch (error) {
419
+ setActivePanel({
420
+ kind: 'error',
421
+ title: 'Preview Error',
422
+ text: error.message
423
+ });
424
+ } finally {
425
+ setLoading('');
426
+ }
427
+ }, [cwd, inkWorkflows, runWithInlineAuth, runWithJiraAuthInstruction]);
428
+
429
+ const cancelCurrentLogwork = useCallback(async () => {
430
+ if (wizard?.tasks?.length) {
431
+ const ok = await prompts.confirm('Discard this unsaved logwork session?', false);
432
+ if (!ok) {
433
+ return;
434
+ }
435
+ }
436
+ sessionRef.current.logworkDraft = null;
437
+ clearSessionPreviews(sessionRef.current);
438
+ setWizard(null);
439
+ setActivePanel({
440
+ kind: 'idle',
441
+ title: 'Ready',
442
+ text: 'Logwork session discarded. Type / for commands.'
443
+ });
444
+ }, [prompts, wizard]);
445
+
446
+ const applyWizardPreview = useCallback(async () => {
447
+ const target = wizard.target || RO_TARGET;
448
+ if (!wizard.tasks.length) {
449
+ setActivePanel({
450
+ kind: 'logwork',
451
+ title: 'Logwork Draft',
452
+ text: target === RO_TARGET ? 'Enter a task like +2 Feature Development | check ui/ux.' : 'Enter a task like +2 Feature Development | check ui/ux (SCB-213).'
453
+ });
454
+ return;
455
+ }
456
+
457
+ const targets = previewTargets(target);
458
+ const missingPreview = targets.some((previewTarget) => !sessionPreview(sessionRef.current, previewTarget));
459
+ if (missingPreview) {
460
+ await previewDraft(wizard);
461
+ return;
462
+ }
463
+
464
+ for (const previewTarget of targets) {
465
+ const applyState = canApplyPreview(sessionPreview(sessionRef.current, previewTarget), { target: previewTarget });
466
+ if (!applyState.ok) {
467
+ setActivePanel({
468
+ kind: 'error',
469
+ title: 'Apply Blocked',
470
+ text: `${targetLabel(previewTarget)}: ${applyState.reason}`
471
+ });
472
+ return;
473
+ }
474
+ }
475
+
476
+ await executeCommand({ type: 'apply', target });
477
+ if (!hasSessionPreview(sessionRef.current)) {
478
+ setWizard(null);
479
+ }
480
+ }, [executeCommand, previewDraft, wizard]);
481
+
482
+ const restoreTaskEditorPanel = useCallback((nextWizard = wizard) => {
483
+ const target = nextWizard.target || RO_TARGET;
484
+ const draft = {
485
+ target,
486
+ date: nextWizard.selectedDate,
487
+ project: nextWizard.selectedProject,
488
+ tasks: nextWizard.tasks,
489
+ preview: nextWizard.preview,
490
+ previews: nextWizard.previews
491
+ };
492
+ setActivePanel({
493
+ kind: 'logwork',
494
+ title: nextWizard.preview ? `${targetLabel(target)} Preview` : `${targetLabel(target)} Draft`,
495
+ text: [
496
+ formatDraftPreview(draft),
497
+ '',
498
+ nextWizard.preview ? 'Empty Enter applies this preview.' : target === RO_TARGET ? 'Enter tasks one per line: +2 Feature Development | check ui/ux' : 'Enter tasks one per line: +2 Feature Development | check ui/ux (SCB-213)',
499
+ nextWizard.preview ? 'Type another +hours task to add more.' : 'Empty Enter applies after preview is ready.',
500
+ 'Type / for task commands. Esc cancels.'
501
+ ].join('\n')
502
+ });
503
+ }, [wizard]);
504
+
505
+ return {
506
+ startNewLogworkWizard,
507
+ startLogworkTargetPicker,
508
+ startAuthTargetPicker,
509
+ startReconciliationPeriodPicker,
510
+ startLogworkWizard,
511
+ startMcpClientPicker,
512
+ cancelMcpClientPicker,
513
+ selectWizardDate,
514
+ selectWizardProject,
515
+ previewDraft,
516
+ cancelCurrentLogwork,
517
+ applyWizardPreview,
518
+ restoreTaskEditorPanel
519
+ };
520
+ }