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.
Files changed (64) hide show
  1. package/README.md +23 -7
  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 +50 -16
  8. package/commands/tui.mjs +1 -0
  9. package/docs/advanced.md +12 -2
  10. package/docs/mcp-setup.md +56 -14
  11. package/docs/operations-guide.md +35 -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-workflow.mjs +187 -2
  25. package/lib/commit-workflow.mjs +188 -21
  26. package/lib/config.mjs +7 -0
  27. package/lib/diagnostics.mjs +53 -4
  28. package/lib/jira-api.mjs +25 -3
  29. package/lib/jira-workflow.mjs +36 -6
  30. package/lib/logwork-config.mjs +64 -13
  31. package/lib/manual-apply-state.mjs +15 -0
  32. package/lib/manual-commands.mjs +2 -2
  33. package/lib/manual-ink-app.mjs +108 -1281
  34. package/lib/manual-ink-draft-actions.mjs +227 -0
  35. package/lib/manual-ink-interaction.mjs +306 -0
  36. package/lib/manual-ink-session.mjs +41 -0
  37. package/lib/manual-ink-task-editor.mjs +338 -0
  38. package/lib/manual-ink-wizard.mjs +520 -0
  39. package/lib/manual-mcp.mjs +27 -15
  40. package/lib/manual-repl.mjs +39 -3
  41. package/lib/mcp-server.mjs +51 -20
  42. package/lib/node-runtime.mjs +115 -0
  43. package/lib/paths.mjs +7 -0
  44. package/lib/project-mapping-workflow.mjs +30 -7
  45. package/lib/query-workflow.mjs +0 -1
  46. package/lib/reconciliation-workflow.mjs +1 -1
  47. package/lib/reminder-config.mjs +38 -2
  48. package/lib/reminder-scheduler.mjs +54 -3
  49. package/lib/reminder-service.mjs +28 -3
  50. package/lib/reminder-workflow.mjs +118 -22
  51. package/lib/ro-api-diagnostics.mjs +109 -0
  52. package/lib/ro-api-entries.mjs +359 -0
  53. package/lib/ro-api-fields.mjs +290 -0
  54. package/lib/ro-api-http.mjs +68 -0
  55. package/lib/ro-api-projects.mjs +137 -0
  56. package/lib/ro-api-tasks.mjs +50 -0
  57. package/lib/ro-api-timesheet.mjs +413 -0
  58. package/lib/ro-delete-workflow.mjs +2 -1
  59. package/lib/ro-entry-reader.mjs +11 -6
  60. package/lib/ro-resubmit-workflow.mjs +2 -1
  61. package/lib/terminal-auth-prompts.mjs +7 -4
  62. package/lib/ui.mjs +80 -59
  63. package/package.json +11 -4
  64. package/skills/logwork/SKILL.md +5 -4
package/README.md CHANGED
@@ -120,12 +120,13 @@ How the draft is built:
120
120
  | `chore` | Other Activities |
121
121
 
122
122
  Conventional prefixes are used first, then keywords in the subject.
123
- - **Hours:** each day's RO remaining time (booked − logged, per project) is split across the groups in 0.5h steps, weighted by commits and lines changed. Days without a booking get 0h. Without RO access, a weekday counts as 8h.
123
+ - **Project:** a project mapping whose `repos` list holds the repo wins (`/map --repo . 5234` in the terminal UI, or the MCP tool `map_project` with `repos`). Otherwise ticket and keyword mappings and the day's booked projects pick it, the same way `log_work` does. Repos with no project are listed so you can map them.
124
+ - **Hours:** each project's RO remaining time (booked − logged) is split across that project's groups in 0.5h steps, weighted by commits and lines changed, and never exceeds it. A project with no room left gets 0h. Groups without a project share the room of booked projects that no group claimed. Days without a booking get 0h. Without RO access, a weekday counts as 8h, split across all groups.
124
125
 
125
126
  Nothing is written. Paste the draft into `/logwork`, or let your AI agent use it.
126
127
 
127
128
  For agents:
128
- - **MCP tool `draft_from_commits`** returns the same draft as `draftText`.
129
+ - **MCP tool `draft_from_commits`** returns the same draft as `draftText`, plus `projectOverrides` (preview entry id → `projectMemberId`) for every line with a project. The agent passes both to `log_work` unchanged. In the terminal UI, pasted lines are resolved again from mappings and bookings.
129
130
  - **MCP prompt `log_from_commits`** gives an agent the full procedure:
130
131
  1. find gaps with `get_logwork`;
131
132
  2. draft from commits;
@@ -186,13 +187,13 @@ The default tasks shared by all projects are:
186
187
 
187
188
  `logwork setup` prints the configs. You can also open the terminal UI with `logwork` and type `/mcp` (or `/mcp cursor`, `/mcp antigravity`, `/mcp copilot`, `/mcp claude-code`, `/mcp codex`).
188
189
 
189
- Every client runs the same thing, `node ~/.logwork-cli/bin/logwork.mjs mcp`, under the server name `logwork`:
190
+ Every client runs the same thing, `<node> ~/.logwork-cli/bin/logwork.mjs mcp`, under the server name `logwork`. The printed configs use the absolute path of your Node binary, because IDEs started from the Dock or Finder often do not see nvm's `node` on `PATH`:
190
191
 
191
192
  ```json
192
193
  {
193
194
  "mcpServers": {
194
195
  "logwork": {
195
- "command": "node",
196
+ "command": "/Users/you/.nvm/versions/node/v24.1.0/bin/node",
196
197
  "args": ["/Users/you/.logwork-cli/bin/logwork.mjs", "mcp"]
197
198
  }
198
199
  }
@@ -202,9 +203,11 @@ Every client runs the same thing, `node ~/.logwork-cli/bin/logwork.mjs mcp`, und
202
203
  For Claude Code, run:
203
204
 
204
205
  ```bash
205
- claude mcp add --transport stdio logwork -- node ~/.logwork-cli/bin/logwork.mjs mcp
206
+ claude mcp add --transport stdio logwork -- "$(which node)" ~/.logwork-cli/bin/logwork.mjs mcp
206
207
  ```
207
208
 
209
+ If you upgrade or remove that Node (`nvm uninstall`, `brew upgrade node`), re-run `logwork setup` and paste the new config. `logwork doctor` warns when the recorded Node path is gone.
210
+
208
211
  Restart or reload the IDE's MCP tools after saving.
209
212
 
210
213
  The server exposes 16 tools and one prompt (`log_from_commits`). It also sends `instructions` covering the approval rules, the line format and the recommended call order. Every write is **two calls to the same tool**:
@@ -289,7 +292,7 @@ In the terminal UI, `/logwork` opens a target picker. `/logwork ro` writes Resou
289
292
  - `/query this-week`
290
293
  - `/reconcile`
291
294
  - `/projects`
292
- - `/map SCB 5234`
295
+ - `/map SCB 5234` (ticket prefix) or `/map --repo . 5234` (this git repo, for commit drafts)
293
296
  - `/history`
294
297
  - `/diagnostics`
295
298
 
@@ -298,6 +301,7 @@ In the terminal UI, `/logwork` opens a target picker. `/logwork ro` writes Resou
298
301
  ```bash
299
302
  logwork reminder enable # 17:00 Monday-Friday, RO and Jira
300
303
  logwork reminder enable --time 18:00 --target ro
304
+ logwork reminder enable --repo ~/code/app --repo ~/code/api # add commit evidence
301
305
  logwork reminder test
302
306
  logwork reminder status
303
307
  logwork reminder disable
@@ -305,7 +309,19 @@ logwork reminder disable
305
309
 
306
310
  For Resource Optimiser, the reminder compares today's logged and booked hours. For Jira, it checks whether you have any worklog today. Complete days stay silent.
307
311
 
308
- macOS uses a user `launchd` agent (`sg.vinova.logwork-cli.reminder`) and Windows uses Task Scheduler (`Logwork Reminder`). Neither schedule stores credentials.
312
+ With `--repo` (repeatable; `--no-repos` clears the list), a reminder about missing RO hours also counts your commits today in those repos and points you to a draft:
313
+
314
+ ```text
315
+ Logwork hôm nay chưa hoàn tất
316
+ RO: 4/8h, còn thiếu 4h
317
+ Hôm nay: 5 commit trong 2 repo
318
+
319
+ Mở Terminal và chạy: logwork commits today (bản nháp từ commit)
320
+ ```
321
+
322
+ Repos listed on project mappings are used too. Run outside a git repo, `logwork commits today` falls back to these repos. If reading commits fails, the reminder shows the plain message.
323
+
324
+ macOS uses a user `launchd` agent (`sg.vinova.logwork-cli.reminder`) and Windows uses Task Scheduler (`Logwork Reminder`). Neither schedule stores credentials. The schedule stores the absolute Node path; after upgrading or removing that Node, run `logwork reminder enable` (or `logwork setup`) again. `logwork reminder status` warns when that path no longer exists.
309
325
 
310
326
  ## Reconcile RO And Jira
311
327
 
package/RELEASE.md CHANGED
@@ -55,6 +55,7 @@ This checks version consistency, git cleanliness, npm auth, npm latest version,
55
55
 
56
56
  The gate must pass all of the following:
57
57
 
58
+ - `npm run lint`
58
59
  - `npm test`
59
60
  - `npm audit --omit=dev --audit-level=moderate`
60
61
  - `npm pack --dry-run`
@@ -1,5 +1,9 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
1
3
  import { draftLogworkFromCommits } from '../lib/commit-workflow.mjs';
2
4
  import { normalizeReconciliationPeriod } from '../lib/reconciliation-workflow.mjs';
5
+ import { loadReminderConfig } from '../lib/reminder-config.mjs';
6
+ import { collectReminderRepos } from '../lib/reminder-workflow.mjs';
3
7
 
4
8
  // `logwork commits [period] [--repo path]... [--author x] [--no-bookings] [--json]`:
5
9
  // turn your git commits into a logwork draft. Read-only.
@@ -44,29 +48,71 @@ export function parseCommitsArgs(args = []) {
44
48
  return options;
45
49
  }
46
50
 
47
- export async function run(args = [], { draft = draftLogworkFromCommits, print = console.log } = {}) {
51
+ export async function run(args = [], {
52
+ draft = draftLogworkFromCommits,
53
+ print = console.log,
54
+ cwd = process.cwd(),
55
+ reminderRepos = loadReminderRepos
56
+ } = {}) {
48
57
  const options = parseCommitsArgs(args);
49
58
  if (options.help) {
50
59
  print(formatCommitsHelp());
51
60
  return 0;
52
61
  }
62
+ // The reminder tells the user to run `logwork commits today` from a fresh
63
+ // terminal, which starts outside any repo. There, fall back to the
64
+ // reminder's repos instead of reporting no commits.
65
+ let repos = options.repos;
66
+ let fallbackNote = '';
67
+ if (!repos.length && !isInsideGitWorkTree(cwd)) {
68
+ const fallback = await reminderRepos().catch(() => []);
69
+ if (fallback.length) {
70
+ repos = fallback;
71
+ fallbackNote = `Not in a git repo; using the reminder repos: ${fallback.join(', ')}`;
72
+ }
73
+ }
53
74
  const result = await draft({
54
75
  period: options.period,
55
- repos: options.repos,
76
+ repos,
56
77
  author: options.author,
57
78
  useBookings: options.useBookings
58
79
  });
59
- print(options.json ? JSON.stringify(result, null, 2) : result.summary);
80
+ if (options.json) {
81
+ print(JSON.stringify(result, null, 2));
82
+ } else {
83
+ print(fallbackNote ? `${fallbackNote}\n\n${result.summary}` : result.summary);
84
+ }
60
85
  return 0;
61
86
  }
62
87
 
88
+ // True when dir or a parent holds .git (a directory, or a file for worktrees
89
+ // and submodules). No git process is started.
90
+ export function isInsideGitWorkTree(dir, { exists = existsSync } = {}) {
91
+ let current = resolve(dir);
92
+ for (;;) {
93
+ if (exists(join(current, '.git'))) {
94
+ return true;
95
+ }
96
+ const parent = dirname(current);
97
+ if (parent === current) {
98
+ return false;
99
+ }
100
+ current = parent;
101
+ }
102
+ }
103
+
104
+ async function loadReminderRepos() {
105
+ return collectReminderRepos({ config: await loadReminderConfig() });
106
+ }
107
+
63
108
  export function formatCommitsHelp() {
64
109
  return `Usage:
65
110
  logwork commits [today|yesterday|this-week|last-week|this-month|last-month]
66
111
  [--repo <path>]... [--author <email>] [--no-bookings] [--json]
67
112
 
68
113
  Reads your commits (all branches, your git user.email) from each repo (default:
69
- the current directory) and drafts logwork lines:
114
+ the current directory; outside a git repo, the repos set with
115
+ \`logwork reminder enable --repo\`) and drafts logwork lines:
70
116
  +<hours> <worklog task> | <description>
71
117
 
72
118
  - Commits are grouped per day by repo, ticket key and kind of work.
@@ -74,8 +120,13 @@ the current directory) and drafts logwork lines:
74
120
  fix -> Bug Fix, refactor/perf/review -> Code Review & Refactoring,
75
121
  test -> Testing Activities, ci/build/release -> Deployment & Release,
76
122
  docs/design -> Requirements & Design, chore -> Other Activities.
77
- - Hours split the day's RO remaining time (booked - logged) by how much work
78
- each group holds. With --no-bookings or without login, a weekday is 8h.
123
+ - Each group gets an RO project: a mapping whose repos holds the repo (/map
124
+ --repo in the terminal UI, or map_project), else ticket/keyword mappings and
125
+ that day's bookings.
126
+ - Each project's RO remaining time (booked - logged) is split across its
127
+ groups by how much work each holds; groups without a project share the room
128
+ of booked projects no group claimed. With --no-bookings or without login, a
129
+ weekday is 8h split across all groups.
79
130
 
80
131
  Nothing is written. Review the draft, then paste it into /logwork or give it
81
132
  to your AI agent (log_work).`;
@@ -56,7 +56,3 @@ function printHistoryHelp(print = console.log) {
56
56
 
57
57
  Shows the local apply result ledger. Target both means entries applied from a combined Both flow.`);
58
58
  }
59
-
60
- async function main() {
61
- await runHistoryCommand(process.argv.slice(2));
62
- }
@@ -10,41 +10,56 @@ import {
10
10
  import * as ui from '../lib/ui.mjs';
11
11
  import { todayLocalDateISO } from '../lib/util.mjs';
12
12
 
13
+ const QUICK_API = { addLogtime, fetchWorklogTasksForProject, getTodayTimesheetProjects };
14
+
13
15
  // `logwork quick [-m "what you did"]`: log one Resource Optimiser entry for
14
16
  // today with guided prompts (project -> worklog task -> description -> hours).
15
- export async function run(argv = []) {
17
+ // The second argument of run and the options of runQuickLog are test seams;
18
+ // the CLI uses the defaults.
19
+ export async function run(argv = [], {
20
+ quickLog = runQuickLog,
21
+ quickUi = ui,
22
+ showIntro = intro,
23
+ print = console.log
24
+ } = {}) {
16
25
  const { message, help } = parseQuickArgs(argv);
17
26
  if (help) {
18
- console.log(formatQuickHelp());
27
+ print(formatQuickHelp());
19
28
  return 0;
20
29
  }
21
30
 
22
31
  try {
23
- intro('Logwork quick');
24
- const result = await runQuickLog({ message });
25
- ui.showSuccess(result.dryRun
32
+ showIntro('Logwork quick');
33
+ const result = await quickLog({ message });
34
+ quickUi.showSuccess(result.dryRun
26
35
  ? 'DRY RUN: payload generated; no API write performed.'
27
36
  : 'Logwork submitted.');
28
37
  return 0;
29
38
  } catch (error) {
30
- ui.showError(error);
39
+ quickUi.showError(error);
31
40
  return 1;
32
41
  }
33
42
  }
34
43
 
35
- export async function runQuickLog({ message = '' } = {}) {
36
- const localDateISO = todayLocalDateISO();
37
- let session = await openSession();
44
+ export async function runQuickLog({
45
+ message = '',
46
+ quickUi = ui,
47
+ openSession: open = openSession,
48
+ api = QUICK_API,
49
+ today = todayLocalDateISO
50
+ } = {}) {
51
+ const localDateISO = today();
52
+ let session = await open();
38
53
 
39
54
  let projects;
40
55
  try {
41
- projects = await loadBookedProjects(session, localDateISO);
56
+ projects = await loadBookedProjects(session, localDateISO, api);
42
57
  } catch (error) {
43
58
  if (!isUnauthorized(error)) {
44
59
  throw error;
45
60
  }
46
- session = await openSession({ forceLogin: true });
47
- projects = await loadBookedProjects(session, localDateISO);
61
+ session = await open({ forceLogin: true });
62
+ projects = await loadBookedProjects(session, localDateISO, api);
48
63
  }
49
64
 
50
65
  const daySummaries = projects.map((project) => ({
@@ -52,8 +67,8 @@ export async function runQuickLog({ message = '' } = {}) {
52
67
  totalHours: project.loggedHours,
53
68
  ok: true
54
69
  }));
55
- const project = await ui.chooseProject(projects, daySummaries);
56
- const tasks = (await fetchWorklogTasksForProject(session.token, project.projectId)).map((task) => ({
70
+ const project = await quickUi.chooseProject(projects, daySummaries);
71
+ const tasks = (await api.fetchWorklogTasksForProject(session.token, project.projectId)).map((task) => ({
57
72
  taskId: task.id,
58
73
  name: task.name,
59
74
  typeOfWork: task.typeOfWork || 'other',
@@ -63,11 +78,11 @@ export async function runQuickLog({ message = '' } = {}) {
63
78
  if (!tasks.length) {
64
79
  throw new Error(`No worklog tasks are available for ${project.projectName}.`);
65
80
  }
66
- const task = await ui.chooseTask(tasks);
67
- const description = await ui.chooseMessageSource(message);
68
- const hours = await ui.askHours(CONFIG.defaultHours);
81
+ const task = await quickUi.chooseTask(tasks);
82
+ const description = await quickUi.chooseMessageSource(message);
83
+ const hours = await quickUi.askHours(CONFIG.defaultHours);
69
84
 
70
- await ui.confirmSubmit({
85
+ await quickUi.confirmSubmit({
71
86
  projectName: project.projectName,
72
87
  hours,
73
88
  taskName: task.name,
@@ -88,13 +103,13 @@ export async function runQuickLog({ message = '' } = {}) {
88
103
  };
89
104
 
90
105
  try {
91
- return await addLogtime(session.token, entry);
106
+ return await api.addLogtime(session.token, entry);
92
107
  } catch (error) {
93
108
  if (!isUnauthorized(error)) {
94
109
  throw error;
95
110
  }
96
- session = await openSession({ forceLogin: true });
97
- return addLogtime(session.token, entry);
111
+ session = await open({ forceLogin: true });
112
+ return api.addLogtime(session.token, entry);
98
113
  }
99
114
  }
100
115
 
@@ -150,8 +165,8 @@ async function openSession({ forceLogin = false } = {}) {
150
165
  return { token, userId: getUserIdFromJwt(token) };
151
166
  }
152
167
 
153
- async function loadBookedProjects(session, localDateISO) {
154
- const projects = await getTodayTimesheetProjects(session.token, session.userId, localDateISO);
168
+ async function loadBookedProjects(session, localDateISO, api) {
169
+ const projects = await api.getTodayTimesheetProjects(session.token, session.userId, localDateISO);
155
170
  if (!projects.length) {
156
171
  throw new Error(`You have no booked project on ${localDateISO}.`);
157
172
  }
@@ -1,5 +1,6 @@
1
1
  import { confirm, isCancel } from '@clack/prompts';
2
2
  import {
3
+ normalizeReminderRepos,
3
4
  normalizeReminderTime,
4
5
  REMINDER_TARGETS
5
6
  } from '../lib/reminder-config.mjs';
@@ -27,6 +28,7 @@ export function parseReminderArgs(args = []) {
27
28
  yes: false,
28
29
  json: false,
29
30
  home: undefined,
31
+ repos: undefined,
30
32
  help: false
31
33
  };
32
34
  for (let index = 0; index < rest.length; index += 1) {
@@ -44,6 +46,27 @@ export function parseReminderArgs(args = []) {
44
46
  index += 1;
45
47
  continue;
46
48
  }
49
+ if (arg === '--repo' || arg.startsWith('--repo=')) {
50
+ const value = arg === '--repo' ? rest[index + 1] : arg.slice('--repo='.length);
51
+ if (!value || (arg === '--repo' && value.startsWith('--'))) {
52
+ throw new Error('--repo requires a path.');
53
+ }
54
+ if (options.repos === null) {
55
+ throw new Error('--repo cannot be combined with --no-repos.');
56
+ }
57
+ options.repos = [...(options.repos || []), value];
58
+ if (arg === '--repo') {
59
+ index += 1;
60
+ }
61
+ continue;
62
+ }
63
+ if (arg === '--no-repos') {
64
+ if (options.repos?.length) {
65
+ throw new Error('--repo cannot be combined with --no-repos.');
66
+ }
67
+ options.repos = null;
68
+ continue;
69
+ }
47
70
  if (arg === '--yes' || arg === '-y') {
48
71
  options.yes = true;
49
72
  continue;
@@ -61,6 +84,16 @@ export function parseReminderArgs(args = []) {
61
84
  if (command !== 'enable' && options.time) {
62
85
  throw new Error('--time is only valid with reminder enable.');
63
86
  }
87
+ if (command !== 'enable' && options.repos !== undefined) {
88
+ throw new Error('--repo and --no-repos are only valid with reminder enable.');
89
+ }
90
+ if (Array.isArray(options.repos)) {
91
+ // Resolve against the directory the command runs in; the scheduled run
92
+ // starts in the helper home.
93
+ options.repos = normalizeReminderRepos(options.repos);
94
+ } else if (options.repos === null) {
95
+ options.repos = [];
96
+ }
64
97
  if (command !== 'run' && options.home) {
65
98
  throw new Error('--home is reserved for the OS reminder runner.');
66
99
  }
@@ -131,6 +164,7 @@ export async function runReminderCli(args = [], {
131
164
  result = await enable({
132
165
  time: options.time,
133
166
  target: options.target,
167
+ ...(options.repos !== undefined ? { repos: options.repos } : {}),
134
168
  confirm: true
135
169
  });
136
170
  } else if (options.command === 'disable') {
@@ -147,12 +181,17 @@ export async function runReminderCli(args = [], {
147
181
 
148
182
  export function formatReminderHelp() {
149
183
  return `Usage:
150
- logwork reminder enable [--time HH:mm] [--target ro|jira|both] [--yes]
184
+ logwork reminder enable [--time HH:mm] [--target ro|jira|both]
185
+ [--repo <path>]... [--no-repos] [--yes]
151
186
  logwork reminder status [--json]
152
187
  logwork reminder test [--target ro|jira|both] [--yes]
153
188
  logwork reminder disable [--yes]
154
189
 
155
- The reminder runs Monday-Friday at 17:00 by default. It uses launchd on macOS and Task Scheduler on Windows. RO compares logged and booked hours; Jira checks whether the current user has a worklog today.`;
190
+ The reminder runs Monday-Friday at 17:00 by default. It uses launchd on macOS and Task Scheduler on Windows. RO compares logged and booked hours; Jira checks whether the current user has a worklog today.
191
+
192
+ --repo (repeatable) sets the git repos whose commits today are counted when RO hours are missing, for example "Hôm nay: 5 commit trong 2 repo", and points to \`logwork commits today\` for a draft. Repos listed on project mappings are also used. --repo replaces the saved list; --no-repos clears it; without either the saved list is kept.
193
+
194
+ The schedule stores the absolute path of the Node that ran \`reminder enable\`. After upgrading or removing that Node (nvm, Homebrew), run \`logwork reminder enable\` or \`logwork setup\` again; \`logwork reminder status\` warns when the path is gone.`;
156
195
  }
157
196
 
158
197
  async function requireInteractiveConfirmation({ isTTY, promptConfirm, message }) {
@@ -1,14 +1,15 @@
1
1
  import { constants as fsConstants, promises as fs } from 'node:fs';
2
2
  import { spawn } from 'node:child_process';
3
- import { delimiter, dirname, join, relative, resolve } from 'node:path';
3
+ import { delimiter, dirname, join, relative, resolve, win32 } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { redactText } from '../lib/auth-redaction.mjs';
6
6
  import { credentialStoreLabel } from '../lib/credential-store.mjs';
7
7
  import { npmInvocation } from '../lib/npm-command.mjs';
8
- import { helperHome } from '../lib/paths.mjs';
8
+ import { writeRuntimeInfo } from '../lib/node-runtime.mjs';
9
+ import { RUNTIME_INFO_FILE_NAME, helperHome } from '../lib/paths.mjs';
9
10
  import {
11
+ MCP_NODE_PATH_NOTE,
10
12
  MCP_SERVER_NAME,
11
- defaultMcpServerPath,
12
13
  formatCodexToml,
13
14
  stdioMcpServersConfig,
14
15
  vscodeMcpConfig
@@ -65,6 +66,7 @@ export async function installUserRuntime({
65
66
  installFn = installDependencies,
66
67
  linkFn = linkGlobalBinaries,
67
68
  authFn = maybeRunAuthLogin,
69
+ runtimeInfoFn = recordRuntimeInfo,
68
70
  reminderFn = refreshReminderSchedule,
69
71
  printFn = printMcpInstructions
70
72
  }) {
@@ -76,6 +78,7 @@ export async function installUserRuntime({
76
78
  await installFn(targetDir);
77
79
  const linkResult = await linkFn(targetDir);
78
80
  const authResult = await authFn(targetDir, options);
81
+ await runtimeInfoFn(targetDir);
79
82
  const reminderResult = await reminderFn(targetDir);
80
83
  await printFn(targetDir, authResult, linkResult, cleanupResult, reminderResult);
81
84
 
@@ -88,17 +91,44 @@ export async function installUserRuntime({
88
91
  };
89
92
  }
90
93
 
91
- // A reminder enabled by an older install still points at the old runtime.
92
- // Re-register it so the OS scheduler runs this runtime (and the legacy entry
93
- // is removed by installReminderScheduler).
94
- async function refreshReminderSchedule(targetDir) {
94
+ // Records the node that ran setup (the one the printed MCP configs and the
95
+ // reminder use), so doctor and reminder status can spot a removed or switched
96
+ // Node later. Best effort: a failed write must not fail the install.
97
+ export async function recordRuntimeInfo(targetDir, {
98
+ nodePath = process.execPath,
99
+ nodeVersion = process.version,
100
+ writeFn = writeRuntimeInfo,
101
+ readFileFn = fs.readFile
102
+ } = {}) {
103
+ try {
104
+ const manifest = JSON.parse(await readFileFn(join(targetDir, 'package.json'), 'utf8'));
105
+ return await writeFn({
106
+ path: join(targetDir, RUNTIME_INFO_FILE_NAME),
107
+ nodePath,
108
+ nodeVersion,
109
+ version: manifest.version || null
110
+ });
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ // A reminder enabled by an older install still points at the old runtime, or
117
+ // at a Node that nvm or Homebrew removed. Re-register it so the OS scheduler
118
+ // runs this runtime with the current node (and the legacy entry is removed by
119
+ // installReminderScheduler).
120
+ export async function refreshReminderSchedule(targetDir, {
121
+ nodePath = process.execPath,
122
+ loadConfig = loadReminderConfig,
123
+ install = installReminderScheduler
124
+ } = {}) {
95
125
  try {
96
- const config = await loadReminderConfig();
126
+ const config = await loadConfig();
97
127
  if (!config?.enabled) {
98
128
  return null;
99
129
  }
100
- await installReminderScheduler(config, { home: targetDir });
101
- return { ok: true };
130
+ await install(config, { home: targetDir, nodePath });
131
+ return { ok: true, nodePath };
102
132
  } catch (error) {
103
133
  return { ok: false, error: error.message };
104
134
  }
@@ -507,9 +537,12 @@ export function formatSetupUserInstructions({
507
537
  linkResult = null,
508
538
  cleanupResult = null,
509
539
  reminderResult = null,
510
- platform = process.platform
540
+ platform = process.platform,
541
+ nodePath = process.execPath
511
542
  } = {}) {
512
- const entryPath = resolve(targetDir, 'bin', 'logwork.mjs');
543
+ const entryPath = platform === 'win32'
544
+ ? win32.resolve(targetDir, 'bin', 'logwork.mjs')
545
+ : resolve(targetDir, 'bin', 'logwork.mjs');
513
546
  const storeLabel = credentialStoreLabel({ platform });
514
547
  const sections = [
515
548
  `logwork-cli installed to ${targetDir}.`,
@@ -528,16 +561,17 @@ export function formatSetupUserInstructions({
528
561
  'Next steps',
529
562
  ...formatNextSteps(authResult),
530
563
  '',
531
- `MCP config (server name "${MCP_SERVER_NAME}", runs: node ${entryPath} mcp)`,
564
+ `MCP config (server name "${MCP_SERVER_NAME}", runs: ${nodePath} ${entryPath} mcp)`,
565
+ MCP_NODE_PATH_NOTE,
532
566
  '',
533
567
  'Cursor / Antigravity / Claude Code (.mcp.json):',
534
- JSON.stringify(stdioMcpServersConfig(entryPath), null, 2),
568
+ JSON.stringify(stdioMcpServersConfig(entryPath, { nodePath }), null, 2),
535
569
  '',
536
570
  'Codex config.toml:',
537
- formatCodexToml(entryPath),
571
+ formatCodexToml(entryPath, { nodePath }),
538
572
  '',
539
573
  'GitHub Copilot / VS Code mcp.json:',
540
- JSON.stringify(vscodeMcpConfig(entryPath), null, 2),
574
+ JSON.stringify(vscodeMcpConfig(entryPath, { nodePath }), null, 2),
541
575
  '',
542
576
  'AI agent skill (log work from your commits)',
543
577
  '- Claude Code / Codex: run `logwork skill install`.',
package/commands/tui.mjs CHANGED
@@ -43,6 +43,7 @@ Common commands inside the UI:
43
43
  /reconcile this-week Compare RO and Jira
44
44
  /projects List RO projects
45
45
  /map SCB 5234 Map a ticket prefix to a project
46
+ /map --repo . 5234 Map this git repo to a project (commit drafts)
46
47
  /mcp Show MCP config for an IDE
47
48
  /history Local apply history
48
49
 
package/docs/advanced.md CHANGED
@@ -27,7 +27,7 @@ Supported overrides:
27
27
  ```text
28
28
  LOGWORK_PROFILE (LOGWORK_HELPER_PROFILE still accepted)
29
29
  LOGWORK_HOME (local state directory; LOGWORK_HELPER_HOME still accepted)
30
- LOGWORK_DRY_RUN=1 (RO writes return the request body instead of calling the API)
30
+ LOGWORK_DRY_RUN=1 (RO and Jira writes return the request instead of calling the API)
31
31
  LOGWORK_API_BASE
32
32
  LOGWORK_LOGIN_URL
33
33
  LOGWORK_JIRA_BASE_URL
@@ -222,6 +222,7 @@ Useful commands inside the session:
222
222
  /projects
223
223
  /projects 5234
224
224
  /map SCB 5234
225
+ /map --repo . 5234
225
226
  /history
226
227
  /diagnostics
227
228
  ```
@@ -285,11 +286,20 @@ Inside `logwork`, use `/history`, `/history ro`, `/history jira`, or `/history b
285
286
 
286
287
  ## Dry Run
287
288
 
288
- `LOGWORK_DRY_RUN=1` makes Resource Optimiser writes return the request body instead of calling the API. Reads still hit the API.
289
+ `LOGWORK_DRY_RUN=1` makes every Resource Optimiser and Jira write return the request (`{ dryRun: true, method, path, body }`) instead of calling the API. The request never contains the token or the `Authorization` header. Reads still hit the API: Jira issue lookup and the duplicate-worklog check still call Jira, and RO previews still read RO.
290
+
291
+ What a dry run reports:
292
+
293
+ - RO apply summaries start with `DRY RUN`; edit, resubmit and delete return `status: "dry_run"`.
294
+ - Jira apply (`log_jira_work` with `confirm: true`, or the terminal UI `/apply`) returns `status: "dry_run"`, `dryRun: true`, one `dry_run` result per entry with its `request`, and a summary that starts with `DRY RUN (LOGWORK_DRY_RUN=1)`.
295
+ - Apply history records a Jira dry run with status `dry_run` and `dryRun: true` entries. They are not counted as submitted.
296
+ - The terminal UI status bar shows `DRY RUN`.
289
297
 
290
298
  ```bash
291
299
  LOGWORK_DRY_RUN=1 logwork quick -m "Dry run task"
292
300
  LOGWORK_DRY_RUN=1 logwork edit 304032 --desc "Dry run" --yes
301
+ npm run dry-run # terminal UI, RO and Jira writes stubbed
302
+ LOGWORK_DRY_RUN=1 logwork mcp # MCP server, RO and Jira writes stubbed
293
303
  ```
294
304
 
295
305
  In Windows PowerShell: