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
@@ -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 }) {
@@ -5,16 +5,22 @@ 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';
10
+ import { mcpArgs, resolveLaunchCommand, writeLauncher } from '../lib/launcher.mjs';
9
11
  import {
12
+ MCP_LAUNCHER_NOTE,
13
+ MCP_NODE_PATH_NOTE,
10
14
  MCP_SERVER_NAME,
11
- defaultMcpServerPath,
15
+ formatClaudeMcpAddCommand,
12
16
  formatCodexToml,
13
17
  stdioMcpServersConfig,
14
18
  vscodeMcpConfig
15
19
  } from '../lib/manual-mcp.mjs';
20
+ import { createMcpInstallContext, describeMcpPlan, planMcpInstall } from '../lib/mcp-install.mjs';
16
21
  import { loadReminderConfig } from '../lib/reminder-config.mjs';
17
22
  import { installReminderScheduler } from '../lib/reminder-scheduler.mjs';
23
+ import { formatMcpInstallResults, isInteractiveTerminal, runMcpInstall } from './mcp.mjs';
18
24
 
19
25
  // Package name before the rename. Its global install provides a `logwork` bin
20
26
  // that would block `npm link` of logwork-cli, so setup removes it first.
@@ -27,8 +33,9 @@ const EXCLUDED_DIRS = new Set([
27
33
  ]);
28
34
 
29
35
  // `logwork setup`: copy this package into ~/.logwork-cli, install its
30
- // dependencies, link the `logwork` command, refresh a migrated reminder, and
31
- // print the MCP configs.
36
+ // dependencies, link the `logwork` command, write the launcher, refresh a
37
+ // migrated reminder, and add or update the `logwork` entry in the MCP clients
38
+ // found on this machine.
32
39
  export async function run(argv = []) {
33
40
  if (argv.includes('--help') || argv.includes('-h')) {
34
41
  console.log(formatSetupHelp());
@@ -43,16 +50,19 @@ export async function run(argv = []) {
43
50
 
44
51
  export function formatSetupHelp() {
45
52
  return `Usage:
46
- logwork setup [--login|--no-login]
53
+ logwork setup [--yes] [--no-mcp-install] [--login|--no-login]
47
54
 
48
- Installs the runtime to ~/.logwork-cli, links the \`logwork\` command, and prints
49
- MCP configs for Cursor, Antigravity, Claude Code, Codex and VS Code.
50
- Settings, mappings and the login session from an older logwork-helper install
51
- are carried over automatically.
55
+ Installs the runtime to ~/.logwork-cli, links the \`logwork\` command, and adds
56
+ the "logwork" MCP server to the clients it finds (Claude Code, Cursor, Codex,
57
+ VS Code, Google Antigravity), asking once per client. Each edited config is
58
+ backed up first. Settings, mappings and the login session from an older
59
+ logwork-helper install are carried over automatically.
52
60
 
53
61
  Options:
54
- --login Log in to Resource Optimiser after installing
55
- --no-login Do not ask to log in`;
62
+ --yes, -y Update every detected MCP client without asking
63
+ --no-mcp-install Do not touch MCP client configs
64
+ --login Log in to Resource Optimiser after installing
65
+ --no-login Do not ask to log in`;
56
66
  }
57
67
 
58
68
  export async function installUserRuntime({
@@ -65,7 +75,10 @@ export async function installUserRuntime({
65
75
  installFn = installDependencies,
66
76
  linkFn = linkGlobalBinaries,
67
77
  authFn = maybeRunAuthLogin,
78
+ runtimeInfoFn = recordRuntimeInfo,
79
+ launcherFn = writeRuntimeLauncher,
68
80
  reminderFn = refreshReminderSchedule,
81
+ mcpInstallFn = installMcpClients,
69
82
  printFn = printMcpInstructions
70
83
  }) {
71
84
  await mkdirFn(targetDir, { recursive: true });
@@ -76,29 +89,98 @@ export async function installUserRuntime({
76
89
  await installFn(targetDir);
77
90
  const linkResult = await linkFn(targetDir);
78
91
  const authResult = await authFn(targetDir, options);
92
+ await runtimeInfoFn(targetDir);
93
+ await launcherFn(targetDir);
79
94
  const reminderResult = await reminderFn(targetDir);
80
- await printFn(targetDir, authResult, linkResult, cleanupResult, reminderResult);
95
+ const mcpResult = await mcpInstallFn(targetDir, options);
96
+ await printFn(targetDir, authResult, linkResult, cleanupResult, reminderResult, mcpResult);
81
97
 
82
98
  return {
83
99
  targetDir,
84
100
  authResult,
85
101
  linkResult,
86
102
  cleanupResult,
87
- reminderResult
103
+ reminderResult,
104
+ mcpResult
88
105
  };
89
106
  }
90
107
 
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) {
108
+ // macOS/Linux: <home>/bin/logwork-launch, which MCP configs and the reminder
109
+ // run. It must exist before the reminder is re-registered.
110
+ export async function writeRuntimeLauncher(targetDir, {
111
+ nodePath = process.execPath,
112
+ platform = process.platform,
113
+ writeFn = writeLauncher
114
+ } = {}) {
115
+ return writeFn({ home: targetDir, nodePath, platform });
116
+ }
117
+
118
+ // Adds or updates the `logwork` entry in the detected MCP clients:
119
+ // - --no-mcp-install: skipped;
120
+ // - --yes: every detected client, without asking;
121
+ // - a terminal: one yes/no per client that needs a change;
122
+ // - otherwise (npm exec from `logwork update`, CI): only the plan.
123
+ export async function installMcpClients(targetDir, options = {}, {
124
+ interactive = isInteractiveTerminal(),
125
+ ctx = createMcpInstallContext({ home: targetDir }),
126
+ install = runMcpInstall,
127
+ plan = planMcpInstall
128
+ } = {}) {
129
+ if (options.mcpInstall === false) {
130
+ return { mode: 'skipped', launch: ctx.launch };
131
+ }
95
132
  try {
96
- const config = await loadReminderConfig();
133
+ if (options.yes || interactive) {
134
+ const result = await install({ ctx, yes: Boolean(options.yes), interactive });
135
+ return { mode: 'installed', launch: ctx.launch, results: result.results };
136
+ }
137
+ const plans = await plan(ctx);
138
+ return { mode: 'planned', launch: ctx.launch, plans, lines: plans.flatMap((item) => describeMcpPlan(item, ctx)) };
139
+ } catch (error) {
140
+ return { mode: 'error', launch: ctx.launch, error: error.message };
141
+ }
142
+ }
143
+
144
+ // Records the node that ran setup. The macOS/Linux launcher tries it first;
145
+ // on Windows the MCP configs and the reminder run it (or the nvm-windows
146
+ // symlink). doctor and reminder status compare it with the current Node.
147
+ // Best effort: a failed write must not fail the install.
148
+ export async function recordRuntimeInfo(targetDir, {
149
+ nodePath = process.execPath,
150
+ nodeVersion = process.version,
151
+ writeFn = writeRuntimeInfo,
152
+ readFileFn = fs.readFile
153
+ } = {}) {
154
+ try {
155
+ const manifest = JSON.parse(await readFileFn(join(targetDir, 'package.json'), 'utf8'));
156
+ return await writeFn({
157
+ path: join(targetDir, RUNTIME_INFO_FILE_NAME),
158
+ nodePath,
159
+ nodeVersion,
160
+ version: manifest.version || null
161
+ });
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+
167
+ // A reminder enabled by an older install still points at the old runtime, or
168
+ // at a Node that nvm or Homebrew removed. Re-register it so the OS scheduler
169
+ // runs this runtime with the current node (and the legacy entry is removed by
170
+ // installReminderScheduler).
171
+ // The schedule runs the launcher (macOS) or a stable node.exe (Windows).
172
+ export async function refreshReminderSchedule(targetDir, {
173
+ nodePath = process.execPath,
174
+ loadConfig = loadReminderConfig,
175
+ install = installReminderScheduler
176
+ } = {}) {
177
+ try {
178
+ const config = await loadConfig();
97
179
  if (!config?.enabled) {
98
180
  return null;
99
181
  }
100
- await installReminderScheduler(config, { home: targetDir });
101
- return { ok: true };
182
+ await install(config, { home: targetDir, nodePath });
183
+ return { ok: true, nodePath };
102
184
  } catch (error) {
103
185
  return { ok: false, error: error.message };
104
186
  }
@@ -407,6 +489,8 @@ export async function findCommandOnPath(commandName, {
407
489
 
408
490
  export function parseSetupUserArgs(args = []) {
409
491
  let loginMode = 'prompt';
492
+ let yes = false;
493
+ let mcpInstall = true;
410
494
 
411
495
  for (const arg of args) {
412
496
  if (arg === '--login') {
@@ -417,10 +501,18 @@ export function parseSetupUserArgs(args = []) {
417
501
  loginMode = 'skip';
418
502
  continue;
419
503
  }
504
+ if (arg === '--yes' || arg === '-y') {
505
+ yes = true;
506
+ continue;
507
+ }
508
+ if (arg === '--no-mcp-install') {
509
+ mcpInstall = false;
510
+ continue;
511
+ }
420
512
  throw new Error(`Unknown setup option: ${arg}`);
421
513
  }
422
514
 
423
- return { loginMode };
515
+ return { loginMode, yes, mcpInstall };
424
516
  }
425
517
 
426
518
  async function maybeRunAuthLogin(targetDir, { loginMode }) {
@@ -491,13 +583,14 @@ function runAuthLogin(targetDir) {
491
583
  });
492
584
  }
493
585
 
494
- function printMcpInstructions(targetDir, authResult = null, linkResult = null, cleanupResult = null, reminderResult = null) {
586
+ function printMcpInstructions(targetDir, authResult = null, linkResult = null, cleanupResult = null, reminderResult = null, mcpResult = null) {
495
587
  console.log(formatSetupUserInstructions({
496
588
  targetDir,
497
589
  authResult,
498
590
  linkResult,
499
591
  cleanupResult,
500
- reminderResult
592
+ reminderResult,
593
+ mcpResult
501
594
  }));
502
595
  }
503
596
 
@@ -507,10 +600,14 @@ export function formatSetupUserInstructions({
507
600
  linkResult = null,
508
601
  cleanupResult = null,
509
602
  reminderResult = null,
510
- platform = process.platform
603
+ mcpResult = null,
604
+ platform = process.platform,
605
+ nodePath = process.execPath,
606
+ env = process.env,
607
+ launch = mcpResult?.launch || resolveLaunchCommand({ platform, home: targetDir, execPath: nodePath, env })
511
608
  } = {}) {
512
- const entryPath = resolve(targetDir, 'bin', 'logwork.mjs');
513
609
  const storeLabel = credentialStoreLabel({ platform });
610
+ const mcp = formatMcpSection({ mcpResult, launch, platform });
514
611
  const sections = [
515
612
  `logwork-cli installed to ${targetDir}.`,
516
613
  '',
@@ -526,27 +623,18 @@ export function formatSetupUserInstructions({
526
623
  formatTerminalCommandInstructions(linkResult),
527
624
  '',
528
625
  'Next steps',
529
- ...formatNextSteps(authResult),
626
+ ...formatNextSteps(authResult, mcp.manual),
530
627
  '',
531
- `MCP config (server name "${MCP_SERVER_NAME}", runs: node ${entryPath} mcp)`,
532
- '',
533
- 'Cursor / Antigravity / Claude Code (.mcp.json):',
534
- JSON.stringify(stdioMcpServersConfig(entryPath), null, 2),
535
- '',
536
- 'Codex config.toml:',
537
- formatCodexToml(entryPath),
538
- '',
539
- 'GitHub Copilot / VS Code mcp.json:',
540
- JSON.stringify(vscodeMcpConfig(entryPath), null, 2),
628
+ ...mcp.lines,
541
629
  '',
542
630
  'AI agent skill (log work from your commits)',
543
631
  '- Claude Code / Codex: run `logwork skill install`.',
544
632
  '- Other MCP clients: use the MCP prompt "log_from_commits".',
545
633
  '',
546
634
  'Verify',
547
- '- Restart or reload your IDE MCP tools after pasting config.',
635
+ '- Restart or reload your IDE MCP tools after a config change.',
548
636
  '- In the IDE, ask: Check my logwork for this week.',
549
- '- An old "logwork-helper" MCP entry can be deleted; it points at the old runtime.',
637
+ '- `logwork mcp status` shows which clients have a current "logwork" entry.',
550
638
  '',
551
639
  'Safety',
552
640
  '- Do not paste passwords, 2FA codes, Bearer tokens, cookies, or raw curl auth logs into AI chat.',
@@ -557,6 +645,70 @@ export function formatSetupUserInstructions({
557
645
  return sections.join('\n');
558
646
  }
559
647
 
648
+ // The MCP part of the setup summary. `manual` is true when the user still has
649
+ // to paste a config somewhere.
650
+ function formatMcpSection({ mcpResult, launch, platform }) {
651
+ const header = `MCP server "${MCP_SERVER_NAME}" runs: ${[launch.command, ...mcpArgs(launch)].join(' ')}`;
652
+ if (mcpResult?.mode === 'installed' && mcpResult.results?.length) {
653
+ const failed = mcpResult.results.some((result) => result.status === 'failed');
654
+ return {
655
+ manual: failed,
656
+ lines: [header, 'MCP clients', ...formatMcpInstallResults(mcpResult.results)]
657
+ };
658
+ }
659
+ if (mcpResult?.mode === 'planned') {
660
+ const pending = (mcpResult.plans || []).some((plan) => plan.action === 'add' || plan.action === 'update');
661
+ if (mcpResult.plans?.length) {
662
+ return {
663
+ manual: false,
664
+ lines: [
665
+ header,
666
+ 'MCP clients (not changed: this is not an interactive terminal)',
667
+ ...(mcpResult.lines || []),
668
+ ...(pending ? ['To apply: run `logwork mcp install` (asks per client) or `logwork mcp install --yes`.'] : [])
669
+ ]
670
+ };
671
+ }
672
+ }
673
+ if (mcpResult?.mode === 'skipped') {
674
+ return {
675
+ manual: false,
676
+ lines: [header, 'MCP clients: not changed (--no-mcp-install). Run `logwork mcp install` to add or update them, or `logwork mcp status` to check.']
677
+ };
678
+ }
679
+ // No client found, an error, or an older caller: print every snippet.
680
+ const lines = [header];
681
+ if (mcpResult?.mode === 'error') {
682
+ lines.push(`MCP clients: could not update automatically (${redactText(mcpResult.error || 'unknown error')}).`);
683
+ } else if (mcpResult) {
684
+ lines.push('MCP clients: none found (Claude Code, Cursor, Codex, VS Code, Google Antigravity).');
685
+ }
686
+ lines.push(...formatManualMcpSnippets(launch, { platform }));
687
+ return { manual: true, lines };
688
+ }
689
+
690
+ export function formatManualMcpSnippets(launch, { platform = process.platform } = {}) {
691
+ const note = launch.stable ? (launch.kind === 'launcher' ? MCP_LAUNCHER_NOTE : '') : MCP_NODE_PATH_NOTE;
692
+ return [
693
+ 'Manual MCP config (paste into your client):',
694
+ ...(note ? [note] : []),
695
+ '',
696
+ 'Cursor / Antigravity / Claude Code (.mcp.json):',
697
+ JSON.stringify(stdioMcpServersConfig(launch), null, 2),
698
+ '',
699
+ 'Claude Code CLI:',
700
+ formatClaudeMcpAddCommand(launch, { platform }),
701
+ '',
702
+ 'Codex config.toml:',
703
+ formatCodexToml(launch),
704
+ '',
705
+ 'GitHub Copilot / VS Code mcp.json:',
706
+ JSON.stringify(vscodeMcpConfig(launch), null, 2),
707
+ '',
708
+ 'An old "logwork-helper" MCP entry can be deleted; it points at the old runtime.'
709
+ ];
710
+ }
711
+
560
712
  function formatAuthStatus(authResult) {
561
713
  if (authResult?.ok) {
562
714
  return 'completed';
@@ -567,7 +719,7 @@ function formatAuthStatus(authResult) {
567
719
  return 'not run';
568
720
  }
569
721
 
570
- function formatNextSteps(authResult) {
722
+ function formatNextSteps(authResult, manualMcp = true) {
571
723
  const steps = [];
572
724
  let index = 1;
573
725
  if (!authResult?.ok) {
@@ -575,9 +727,12 @@ function formatNextSteps(authResult) {
575
727
  steps.push(' logwork login');
576
728
  index += 1;
577
729
  }
578
- steps.push(`${index}. Paste one MCP config below into your IDE.`);
579
- steps.push(`${index + 1}. Restart or reload your IDE MCP tools.`);
580
- steps.push(`${index + 2}. Verify with: Check my logwork for this week.`);
730
+ if (manualMcp) {
731
+ steps.push(`${index}. Paste the MCP config below into your IDE (or run \`logwork mcp install\`).`);
732
+ index += 1;
733
+ }
734
+ steps.push(`${index}. Restart or reload your IDE MCP tools.`);
735
+ steps.push(`${index + 1}. Verify with: Check my logwork for this week.`);
581
736
  return steps;
582
737
  }
583
738
 
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
@@ -78,9 +78,9 @@ To install and start auth immediately:
78
78
  logwork setup --login
79
79
  ```
80
80
 
81
- Use `--no-login` when installing in scripts or CI.
81
+ Use `--no-login` when installing in scripts or CI. Add `--no-mcp-install` to leave MCP client configs alone, or `--yes` to update every detected client without asking. Without a terminal and without `--yes`, setup only prints what `logwork mcp install` would change.
82
82
 
83
- `logwork setup` copies the package into `~/.logwork-cli`, runs `npm ci --omit=dev` there, removes a globally installed `logwork-helper` package (its `logwork` bin would block linking), runs `npm link`, re-registers an enabled reminder, and prints MCP configs.
83
+ `logwork setup` copies the package into `~/.logwork-cli`, runs `npm ci --omit=dev` there, removes a globally installed `logwork-helper` package (its `logwork` bin would block linking), runs `npm link`, writes `runtime.json` and the launcher (`bin/logwork-launch`, macOS), re-registers an enabled reminder, and adds or updates the `logwork` entry in the detected MCP clients ([details](mcp-setup.md#automatic-setup)). It prints the manual snippets only for clients it could not update, or when it found none.
84
84
 
85
85
  ## Update
86
86
 
@@ -91,7 +91,7 @@ logwork update install # interactive confirmation
91
91
  logwork update install --version 0.3.1 --yes
92
92
  ```
93
93
 
94
- The installer accepts only the version currently published under npm's `latest` tag. It runs `npm exec --package=logwork-cli@<version> -- logwork setup --no-login` with a fixed argument list. Then it verifies the copied `package.json` version in `~/.logwork-cli`.
94
+ The installer accepts only the version currently published under npm's `latest` tag. It runs `npm exec --package=logwork-cli@<version> -- logwork setup --no-login` with a fixed argument list. Then it verifies the copied `package.json` version in `~/.logwork-cli`. That setup runs without a terminal, so it does not edit MCP configs; they keep working because they point at the launcher (macOS) or a stable `node.exe` (Windows). `logwork mcp status` shows whether anything needs `logwork mcp install`.
95
95
 
96
96
  Project mappings, drafts, apply history, update cache, and OS credential-store records are preserved.
97
97
 
@@ -222,6 +222,8 @@ Useful commands inside the session:
222
222
  /projects
223
223
  /projects 5234
224
224
  /map SCB 5234
225
+ /map --repo
226
+ /map --repo ~/code/fcs GKE Fair Chem
225
227
  /history
226
228
  /diagnostics
227
229
  ```
@@ -271,8 +273,8 @@ Inside `logwork`, use `/history`, `/history ro`, `/history jira`, or `/history b
271
273
 
272
274
  ## Troubleshooting
273
275
 
274
- - **IDE does not show tools:** restart or reload the MCP client. Check that the config runs `node ~/.logwork-cli/bin/logwork.mjs mcp` (on Windows, `%USERPROFILE%\.logwork-cli\bin\logwork.mjs`) and that the server name is `logwork`.
275
- - **Tools show old names such as `preview_logwork_batch`:** the IDE is still connected to the old `logwork-helper` entry. Replace that entry with the config printed by `logwork setup`; the [tool name table](mcp-setup.md#tool-name-changes-from-logwork-helper) lists the renames.
276
+ - **IDE does not show tools:** run `logwork mcp status`. If the client's entry is missing or stale, run `logwork mcp install`, then restart or reload the MCP client. The entry should run `~/.logwork-cli/bin/logwork-launch mcp` (on Windows, `node.exe %USERPROFILE%\.logwork-cli\bin\logwork.mjs mcp`) under the server name `logwork`. `logwork doctor` also checks that the launcher finds a usable Node.
277
+ - **Tools show old names such as `preview_logwork_batch`:** the IDE is still connected to the old `logwork-helper` entry. `logwork mcp install` removes it; the [tool name table](mcp-setup.md#tool-name-changes-from-logwork-helper) lists the renames.
276
278
  - **`logwork: command not found` after `logwork setup`:** open a new terminal first. If it still fails, run `npm install -g logwork-cli`, or make sure your npm global bin directory is on `PATH`.
277
279
  - **`npm link` fails with `EEXIST` on `logwork`:** an old global `logwork-helper` package still owns the bin. Run `npm rm -g logwork-helper`, then `logwork setup` again.
278
280
  - **Not authenticated to Resource Optimiser:** run `logwork login`, or ask the assistant to call `login`. Enter secrets only in the terminal.
@@ -285,11 +287,20 @@ Inside `logwork`, use `/history`, `/history ro`, `/history jira`, or `/history b
285
287
 
286
288
  ## Dry Run
287
289
 
288
- `LOGWORK_DRY_RUN=1` makes Resource Optimiser writes return the request body instead of calling the API. Reads still hit the API.
290
+ `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.
291
+
292
+ What a dry run reports:
293
+
294
+ - RO apply summaries start with `DRY RUN`; edit, resubmit and delete return `status: "dry_run"`.
295
+ - 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)`.
296
+ - Apply history records a Jira dry run with status `dry_run` and `dryRun: true` entries. They are not counted as submitted.
297
+ - The terminal UI status bar shows `DRY RUN`.
289
298
 
290
299
  ```bash
291
300
  LOGWORK_DRY_RUN=1 logwork quick -m "Dry run task"
292
301
  LOGWORK_DRY_RUN=1 logwork edit 304032 --desc "Dry run" --yes
302
+ npm run dry-run # terminal UI, RO and Jira writes stubbed
303
+ LOGWORK_DRY_RUN=1 logwork mcp # MCP server, RO and Jira writes stubbed
293
304
  ```
294
305
 
295
306
  In Windows PowerShell: