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,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,318 @@
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
+ SelectPrompt,
15
+ TargetPicker,
16
+ TaskEditPicker,
17
+ TaskRemovePicker
18
+ } from './manual-ink-ui.mjs';
19
+ import { AuthPrompt } from './manual-ink-auth.mjs';
20
+ import { RECONCILIATION_PERIOD_OPTIONS } from './manual-ink-session.mjs';
21
+
22
+ const h = React.createElement;
23
+
24
+ export function renderManualInteraction({
25
+ authPrompt,
26
+ authActive,
27
+ pendingConfirm,
28
+ pendingSelect,
29
+ wizard,
30
+ inputValue,
31
+ selectedIndex,
32
+ setInputValue,
33
+ setSelectedIndex,
34
+ setAuthPrompt,
35
+ setWizard,
36
+ setActivePanel,
37
+ resolveAuthPrompt,
38
+ rejectAuthPrompt,
39
+ resolveConfirm,
40
+ changeSelect,
41
+ resolveSelect,
42
+ exit,
43
+ executeCommand,
44
+ submitInput,
45
+ startLogworkWizard,
46
+ startNewLogworkWizard,
47
+ selectWizardDate,
48
+ selectWizardProject,
49
+ cancelMcpClientPicker,
50
+ cancelCurrentLogwork,
51
+ restoreTaskEditorPanel,
52
+ resumeManualDraft,
53
+ deleteSavedDraft,
54
+ returnFromDraftPicker,
55
+ confirmRemoveTasks,
56
+ cancelRemoveTasks,
57
+ selectTaskForEdit
58
+ }) {
59
+ return authPrompt
60
+ ? h(AuthPrompt, {
61
+ prompt: authPrompt,
62
+ inputValue,
63
+ onInputChange(value) {
64
+ setInputValue(value);
65
+ },
66
+ onPromptChange(nextPrompt) {
67
+ setAuthPrompt(nextPrompt);
68
+ },
69
+ onResolve: resolveAuthPrompt,
70
+ onReject: rejectAuthPrompt
71
+ })
72
+ : authActive
73
+ ? h(AuthPrompt, {
74
+ prompt: { step: 'working' },
75
+ inputValue,
76
+ onInputChange(value) {
77
+ setInputValue(value);
78
+ },
79
+ onPromptChange(nextPrompt) {
80
+ setAuthPrompt(nextPrompt);
81
+ },
82
+ onResolve: resolveAuthPrompt,
83
+ onReject: rejectAuthPrompt
84
+ })
85
+ : pendingConfirm
86
+ ? h(ConfirmDialog, {
87
+ message: pendingConfirm.message,
88
+ initialValue: pendingConfirm.initialValue,
89
+ onResolve: resolveConfirm
90
+ })
91
+ : pendingSelect
92
+ ? h(SelectPrompt, {
93
+ message: pendingSelect.message,
94
+ options: pendingSelect.options,
95
+ selectedIndex: pendingSelect.selectedIndex || 0,
96
+ onChange: changeSelect,
97
+ onResolve: resolveSelect
98
+ })
99
+ : wizard?.step === 'select_logwork_target'
100
+ ? h(TargetPicker, {
101
+ title: 'Pick logwork target',
102
+ targets: LOGWORK_TARGETS,
103
+ selectedIndex: wizard.targetIndex || 0,
104
+ onChange(index) {
105
+ setWizard((current) => ({
106
+ ...current,
107
+ targetIndex: index
108
+ }));
109
+ },
110
+ onSelect(target) {
111
+ startLogworkWizard(target.key);
112
+ },
113
+ onCancel() {
114
+ setWizard(null);
115
+ setActivePanel({
116
+ kind: 'idle',
117
+ title: 'Ready',
118
+ text: 'Type / for commands.'
119
+ });
120
+ }
121
+ })
122
+ : wizard?.step === 'select_auth_target'
123
+ ? h(TargetPicker, {
124
+ title: 'Pick auth target',
125
+ targets: AUTH_TARGETS,
126
+ selectedIndex: wizard.targetIndex || 0,
127
+ onChange(index) {
128
+ setWizard((current) => ({
129
+ ...current,
130
+ targetIndex: index
131
+ }));
132
+ },
133
+ onSelect(target) {
134
+ executeCommand({
135
+ type: 'auth',
136
+ target: target.key
137
+ });
138
+ },
139
+ onCancel() {
140
+ setWizard(null);
141
+ setActivePanel({
142
+ kind: 'idle',
143
+ title: 'Ready',
144
+ text: 'Type / for commands.'
145
+ });
146
+ }
147
+ })
148
+ : wizard?.step === 'select_reconciliation_period'
149
+ ? h(TargetPicker, {
150
+ title: 'Pick reconciliation period',
151
+ targets: RECONCILIATION_PERIOD_OPTIONS,
152
+ selectedIndex: wizard.periodIndex || 0,
153
+ onChange(index) {
154
+ setWizard((current) => ({
155
+ ...current,
156
+ periodIndex: index
157
+ }));
158
+ },
159
+ onSelect(period) {
160
+ executeCommand({
161
+ type: 'reconcile',
162
+ period: period.key
163
+ });
164
+ },
165
+ onCancel() {
166
+ setWizard(null);
167
+ setActivePanel({
168
+ kind: 'idle',
169
+ title: 'Ready',
170
+ text: 'Type / for commands.'
171
+ });
172
+ }
173
+ })
174
+ : wizard?.step === 'select_draft'
175
+ ? h(DraftPicker, {
176
+ drafts: wizard.drafts || [],
177
+ selectedIndex: wizard.draftIndex || 0,
178
+ includeStartNew: wizard.draftSource === 'start',
179
+ onChange(index) {
180
+ setWizard((current) => ({
181
+ ...current,
182
+ draftIndex: index
183
+ }));
184
+ },
185
+ onResume(draft) {
186
+ resumeManualDraft(draft);
187
+ },
188
+ onDelete(draft) {
189
+ deleteSavedDraft(draft);
190
+ },
191
+ onStartNew() {
192
+ startNewLogworkWizard(RO_TARGET);
193
+ },
194
+ onCancel() {
195
+ returnFromDraftPicker();
196
+ }
197
+ })
198
+ : wizard?.step === 'select_date'
199
+ ? h(DatePicker, {
200
+ options: wizard.dateOptions,
201
+ selectedIndex: wizard.dateIndex,
202
+ onChange(index) {
203
+ setWizard((current) => ({
204
+ ...current,
205
+ dateIndex: index
206
+ }));
207
+ },
208
+ onSelect(option) {
209
+ selectWizardDate(option);
210
+ }
211
+ })
212
+ : wizard?.step === 'select_project'
213
+ ? h(ProjectPicker, {
214
+ options: wizard.projectOptions,
215
+ selectedIndex: wizard.projectIndex,
216
+ onChange(index) {
217
+ setWizard((current) => ({
218
+ ...current,
219
+ projectIndex: index
220
+ }));
221
+ },
222
+ onSelect(project) {
223
+ selectWizardProject(project);
224
+ }
225
+ })
226
+ : wizard?.step === 'select_mcp_client'
227
+ ? h(McpClientPicker, {
228
+ selectedIndex: wizard.mcpClientIndex || 0,
229
+ onChange(index) {
230
+ setWizard((current) => ({
231
+ ...current,
232
+ mcpClientIndex: index
233
+ }));
234
+ },
235
+ onSelect(client) {
236
+ executeCommand({
237
+ type: 'mcp',
238
+ client: client.key
239
+ });
240
+ },
241
+ onCancel() {
242
+ cancelMcpClientPicker();
243
+ }
244
+ })
245
+ : wizard?.step === 'remove_tasks'
246
+ ? h(TaskRemovePicker, {
247
+ tasks: wizard.tasks,
248
+ selectedIndex: wizard.removeIndex || 0,
249
+ selectedIndexes: wizard.removeSelectedIndexes || [],
250
+ onChange(index) {
251
+ setWizard((current) => ({
252
+ ...current,
253
+ removeIndex: index
254
+ }));
255
+ },
256
+ onToggle(index) {
257
+ setWizard((current) => ({
258
+ ...current,
259
+ removeSelectedIndexes: toggleTaskSelection(current.removeSelectedIndexes || [], index)
260
+ }));
261
+ },
262
+ onSubmit(indexes) {
263
+ confirmRemoveTasks(indexes);
264
+ },
265
+ onCancel() {
266
+ cancelRemoveTasks();
267
+ }
268
+ })
269
+ : wizard?.step === 'edit_task_select'
270
+ ? h(TaskEditPicker, {
271
+ tasks: wizard.tasks,
272
+ selectedIndex: wizard.editIndex || 0,
273
+ onChange(index) {
274
+ setWizard((current) => ({
275
+ ...current,
276
+ editIndex: index
277
+ }));
278
+ },
279
+ onSelect(index) {
280
+ selectTaskForEdit(index);
281
+ },
282
+ onCancel() {
283
+ setWizard((current) => ({
284
+ ...current,
285
+ step: 'edit_tasks'
286
+ }));
287
+ restoreTaskEditorPanel();
288
+ }
289
+ })
290
+ : h(CommandInput, {
291
+ value: inputValue,
292
+ onChange(value) {
293
+ setInputValue(value);
294
+ setSelectedIndex(0);
295
+ },
296
+ onSubmit: submitInput,
297
+ selectedIndex,
298
+ setSelectedIndex,
299
+ mode: wizard?.step === 'edit_tasks' ? 'task' : wizard?.step === 'edit_task_replace' ? 'task-edit' : 'command',
300
+ onEscape() {
301
+ if (!wizard) {
302
+ exit();
303
+ return;
304
+ }
305
+ if (wizard?.step === 'edit_tasks') {
306
+ cancelCurrentLogwork();
307
+ return;
308
+ }
309
+ if (wizard?.step === 'edit_task_replace') {
310
+ setWizard((current) => ({
311
+ ...current,
312
+ step: 'edit_tasks'
313
+ }));
314
+ restoreTaskEditorPanel();
315
+ }
316
+ }
317
+ });
318
+ }
@@ -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
+ }