logwork-cli 1.1.0 → 1.2.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 +35 -9
  2. package/RELEASE.md +1 -0
  3. package/commands/commits.mjs +57 -6
  4. package/commands/history.mjs +0 -4
  5. package/commands/quick.mjs +38 -23
  6. package/commands/reminder.mjs +41 -2
  7. package/commands/setup.mjs +157 -19
  8. package/commands/tui.mjs +1 -0
  9. package/docs/advanced.md +12 -2
  10. package/docs/mcp-setup.md +57 -15
  11. package/docs/operations-guide.md +37 -2
  12. package/docs/recipes.md +27 -0
  13. package/docs/security.md +3 -1
  14. package/examples/mcp/antigravity.mcp_config.json +1 -1
  15. package/examples/mcp/codex.config.toml +1 -1
  16. package/examples/mcp/copilot.mcp.json +1 -1
  17. package/lib/api-auth-forms.mjs +393 -0
  18. package/lib/api-auth-http.mjs +184 -0
  19. package/lib/api-auth-support.mjs +101 -0
  20. package/lib/api-auth-tokens.mjs +129 -0
  21. package/lib/api-auth.mjs +39 -813
  22. package/lib/api.mjs +38 -1422
  23. package/lib/apply-ledger.mjs +1 -1
  24. package/lib/batch-parser.mjs +23 -0
  25. package/lib/batch-workflow.mjs +201 -2
  26. package/lib/commit-workflow.mjs +278 -30
  27. package/lib/config.mjs +7 -0
  28. package/lib/diagnostics.mjs +53 -4
  29. package/lib/jira-api.mjs +25 -3
  30. package/lib/jira-workflow.mjs +36 -6
  31. package/lib/keychain.mjs +19 -12
  32. package/lib/logwork-config.mjs +64 -13
  33. package/lib/manual-apply-state.mjs +15 -0
  34. package/lib/manual-commands.mjs +2 -2
  35. package/lib/manual-ink-app.mjs +108 -1281
  36. package/lib/manual-ink-draft-actions.mjs +227 -0
  37. package/lib/manual-ink-interaction.mjs +306 -0
  38. package/lib/manual-ink-session.mjs +41 -0
  39. package/lib/manual-ink-task-editor.mjs +338 -0
  40. package/lib/manual-ink-wizard.mjs +520 -0
  41. package/lib/manual-mcp.mjs +27 -15
  42. package/lib/manual-repl.mjs +39 -3
  43. package/lib/mcp-server.mjs +63 -21
  44. package/lib/node-runtime.mjs +115 -0
  45. package/lib/npm-command.mjs +29 -0
  46. package/lib/paths.mjs +11 -1
  47. package/lib/project-mapping-workflow.mjs +30 -7
  48. package/lib/query-workflow.mjs +0 -1
  49. package/lib/reconciliation-workflow.mjs +1 -1
  50. package/lib/release-doctor.mjs +4 -2
  51. package/lib/reminder-config.mjs +38 -2
  52. package/lib/reminder-scheduler.mjs +54 -3
  53. package/lib/reminder-service.mjs +28 -3
  54. package/lib/reminder-workflow.mjs +118 -22
  55. package/lib/ro-api-diagnostics.mjs +109 -0
  56. package/lib/ro-api-entries.mjs +359 -0
  57. package/lib/ro-api-fields.mjs +290 -0
  58. package/lib/ro-api-http.mjs +68 -0
  59. package/lib/ro-api-projects.mjs +137 -0
  60. package/lib/ro-api-tasks.mjs +50 -0
  61. package/lib/ro-api-timesheet.mjs +413 -0
  62. package/lib/ro-delete-workflow.mjs +5 -8
  63. package/lib/ro-edit-workflow.mjs +66 -8
  64. package/lib/ro-entry-reader.mjs +49 -6
  65. package/lib/ro-resubmit-workflow.mjs +35 -21
  66. package/lib/terminal-auth-prompts.mjs +7 -4
  67. package/lib/ui.mjs +80 -59
  68. package/lib/update-service.mjs +5 -2
  69. package/package.json +11 -4
  70. package/skills/logwork/SKILL.md +6 -4
@@ -0,0 +1,227 @@
1
+ // Saved-draft actions for the Ink logwork wizard: save, list, resume, delete.
2
+ import { useCallback } from 'react';
3
+ import {
4
+ buildProjectOptions,
5
+ buildWeekDateOptions,
6
+ formatDraftPreview
7
+ } from './manual-logwork-wizard.mjs';
8
+ import { JIRA_TARGET, RO_TARGET } from './manual-targets.mjs';
9
+ import {
10
+ deleteManualDraft,
11
+ formatManualDraftLabel,
12
+ loadManualDrafts,
13
+ saveManualDraft
14
+ } from './manual-drafts.mjs';
15
+ import { sameProjectIdentity } from './manual-ink-ui.mjs';
16
+ import { clearSessionPreviews } from './manual-ink-session.mjs';
17
+
18
+ export function useDraftActions(ctx) {
19
+ const {
20
+ cwd,
21
+ sessionRef,
22
+ wizard,
23
+ setWizard,
24
+ setActivePanel,
25
+ setLoading,
26
+ inkWorkflows,
27
+ prompts,
28
+ runWithInlineAuth,
29
+ previewDraft,
30
+ startNewLogworkWizard
31
+ } = ctx;
32
+
33
+ const saveCurrentDraft = useCallback(async () => {
34
+ if (wizard?.target === JIRA_TARGET) {
35
+ setActivePanel({
36
+ kind: 'logwork',
37
+ title: 'Save Draft',
38
+ text: 'Saved drafts are only supported for Resource Optimiser logwork in this version.'
39
+ });
40
+ return null;
41
+ }
42
+
43
+ if (!wizard?.selectedDate || !wizard?.selectedProject || !wizard?.tasks?.length) {
44
+ setActivePanel({
45
+ kind: 'logwork',
46
+ title: 'Save Draft',
47
+ text: 'Add at least one task before saving a draft.'
48
+ });
49
+ return null;
50
+ }
51
+
52
+ try {
53
+ const saved = await saveManualDraft({
54
+ id: wizard.activeDraftId,
55
+ cwd,
56
+ date: wizard.selectedDate,
57
+ project: wizard.selectedProject,
58
+ tasks: wizard.tasks,
59
+ latestPreviewStatus: wizard.preview?.status
60
+ }, { cwd });
61
+ setWizard((current) => ({
62
+ ...current,
63
+ activeDraftId: saved.id
64
+ }));
65
+ setActivePanel({
66
+ kind: 'success',
67
+ title: 'Draft Saved',
68
+ text: [
69
+ `Saved ${saved.tasks.length} task${saved.tasks.length === 1 ? '' : 's'} locally.`,
70
+ formatManualDraftLabel(saved),
71
+ '',
72
+ 'Continue editing, press empty Enter to apply, or type /drafts.'
73
+ ].join('\n')
74
+ });
75
+ return saved;
76
+ } catch (error) {
77
+ setActivePanel({
78
+ kind: 'error',
79
+ title: 'Save Draft Error',
80
+ text: error.message
81
+ });
82
+ return null;
83
+ }
84
+ }, [cwd, wizard]);
85
+
86
+ const openDraftPicker = useCallback(async (source = 'editor') => {
87
+ setLoading('Loading saved drafts...');
88
+ try {
89
+ const drafts = await loadManualDrafts({ cwd });
90
+ if (!drafts.length) {
91
+ setActivePanel({
92
+ kind: 'logwork',
93
+ title: 'Saved Drafts',
94
+ text: 'No saved drafts found.'
95
+ });
96
+ return;
97
+ }
98
+ setWizard((current) => ({
99
+ ...current,
100
+ step: 'select_draft',
101
+ drafts,
102
+ draftIndex: 0,
103
+ draftSource: source
104
+ }));
105
+ setActivePanel({
106
+ kind: 'logwork',
107
+ title: 'Saved Drafts',
108
+ text: 'Enter resumes · d deletes · Esc returns.'
109
+ });
110
+ } catch (error) {
111
+ setActivePanel({
112
+ kind: 'error',
113
+ title: 'Draft Error',
114
+ text: error.message
115
+ });
116
+ } finally {
117
+ setLoading('');
118
+ }
119
+ }, [cwd]);
120
+
121
+ const resumeManualDraft = useCallback(async (draft) => {
122
+ setLoading('Resuming saved draft...');
123
+ try {
124
+ const weekly = await runWithInlineAuth(() => inkWorkflows.queryLogwork({
125
+ period: 'this_week',
126
+ cwd,
127
+ includeEntries: false
128
+ }));
129
+ const dateOptions = buildWeekDateOptions(weekly);
130
+ const projectsResult = await runWithInlineAuth(() => inkWorkflows.listLogworkProjects({ cwd }));
131
+ const projectOptions = buildProjectOptions({
132
+ projectsResult,
133
+ weekly,
134
+ date: draft.date
135
+ });
136
+ const selectedProject = projectOptions.find((project) => sameProjectIdentity(project, draft.project)) || draft.project;
137
+ const nextWizard = {
138
+ step: 'edit_tasks',
139
+ target: RO_TARGET,
140
+ weekly,
141
+ dateOptions,
142
+ dateIndex: Math.max(0, dateOptions.findIndex((option) => option.date === draft.date)),
143
+ projectsResult,
144
+ projectOptions,
145
+ projectIndex: Math.max(0, projectOptions.findIndex((project) => sameProjectIdentity(project, selectedProject))),
146
+ selectedDate: draft.date,
147
+ selectedProject,
148
+ tasks: draft.tasks,
149
+ preview: null,
150
+ activeDraftId: draft.id
151
+ };
152
+ sessionRef.current.logworkDraft = {
153
+ target: RO_TARGET,
154
+ date: draft.date,
155
+ project: selectedProject,
156
+ tasks: draft.tasks,
157
+ preview: null
158
+ };
159
+ clearSessionPreviews(sessionRef.current);
160
+ sessionRef.current.activeTarget = RO_TARGET;
161
+ await previewDraft(nextWizard);
162
+ } catch (error) {
163
+ setActivePanel({
164
+ kind: 'error',
165
+ title: 'Resume Draft Error',
166
+ text: error.message
167
+ });
168
+ } finally {
169
+ setLoading('');
170
+ }
171
+ }, [cwd, inkWorkflows, previewDraft, runWithInlineAuth]);
172
+
173
+ const deleteSavedDraft = useCallback(async (draft) => {
174
+ const ok = await prompts.confirm(`Delete saved draft "${formatManualDraftLabel(draft)}"?`, false);
175
+ if (!ok) {
176
+ return;
177
+ }
178
+ await deleteManualDraft(draft.id);
179
+ const drafts = await loadManualDrafts({ cwd });
180
+ setWizard((current) => ({
181
+ ...current,
182
+ drafts,
183
+ draftIndex: 0
184
+ }));
185
+ setActivePanel({
186
+ kind: 'success',
187
+ title: 'Draft Deleted',
188
+ text: drafts.length ? 'Draft deleted. Choose another saved draft or start new.' : 'Draft deleted. No saved drafts remain.'
189
+ });
190
+ }, [cwd, prompts]);
191
+
192
+ const returnFromDraftPicker = useCallback(() => {
193
+ if (wizard?.draftSource === 'start') {
194
+ startNewLogworkWizard(RO_TARGET);
195
+ return;
196
+ }
197
+ setWizard((current) => ({
198
+ ...current,
199
+ step: 'edit_tasks',
200
+ draftSource: undefined
201
+ }));
202
+ const draft = {
203
+ target: wizard?.target || RO_TARGET,
204
+ date: wizard?.selectedDate,
205
+ project: wizard?.selectedProject,
206
+ tasks: wizard?.tasks || [],
207
+ preview: wizard?.preview
208
+ };
209
+ setActivePanel({
210
+ kind: 'logwork',
211
+ title: wizard?.preview ? 'Logwork Preview' : 'Logwork Draft',
212
+ text: [
213
+ formatDraftPreview(draft),
214
+ '',
215
+ 'Type / for task commands. Empty Enter applies when ready.'
216
+ ].join('\n')
217
+ });
218
+ }, [startNewLogworkWizard, wizard]);
219
+
220
+ return {
221
+ saveCurrentDraft,
222
+ openDraftPicker,
223
+ resumeManualDraft,
224
+ deleteSavedDraft,
225
+ returnFromDraftPicker
226
+ };
227
+ }
@@ -0,0 +1,306 @@
1
+ // The bottom interaction area of the Ink app: the auth prompt, a confirm dialog,
2
+ // the active wizard picker, or the command input. A plain render function (not a
3
+ // component) so the element tree matches the single-component layout.
4
+ import React from 'react';
5
+ import { toggleTaskSelection } from './manual-logwork-wizard.mjs';
6
+ import { AUTH_TARGETS, LOGWORK_TARGETS, RO_TARGET } from './manual-targets.mjs';
7
+ import {
8
+ CommandInput,
9
+ ConfirmDialog,
10
+ DatePicker,
11
+ DraftPicker,
12
+ McpClientPicker,
13
+ ProjectPicker,
14
+ TargetPicker,
15
+ TaskEditPicker,
16
+ TaskRemovePicker
17
+ } from './manual-ink-ui.mjs';
18
+ import { AuthPrompt } from './manual-ink-auth.mjs';
19
+ import { RECONCILIATION_PERIOD_OPTIONS } from './manual-ink-session.mjs';
20
+
21
+ const h = React.createElement;
22
+
23
+ export function renderManualInteraction({
24
+ authPrompt,
25
+ authActive,
26
+ pendingConfirm,
27
+ wizard,
28
+ inputValue,
29
+ selectedIndex,
30
+ setInputValue,
31
+ setSelectedIndex,
32
+ setAuthPrompt,
33
+ setWizard,
34
+ setActivePanel,
35
+ resolveAuthPrompt,
36
+ rejectAuthPrompt,
37
+ resolveConfirm,
38
+ exit,
39
+ executeCommand,
40
+ submitInput,
41
+ startLogworkWizard,
42
+ startNewLogworkWizard,
43
+ selectWizardDate,
44
+ selectWizardProject,
45
+ cancelMcpClientPicker,
46
+ cancelCurrentLogwork,
47
+ restoreTaskEditorPanel,
48
+ resumeManualDraft,
49
+ deleteSavedDraft,
50
+ returnFromDraftPicker,
51
+ confirmRemoveTasks,
52
+ cancelRemoveTasks,
53
+ selectTaskForEdit
54
+ }) {
55
+ return authPrompt
56
+ ? h(AuthPrompt, {
57
+ prompt: authPrompt,
58
+ inputValue,
59
+ onInputChange(value) {
60
+ setInputValue(value);
61
+ },
62
+ onPromptChange(nextPrompt) {
63
+ setAuthPrompt(nextPrompt);
64
+ },
65
+ onResolve: resolveAuthPrompt,
66
+ onReject: rejectAuthPrompt
67
+ })
68
+ : authActive
69
+ ? h(AuthPrompt, {
70
+ prompt: { step: 'working' },
71
+ inputValue,
72
+ onInputChange(value) {
73
+ setInputValue(value);
74
+ },
75
+ onPromptChange(nextPrompt) {
76
+ setAuthPrompt(nextPrompt);
77
+ },
78
+ onResolve: resolveAuthPrompt,
79
+ onReject: rejectAuthPrompt
80
+ })
81
+ : pendingConfirm
82
+ ? h(ConfirmDialog, {
83
+ message: pendingConfirm.message,
84
+ initialValue: pendingConfirm.initialValue,
85
+ onResolve: resolveConfirm
86
+ })
87
+ : wizard?.step === 'select_logwork_target'
88
+ ? h(TargetPicker, {
89
+ title: 'Pick logwork target',
90
+ targets: LOGWORK_TARGETS,
91
+ selectedIndex: wizard.targetIndex || 0,
92
+ onChange(index) {
93
+ setWizard((current) => ({
94
+ ...current,
95
+ targetIndex: index
96
+ }));
97
+ },
98
+ onSelect(target) {
99
+ startLogworkWizard(target.key);
100
+ },
101
+ onCancel() {
102
+ setWizard(null);
103
+ setActivePanel({
104
+ kind: 'idle',
105
+ title: 'Ready',
106
+ text: 'Type / for commands.'
107
+ });
108
+ }
109
+ })
110
+ : wizard?.step === 'select_auth_target'
111
+ ? h(TargetPicker, {
112
+ title: 'Pick auth target',
113
+ targets: AUTH_TARGETS,
114
+ selectedIndex: wizard.targetIndex || 0,
115
+ onChange(index) {
116
+ setWizard((current) => ({
117
+ ...current,
118
+ targetIndex: index
119
+ }));
120
+ },
121
+ onSelect(target) {
122
+ executeCommand({
123
+ type: 'auth',
124
+ target: target.key
125
+ });
126
+ },
127
+ onCancel() {
128
+ setWizard(null);
129
+ setActivePanel({
130
+ kind: 'idle',
131
+ title: 'Ready',
132
+ text: 'Type / for commands.'
133
+ });
134
+ }
135
+ })
136
+ : wizard?.step === 'select_reconciliation_period'
137
+ ? h(TargetPicker, {
138
+ title: 'Pick reconciliation period',
139
+ targets: RECONCILIATION_PERIOD_OPTIONS,
140
+ selectedIndex: wizard.periodIndex || 0,
141
+ onChange(index) {
142
+ setWizard((current) => ({
143
+ ...current,
144
+ periodIndex: index
145
+ }));
146
+ },
147
+ onSelect(period) {
148
+ executeCommand({
149
+ type: 'reconcile',
150
+ period: period.key
151
+ });
152
+ },
153
+ onCancel() {
154
+ setWizard(null);
155
+ setActivePanel({
156
+ kind: 'idle',
157
+ title: 'Ready',
158
+ text: 'Type / for commands.'
159
+ });
160
+ }
161
+ })
162
+ : wizard?.step === 'select_draft'
163
+ ? h(DraftPicker, {
164
+ drafts: wizard.drafts || [],
165
+ selectedIndex: wizard.draftIndex || 0,
166
+ includeStartNew: wizard.draftSource === 'start',
167
+ onChange(index) {
168
+ setWizard((current) => ({
169
+ ...current,
170
+ draftIndex: index
171
+ }));
172
+ },
173
+ onResume(draft) {
174
+ resumeManualDraft(draft);
175
+ },
176
+ onDelete(draft) {
177
+ deleteSavedDraft(draft);
178
+ },
179
+ onStartNew() {
180
+ startNewLogworkWizard(RO_TARGET);
181
+ },
182
+ onCancel() {
183
+ returnFromDraftPicker();
184
+ }
185
+ })
186
+ : wizard?.step === 'select_date'
187
+ ? h(DatePicker, {
188
+ options: wizard.dateOptions,
189
+ selectedIndex: wizard.dateIndex,
190
+ onChange(index) {
191
+ setWizard((current) => ({
192
+ ...current,
193
+ dateIndex: index
194
+ }));
195
+ },
196
+ onSelect(option) {
197
+ selectWizardDate(option);
198
+ }
199
+ })
200
+ : wizard?.step === 'select_project'
201
+ ? h(ProjectPicker, {
202
+ options: wizard.projectOptions,
203
+ selectedIndex: wizard.projectIndex,
204
+ onChange(index) {
205
+ setWizard((current) => ({
206
+ ...current,
207
+ projectIndex: index
208
+ }));
209
+ },
210
+ onSelect(project) {
211
+ selectWizardProject(project);
212
+ }
213
+ })
214
+ : wizard?.step === 'select_mcp_client'
215
+ ? h(McpClientPicker, {
216
+ selectedIndex: wizard.mcpClientIndex || 0,
217
+ onChange(index) {
218
+ setWizard((current) => ({
219
+ ...current,
220
+ mcpClientIndex: index
221
+ }));
222
+ },
223
+ onSelect(client) {
224
+ executeCommand({
225
+ type: 'mcp',
226
+ client: client.key
227
+ });
228
+ },
229
+ onCancel() {
230
+ cancelMcpClientPicker();
231
+ }
232
+ })
233
+ : wizard?.step === 'remove_tasks'
234
+ ? h(TaskRemovePicker, {
235
+ tasks: wizard.tasks,
236
+ selectedIndex: wizard.removeIndex || 0,
237
+ selectedIndexes: wizard.removeSelectedIndexes || [],
238
+ onChange(index) {
239
+ setWizard((current) => ({
240
+ ...current,
241
+ removeIndex: index
242
+ }));
243
+ },
244
+ onToggle(index) {
245
+ setWizard((current) => ({
246
+ ...current,
247
+ removeSelectedIndexes: toggleTaskSelection(current.removeSelectedIndexes || [], index)
248
+ }));
249
+ },
250
+ onSubmit(indexes) {
251
+ confirmRemoveTasks(indexes);
252
+ },
253
+ onCancel() {
254
+ cancelRemoveTasks();
255
+ }
256
+ })
257
+ : wizard?.step === 'edit_task_select'
258
+ ? h(TaskEditPicker, {
259
+ tasks: wizard.tasks,
260
+ selectedIndex: wizard.editIndex || 0,
261
+ onChange(index) {
262
+ setWizard((current) => ({
263
+ ...current,
264
+ editIndex: index
265
+ }));
266
+ },
267
+ onSelect(index) {
268
+ selectTaskForEdit(index);
269
+ },
270
+ onCancel() {
271
+ setWizard((current) => ({
272
+ ...current,
273
+ step: 'edit_tasks'
274
+ }));
275
+ restoreTaskEditorPanel();
276
+ }
277
+ })
278
+ : h(CommandInput, {
279
+ value: inputValue,
280
+ onChange(value) {
281
+ setInputValue(value);
282
+ setSelectedIndex(0);
283
+ },
284
+ onSubmit: submitInput,
285
+ selectedIndex,
286
+ setSelectedIndex,
287
+ mode: wizard?.step === 'edit_tasks' ? 'task' : wizard?.step === 'edit_task_replace' ? 'task-edit' : 'command',
288
+ onEscape() {
289
+ if (!wizard) {
290
+ exit();
291
+ return;
292
+ }
293
+ if (wizard?.step === 'edit_tasks') {
294
+ cancelCurrentLogwork();
295
+ return;
296
+ }
297
+ if (wizard?.step === 'edit_task_replace') {
298
+ setWizard((current) => ({
299
+ ...current,
300
+ step: 'edit_tasks'
301
+ }));
302
+ restoreTaskEditorPanel();
303
+ }
304
+ }
305
+ });
306
+ }
@@ -0,0 +1,41 @@
1
+ // Manual session preview bookkeeping shared by the Ink app and its action hooks.
2
+ import { JIRA_TARGET, RO_TARGET } from './manual-targets.mjs';
3
+
4
+ export const RECONCILIATION_PERIOD_OPTIONS = [
5
+ { key: 'today', label: 'Today', description: 'Compare today' },
6
+ { key: 'yesterday', label: 'Yesterday', description: 'Compare yesterday' },
7
+ { key: 'this_week', label: 'This week', description: 'Compare Monday through Sunday' },
8
+ { key: 'last_week', label: 'Last week', description: 'Compare the previous week' },
9
+ { key: 'this_month', label: 'This month', description: 'Compare the current month' },
10
+ { key: 'last_month', label: 'Last month', description: 'Compare the previous month' }
11
+ ];
12
+
13
+ export function setSessionPreview(session, target, preview) {
14
+ session.activeTarget = target;
15
+ session.previews = {
16
+ ...(session.previews || {}),
17
+ [target]: preview
18
+ };
19
+ }
20
+
21
+ export function clearSessionPreviews(session) {
22
+ session.previews = {
23
+ [RO_TARGET]: null,
24
+ [JIRA_TARGET]: null
25
+ };
26
+ }
27
+
28
+ export function sessionPreview(session, target) {
29
+ return session.previews?.[target] || null;
30
+ }
31
+
32
+ export function hasSessionPreview(session) {
33
+ return Boolean(session.previews?.[RO_TARGET] || session.previews?.[JIRA_TARGET]);
34
+ }
35
+
36
+ export function activeSessionPreview(session) {
37
+ if (session.activeTarget === JIRA_TARGET) {
38
+ return sessionPreview(session, JIRA_TARGET);
39
+ }
40
+ return sessionPreview(session, RO_TARGET);
41
+ }