logwork-cli 1.1.1 → 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.
- package/README.md +23 -7
- package/RELEASE.md +1 -0
- package/commands/commits.mjs +57 -6
- package/commands/history.mjs +0 -4
- package/commands/quick.mjs +38 -23
- package/commands/reminder.mjs +41 -2
- package/commands/setup.mjs +50 -16
- package/commands/tui.mjs +1 -0
- package/docs/advanced.md +12 -2
- package/docs/mcp-setup.md +56 -14
- package/docs/operations-guide.md +35 -2
- package/docs/recipes.md +27 -0
- package/docs/security.md +3 -1
- package/examples/mcp/antigravity.mcp_config.json +1 -1
- package/examples/mcp/codex.config.toml +1 -1
- package/examples/mcp/copilot.mcp.json +1 -1
- package/lib/api-auth-forms.mjs +393 -0
- package/lib/api-auth-http.mjs +184 -0
- package/lib/api-auth-support.mjs +101 -0
- package/lib/api-auth-tokens.mjs +129 -0
- package/lib/api-auth.mjs +39 -813
- package/lib/api.mjs +38 -1422
- package/lib/apply-ledger.mjs +1 -1
- package/lib/batch-workflow.mjs +187 -2
- package/lib/commit-workflow.mjs +188 -21
- package/lib/config.mjs +7 -0
- package/lib/diagnostics.mjs +53 -4
- package/lib/jira-api.mjs +25 -3
- package/lib/jira-workflow.mjs +36 -6
- package/lib/logwork-config.mjs +64 -13
- package/lib/manual-apply-state.mjs +15 -0
- package/lib/manual-commands.mjs +2 -2
- package/lib/manual-ink-app.mjs +108 -1281
- package/lib/manual-ink-draft-actions.mjs +227 -0
- package/lib/manual-ink-interaction.mjs +306 -0
- package/lib/manual-ink-session.mjs +41 -0
- package/lib/manual-ink-task-editor.mjs +338 -0
- package/lib/manual-ink-wizard.mjs +520 -0
- package/lib/manual-mcp.mjs +27 -15
- package/lib/manual-repl.mjs +39 -3
- package/lib/mcp-server.mjs +51 -20
- package/lib/node-runtime.mjs +115 -0
- package/lib/paths.mjs +7 -0
- package/lib/project-mapping-workflow.mjs +30 -7
- package/lib/query-workflow.mjs +0 -1
- package/lib/reconciliation-workflow.mjs +1 -1
- package/lib/reminder-config.mjs +38 -2
- package/lib/reminder-scheduler.mjs +54 -3
- package/lib/reminder-service.mjs +28 -3
- package/lib/reminder-workflow.mjs +118 -22
- package/lib/ro-api-diagnostics.mjs +109 -0
- package/lib/ro-api-entries.mjs +359 -0
- package/lib/ro-api-fields.mjs +290 -0
- package/lib/ro-api-http.mjs +68 -0
- package/lib/ro-api-projects.mjs +137 -0
- package/lib/ro-api-tasks.mjs +50 -0
- package/lib/ro-api-timesheet.mjs +413 -0
- package/lib/ro-delete-workflow.mjs +2 -1
- package/lib/ro-entry-reader.mjs +11 -6
- package/lib/ro-resubmit-workflow.mjs +2 -1
- package/lib/terminal-auth-prompts.mjs +7 -4
- package/lib/ui.mjs +80 -59
- package/package.json +11 -4
- package/skills/logwork/SKILL.md +5 -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
|
+
}
|
package/lib/manual-mcp.mjs
CHANGED
|
@@ -39,11 +39,14 @@ export const MCP_CLIENTS = [
|
|
|
39
39
|
}
|
|
40
40
|
];
|
|
41
41
|
|
|
42
|
-
// IDE configs run
|
|
43
|
-
//
|
|
44
|
-
// without the npm global bin directory on
|
|
42
|
+
// IDE configs run `<absolute node> <runtime>/bin/logwork.mjs mcp`. Both paths
|
|
43
|
+
// are absolute because GUI IDEs (Cursor, VS Code started from the Dock or
|
|
44
|
+
// Finder) often start without the npm global bin directory or nvm's node on
|
|
45
|
+
// PATH, so neither `logwork` nor a bare `node` would be found.
|
|
45
46
|
export const MCP_SERVER_NAME = 'logwork';
|
|
46
47
|
|
|
48
|
+
export const MCP_NODE_PATH_NOTE = 'The config uses the absolute path of the Node that ran this command. If Node is upgraded or moved (nvm, Homebrew, a new installer), re-run `logwork setup` and paste the new config.';
|
|
49
|
+
|
|
47
50
|
export function defaultMcpServerPath() {
|
|
48
51
|
return join(helperHome(), 'bin', 'logwork.mjs');
|
|
49
52
|
}
|
|
@@ -83,6 +86,7 @@ export function formatMcpClientOptions(clients = MCP_CLIENTS) {
|
|
|
83
86
|
export function formatMcpSetup({
|
|
84
87
|
client,
|
|
85
88
|
serverPath = defaultMcpServerPath(),
|
|
89
|
+
nodePath = process.execPath,
|
|
86
90
|
platform = process.platform
|
|
87
91
|
} = {}) {
|
|
88
92
|
const resolvedClient = typeof client === 'object' ? client : resolveMcpClient(client);
|
|
@@ -92,6 +96,7 @@ export function formatMcpSetup({
|
|
|
92
96
|
|
|
93
97
|
const lines = [
|
|
94
98
|
`MCP setup: ${resolvedClient.label}`,
|
|
99
|
+
`Node: ${nodePath}`,
|
|
95
100
|
`Entry path: ${serverPath} (run with the "mcp" argument)`,
|
|
96
101
|
''
|
|
97
102
|
];
|
|
@@ -100,34 +105,35 @@ export function formatMcpSetup({
|
|
|
100
105
|
lines.push(
|
|
101
106
|
`Copy this TOML into ${resolvedClient.target}:`,
|
|
102
107
|
'',
|
|
103
|
-
formatCodexToml(serverPath)
|
|
108
|
+
formatCodexToml(serverPath, { nodePath })
|
|
104
109
|
);
|
|
105
110
|
} else if (resolvedClient.key === 'copilot') {
|
|
106
111
|
lines.push(
|
|
107
112
|
`Copy this JSON into ${resolvedClient.target}:`,
|
|
108
113
|
'',
|
|
109
|
-
JSON.stringify(vscodeMcpConfig(serverPath), null, 2)
|
|
114
|
+
JSON.stringify(vscodeMcpConfig(serverPath, { nodePath }), null, 2)
|
|
110
115
|
);
|
|
111
116
|
} else if (resolvedClient.key === 'claude-code') {
|
|
112
117
|
lines.push(
|
|
113
118
|
'Recommended command:',
|
|
114
119
|
'',
|
|
115
|
-
`claude mcp add --transport stdio ${MCP_SERVER_NAME} --
|
|
120
|
+
`claude mcp add --transport stdio ${MCP_SERVER_NAME} -- ${quoteShellArg(nodePath, { platform })} ${quoteShellArg(serverPath, { platform })} mcp`,
|
|
116
121
|
'',
|
|
117
122
|
'Or copy this JSON into project .mcp.json:',
|
|
118
123
|
'',
|
|
119
|
-
JSON.stringify(stdioMcpServersConfig(serverPath), null, 2)
|
|
124
|
+
JSON.stringify(stdioMcpServersConfig(serverPath, { nodePath }), null, 2)
|
|
120
125
|
);
|
|
121
126
|
} else {
|
|
122
127
|
lines.push(
|
|
123
128
|
`Copy this JSON into ${resolvedClient.target}:`,
|
|
124
129
|
'',
|
|
125
|
-
JSON.stringify(stdioMcpServersConfig(serverPath), null, 2)
|
|
130
|
+
JSON.stringify(stdioMcpServersConfig(serverPath, { nodePath }), null, 2)
|
|
126
131
|
);
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
lines.push(
|
|
130
135
|
'',
|
|
136
|
+
MCP_NODE_PATH_NOTE,
|
|
131
137
|
'After saving, restart or reload the MCP client.',
|
|
132
138
|
'Do not paste passwords, 2FA codes, Bearer tokens, cookies, or raw auth logs into AI chat.'
|
|
133
139
|
);
|
|
@@ -142,41 +148,47 @@ function normalizeMcpClientValue(value) {
|
|
|
142
148
|
.replace(/[\s_]+/g, '-');
|
|
143
149
|
}
|
|
144
150
|
|
|
145
|
-
export function stdioMcpServersConfig(serverPath) {
|
|
151
|
+
export function stdioMcpServersConfig(serverPath, { nodePath = process.execPath } = {}) {
|
|
146
152
|
return {
|
|
147
153
|
mcpServers: {
|
|
148
154
|
[MCP_SERVER_NAME]: {
|
|
149
|
-
command:
|
|
155
|
+
command: nodePath,
|
|
150
156
|
args: [serverPath, 'mcp']
|
|
151
157
|
}
|
|
152
158
|
}
|
|
153
159
|
};
|
|
154
160
|
}
|
|
155
161
|
|
|
156
|
-
export function vscodeMcpConfig(serverPath) {
|
|
162
|
+
export function vscodeMcpConfig(serverPath, { nodePath = process.execPath } = {}) {
|
|
157
163
|
return {
|
|
158
164
|
servers: {
|
|
159
165
|
[MCP_SERVER_NAME]: {
|
|
160
166
|
type: 'stdio',
|
|
161
|
-
command:
|
|
167
|
+
command: nodePath,
|
|
162
168
|
args: [serverPath, 'mcp']
|
|
163
169
|
}
|
|
164
170
|
}
|
|
165
171
|
};
|
|
166
172
|
}
|
|
167
173
|
|
|
168
|
-
export function formatCodexToml(serverPath) {
|
|
174
|
+
export function formatCodexToml(serverPath, { nodePath = process.execPath } = {}) {
|
|
169
175
|
return [
|
|
170
176
|
`[mcp_servers.${MCP_SERVER_NAME}]`,
|
|
171
|
-
|
|
177
|
+
`command = "${escapeTomlString(nodePath)}"`,
|
|
172
178
|
`args = ["${escapeTomlString(serverPath)}", "mcp"]`,
|
|
173
179
|
'startup_timeout_sec = 20',
|
|
174
180
|
'tool_timeout_sec = 120'
|
|
175
181
|
].join('\n');
|
|
176
182
|
}
|
|
177
183
|
|
|
184
|
+
// TOML basic string: escape backslashes (Windows paths), quotes and control
|
|
185
|
+
// characters.
|
|
178
186
|
function escapeTomlString(value) {
|
|
179
|
-
return String(value)
|
|
187
|
+
return String(value)
|
|
188
|
+
.replace(/\\/g, '\\\\')
|
|
189
|
+
.replace(/"/g, '\\"')
|
|
190
|
+
// eslint-disable-next-line no-control-regex -- TOML needs control characters escaped.
|
|
191
|
+
.replace(/[\u0000-\u001f\u007f]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
180
192
|
}
|
|
181
193
|
|
|
182
194
|
function quoteShellArg(value, { platform = process.platform } = {}) {
|