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
package/README.md CHANGED
@@ -26,7 +26,7 @@ logwork setup
26
26
  - installs the runtime to `~/.logwork-cli` (`%USERPROFILE%\.logwork-cli` on Windows),
27
27
  - links the `logwork` command,
28
28
  - offers to log in,
29
- - prints copy-ready MCP configs that use your real local path.
29
+ - adds the `logwork` MCP server to the AI clients it finds (Claude Code, Cursor, Codex, VS Code, Google Antigravity). It asks once per client; answer yes. See [Set Up MCP](#set-up-mcp).
30
30
 
31
31
  ## Upgrading from logwork-helper
32
32
 
@@ -57,7 +57,7 @@ Most of the move is automatic:
57
57
 
58
58
  Two things you need to do yourself:
59
59
 
60
- - **IDE config:** replace the old `logwork-helper` MCP entry with the new `logwork` entry printed by `logwork setup` or `/mcp`.
60
+ - **IDE config:** `logwork setup` replaces the old `logwork-helper` entry with the new `logwork` entry in the clients it can edit. For any other client, use the config printed by `logwork setup` or `/mcp`.
61
61
  - **MCP tool names:** the tool names changed. See the [old → new tool name table](docs/mcp-setup.md#tool-name-changes-from-logwork-helper).
62
62
 
63
63
  ## Log In
@@ -77,7 +77,7 @@ Sessions are stored in macOS Keychain or Windows Credential Manager. The default
77
77
  ```text
78
78
  logwork Open the terminal UI
79
79
  logwork query [period] Booked vs logged hours with entry ids (today, yesterday, this-week, last-week, this-month, last-month)
80
- logwork commits [period] Draft logwork lines from your git commits (--repo, --author)
80
+ logwork commits [period] Draft logwork lines from your git commits (--repo, --author, --no-prompt)
81
81
  logwork quick [-m text] Log one entry for today with guided prompts
82
82
  logwork edit <id> ... Change an existing RO entry (replaces it)
83
83
  logwork delete <id> Delete a submitted or approved RO entry
@@ -85,7 +85,9 @@ logwork reconcile Compare RO and Jira hours by day (read-only)
85
85
  logwork history Local apply history
86
86
  logwork login|logout|status [ro|jira]
87
87
  logwork skill install Install the AI agent skill (Claude Code, Codex)
88
- logwork setup Install the runtime and print MCP configs
88
+ logwork setup Install the runtime and add logwork to your MCP clients
89
+ logwork mcp install Add or update the logwork entry in your MCP clients
90
+ logwork mcp status Check each MCP client's logwork entry
89
91
  logwork mcp Run the MCP stdio server (used by IDE configs)
90
92
  logwork reminder ... Weekday logwork reminder
91
93
  logwork update ... Check for or install a new version
@@ -120,12 +122,20 @@ How the draft is built:
120
122
  | `chore` | Other Activities |
121
123
 
122
124
  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.
125
+ - **Project:** a project mapping whose `repos` list holds the repo wins. Otherwise ticket and keyword mappings and the day's booked projects pick it, the same way `log_work` does.
126
+ - **Repos with no project:** you are asked once, in place. In an interactive terminal, `logwork commits` shows `Which RO project does <repo> belong to?` with your member projects. The likely project is preselected, with the reason as a hint:
127
+ - a ticket key from the repo's commits that you logged to that project in the last 30 days;
128
+ - the repo folder or git remote name matching the project name, its acronym in parentheses (`fcs-web` → `GKE Fair Chem System (FCS)`) or its initials;
129
+ - the only project booked on the repo's commit days.
124
130
 
125
- Nothing is written. Paste the draft into `/logwork`, or let your AI agent use it.
131
+ Your choice is saved as a repo mapping (user scope) and the draft is redone. Choose `Skip for now` to be asked again next time. `--no-prompt`, `--json` or a non-interactive terminal never ask; the summary then names the likely project per repo.
132
+ - **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.
133
+
134
+ Nothing is written to RO or Jira. Paste the draft into `/logwork`, or let your AI agent use it.
126
135
 
127
136
  For agents:
128
- - **MCP tool `draft_from_commits`** returns the same draft as `draftText`.
137
+ - **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.
138
+ - For `unmappedRepos` it also returns `repoSuggestions` (ranked projects with reasons per repo) and `projectCandidates`. The agent asks you one short question per repo by project name ("Is fcs-web part of GKE Fair Chem System (FCS)?"), then saves the answer with `map_project` and drafts again. You never type an id.
129
139
  - **MCP prompt `log_from_commits`** gives an agent the full procedure:
130
140
  1. find gaps with `get_logwork`;
131
141
  2. draft from commits;
@@ -184,28 +194,33 @@ The default tasks shared by all projects are:
184
194
 
185
195
  ## Set Up MCP
186
196
 
187
- `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`).
197
+ Run `logwork setup` and answer yes for each client. It finds the clients you have and adds or updates the `logwork` server in each:
188
198
 
189
- Every client runs the same thing, `node ~/.logwork-cli/bin/logwork.mjs mcp`, under the server name `logwork`:
199
+ | Client | What setup changes |
200
+ |---|---|
201
+ | Claude Code | `claude mcp add --scope user logwork ...` (or `~/.claude.json` when the `claude` CLI is not on `PATH`) |
202
+ | Cursor | `~/.cursor/mcp.json` |
203
+ | Codex | `~/.codex/config.toml` |
204
+ | VS Code | user `mcp.json` (`~/Library/Application Support/Code/User` on macOS, `%APPDATA%\Code\User` on Windows) |
205
+ | Google Antigravity | `~/.gemini/antigravity/mcp_config.json` |
190
206
 
191
- ```json
192
- {
193
- "mcpServers": {
194
- "logwork": {
195
- "command": "node",
196
- "args": ["/Users/you/.logwork-cli/bin/logwork.mjs", "mcp"]
197
- }
198
- }
199
- }
200
- ```
201
-
202
- For Claude Code, run:
207
+ It touches only the `logwork` entry, removes an old `logwork-helper` entry, and backs up each file first (`<file>.logwork-backup-<timestamp>`). A file it cannot parse safely (JSON with comments, unusual TOML) is left alone, and setup prints the snippet to paste instead.
203
208
 
204
209
  ```bash
205
- claude mcp add --transport stdio logwork -- node ~/.logwork-cli/bin/logwork.mjs mcp
210
+ logwork mcp status # which clients have a current logwork entry
211
+ logwork mcp install # add or update them later (asks per client)
212
+ logwork mcp install --dry-run # show the changes without writing
213
+ logwork setup --yes # update every detected client without asking
214
+ logwork setup --no-mcp-install
206
215
  ```
207
216
 
208
- Restart or reload the IDE's MCP tools after saving.
217
+ The configs never name a Node version, so Node upgrades need no config change:
218
+ - **macOS:** clients run `~/.logwork-cli/bin/logwork-launch mcp`. This small script, written by `logwork setup`, starts logwork with the first Node 22+ it finds: the Node that ran setup, `node` on `PATH`, the newest nvm Node, Volta or fnm, then Homebrew.
219
+ - **Windows:** clients run `node.exe %USERPROFILE%\.logwork-cli\bin\logwork.mjs mcp` with a stable `node.exe`: the nvm-windows symlink (`%NVM_SYMLINK%\node.exe`) or `C:\Program Files\nodejs\node.exe`.
220
+
221
+ For a client setup does not know, open the terminal UI with `logwork` and type `/mcp` (or `/mcp cursor`, `/mcp antigravity`, `/mcp copilot`, `/mcp claude-code`, `/mcp codex`) for a copy-ready config. [docs/mcp-setup.md](docs/mcp-setup.md) has the manual snippets.
222
+
223
+ Restart or reload the IDE's MCP tools after a change.
209
224
 
210
225
  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**:
211
226
  1. Call the tool without `confirm`. It returns a preview with a `batchId` or `previewId` and a summary.
@@ -289,7 +304,7 @@ In the terminal UI, `/logwork` opens a target picker. `/logwork ro` writes Resou
289
304
  - `/query this-week`
290
305
  - `/reconcile`
291
306
  - `/projects`
292
- - `/map SCB 5234`
307
+ - `/map --repo` (this git repo, for commit drafts: opens a project picker with the likely project first), `/map --repo ~/code/fcs GKE Fair` (by name), or `/map SCB 5234` (ticket prefix)
293
308
  - `/history`
294
309
  - `/diagnostics`
295
310
 
@@ -298,6 +313,7 @@ In the terminal UI, `/logwork` opens a target picker. `/logwork ro` writes Resou
298
313
  ```bash
299
314
  logwork reminder enable # 17:00 Monday-Friday, RO and Jira
300
315
  logwork reminder enable --time 18:00 --target ro
316
+ logwork reminder enable --repo ~/code/app --repo ~/code/api # add commit evidence
301
317
  logwork reminder test
302
318
  logwork reminder status
303
319
  logwork reminder disable
@@ -305,7 +321,19 @@ logwork reminder disable
305
321
 
306
322
  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
323
 
308
- macOS uses a user `launchd` agent (`sg.vinova.logwork-cli.reminder`) and Windows uses Task Scheduler (`Logwork Reminder`). Neither schedule stores credentials.
324
+ 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:
325
+
326
+ ```text
327
+ Logwork hôm nay chưa hoàn tất
328
+ RO: 4/8h, còn thiếu 4h
329
+ Hôm nay: 5 commit trong 2 repo
330
+
331
+ Mở Terminal và chạy: logwork commits today (bản nháp từ commit)
332
+ ```
333
+
334
+ 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.
335
+
336
+ macOS uses a user `launchd` agent (`sg.vinova.logwork-cli.reminder`) and Windows uses Task Scheduler (`Logwork Reminder`). Neither schedule stores credentials. On macOS the schedule runs the `logwork-launch` launcher, so Node upgrades do not break it. On Windows it runs a stable `node.exe`. A schedule from an older version that still runs a raw Node path is flagged by `logwork reminder status`; run `logwork setup` to re-register it.
309
337
 
310
338
  ## Reconcile RO And Jira
311
339
 
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`
@@ -63,8 +64,9 @@ The gate must pass all of the following:
63
64
  ## Manual Verification
64
65
 
65
66
  - Install from the packed tarball in a temporary directory.
66
- - Run `logwork setup --no-login`.
67
- - Confirm the printed MCP config uses server name `logwork` and `node ~/.logwork-cli/bin/logwork.mjs mcp`.
67
+ - Run `logwork setup --no-login` with a temporary `HOME` and `LOGWORK_HOME`, so no real MCP client config is touched.
68
+ - Confirm `~/.logwork-cli/bin/logwork-launch` exists and is executable (macOS), and that `logwork-launch --version` prints the package version.
69
+ - Confirm `logwork mcp install --dry-run` lists the detected clients with server name `logwork` and command `~/.logwork-cli/bin/logwork-launch mcp` (Windows: `node.exe ...\bin\logwork.mjs mcp`), and that `logwork mcp status` reports them.
68
70
  - Run `logwork --help` and `logwork --version`.
69
71
  - Run `logwork update check --force` against the published npm version.
70
72
  - Run `logwork mcp` through an MCP client smoke test and confirm it lists the 16 tools (`get_logwork`, `draft_from_commits`, `list_projects`, `list_tasks`, `log_work`, `edit_entry`, `resubmit_entry`, `delete_entry`, `log_jira_work`, `get_jira_issue`, `reconcile`, `get_history`, `map_project`, `login`, `reminder`, `update`).
package/bin/logwork.mjs CHANGED
@@ -20,8 +20,8 @@ export const COMMANDS = [
20
20
  { name: 'logout', usage: 'logout [ro|jira]', summary: 'Remove the stored RO (default) or Jira session', load: () => import('../commands/auth.mjs'), fn: 'runLogout' },
21
21
  { name: 'status', usage: 'status', summary: 'Show RO and Jira login status', load: () => import('../commands/auth.mjs'), fn: 'runStatus' },
22
22
  { name: 'skill', usage: 'skill [print|install] [--target claude|codex|all]', summary: 'Print or install the AI agent skill for logging from commits', load: () => import('../commands/skill.mjs'), fn: 'run' },
23
- { name: 'setup', usage: 'setup [--login|--no-login]', summary: 'Install the runtime and print MCP configs', load: () => import('../commands/setup.mjs'), fn: 'run' },
24
- { name: 'mcp', usage: 'mcp', summary: 'Run the MCP stdio server (used by IDE configs)', load: () => import('../lib/mcp-server.mjs'), fn: 'startMcpServer' },
23
+ { name: 'setup', usage: 'setup [--yes] [--no-mcp-install] [--login|--no-login]', summary: 'Install the runtime and add logwork to your MCP clients', load: () => import('../commands/setup.mjs'), fn: 'run' },
24
+ { name: 'mcp', usage: 'mcp [install|status]', summary: 'Run the MCP stdio server; install or check IDE configs', load: () => import('../commands/mcp.mjs'), fn: 'run' },
25
25
  { name: 'reminder', usage: 'reminder enable|disable|status|test', summary: 'Manage the weekday logwork reminder', load: () => import('../commands/reminder.mjs'), fn: 'runReminderCli' },
26
26
  { name: 'update', usage: 'update [check|install]', summary: 'Check for or install a new version', load: () => import('../commands/update.mjs'), fn: 'runUpdateCli' },
27
27
  { name: 'doctor', usage: 'doctor', summary: 'Write a sanitized diagnostics report', load: () => import('../commands/doctor.mjs'), fn: 'run' },
@@ -1,10 +1,18 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { basename, dirname, join, resolve } from 'node:path';
1
3
  import { draftLogworkFromCommits } from '../lib/commit-workflow.mjs';
4
+ import { upsertProjectMapping } from '../lib/project-mapping-workflow.mjs';
2
5
  import { normalizeReconciliationPeriod } from '../lib/reconciliation-workflow.mjs';
6
+ import { loadReminderConfig } from '../lib/reminder-config.mjs';
7
+ import { collectReminderRepos } from '../lib/reminder-workflow.mjs';
8
+ import { chooseRepoProject } from '../lib/ui.mjs';
3
9
 
4
- // `logwork commits [period] [--repo path]... [--author x] [--no-bookings] [--json]`:
5
- // turn your git commits into a logwork draft. Read-only.
10
+ // `logwork commits [period] [--repo path]... [--author x] [--no-bookings] [--no-prompt] [--json]`:
11
+ // turn your git commits into a logwork draft. Never writes to RO or Jira; in an
12
+ // interactive terminal it asks once per unmapped repo which project it belongs
13
+ // to and saves the answer as a local repo mapping.
6
14
  export function parseCommitsArgs(args = []) {
7
- const options = { period: 'today', repos: [], author: undefined, useBookings: true, json: false, help: false };
15
+ const options = { period: 'today', repos: [], author: undefined, useBookings: true, json: false, prompt: true, help: false };
8
16
  const positional = [];
9
17
  for (let index = 0; index < args.length; index += 1) {
10
18
  const arg = args[index];
@@ -14,6 +22,8 @@ export function parseCommitsArgs(args = []) {
14
22
  options.json = true;
15
23
  } else if (arg === '--no-bookings') {
16
24
  options.useBookings = false;
25
+ } else if (arg === '--no-prompt') {
26
+ options.prompt = false;
17
27
  } else if (arg === '--repo' || arg === '--author') {
18
28
  const value = args[index + 1];
19
29
  if (!value || value.startsWith('--')) {
@@ -44,29 +54,125 @@ export function parseCommitsArgs(args = []) {
44
54
  return options;
45
55
  }
46
56
 
47
- export async function run(args = [], { draft = draftLogworkFromCommits, print = console.log } = {}) {
57
+ export async function run(args = [], {
58
+ draft = draftLogworkFromCommits,
59
+ print = console.log,
60
+ cwd = process.cwd(),
61
+ reminderRepos = loadReminderRepos,
62
+ isInteractive = defaultIsInteractive,
63
+ chooseProject = chooseRepoProject,
64
+ saveMapping = upsertProjectMapping
65
+ } = {}) {
48
66
  const options = parseCommitsArgs(args);
49
67
  if (options.help) {
50
68
  print(formatCommitsHelp());
51
69
  return 0;
52
70
  }
53
- const result = await draft({
71
+ // The reminder tells the user to run `logwork commits today` from a fresh
72
+ // terminal, which starts outside any repo. There, fall back to the
73
+ // reminder's repos instead of reporting no commits.
74
+ let repos = options.repos;
75
+ let fallbackNote = '';
76
+ if (!repos.length && !isInsideGitWorkTree(cwd)) {
77
+ const fallback = await reminderRepos().catch(() => []);
78
+ if (fallback.length) {
79
+ repos = fallback;
80
+ fallbackNote = `Not in a git repo; using the reminder repos: ${fallback.join(', ')}`;
81
+ }
82
+ }
83
+ const draftOptions = {
54
84
  period: options.period,
55
- repos: options.repos,
85
+ repos,
56
86
  author: options.author,
57
87
  useBookings: options.useBookings
58
- });
59
- print(options.json ? JSON.stringify(result, null, 2) : result.summary);
88
+ };
89
+ const result = await draft(draftOptions);
90
+ if (options.json) {
91
+ print(JSON.stringify(result, null, 2));
92
+ return 0;
93
+ }
94
+ print(fallbackNote ? `${fallbackNote}\n\n${result.summary}` : result.summary);
95
+
96
+ if (!options.prompt || !isInteractive() || !result.unmappedRepos?.length) {
97
+ return 0;
98
+ }
99
+ const mapped = await askRepoProjects(result, { chooseProject, saveMapping, print, cwd });
100
+ if (mapped) {
101
+ const updated = await draft(draftOptions);
102
+ print(`\nUpdated draft:\n\n${updated.summary}`);
103
+ }
60
104
  return 0;
61
105
  }
62
106
 
107
+ // One select prompt per unmapped repo (once per run); a chosen project is
108
+ // saved as a user-scope repo mapping. Returns how many repos were mapped.
109
+ export async function askRepoProjects(result, { chooseProject = chooseRepoProject, saveMapping = upsertProjectMapping, print = console.log, cwd = process.cwd() } = {}) {
110
+ const candidates = result.projectCandidates || [];
111
+ if (!candidates.length) {
112
+ return 0;
113
+ }
114
+ let mapped = 0;
115
+ for (const repo of result.unmappedRepos) {
116
+ const repoName = basename(repo) || repo;
117
+ const project = await chooseProject({
118
+ repoName,
119
+ suggestions: result.repoSuggestions?.[repo] || [],
120
+ candidates
121
+ });
122
+ if (!project) {
123
+ print(`Skipped ${repoName}; it will be asked again next time.`);
124
+ continue;
125
+ }
126
+ try {
127
+ await saveMapping({
128
+ repos: [repo],
129
+ ...(project.projectMemberId !== undefined ? { projectMemberId: project.projectMemberId } : { projectName: project.projectName }),
130
+ scope: 'user',
131
+ confirm: true,
132
+ cwd
133
+ });
134
+ print(`Mapped ${repoName} to ${project.projectName}. Later drafts use it automatically.`);
135
+ mapped += 1;
136
+ } catch (error) {
137
+ print(`Could not map ${repoName}: ${error.message}`);
138
+ }
139
+ }
140
+ return mapped;
141
+ }
142
+
143
+ function defaultIsInteractive() {
144
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
145
+ }
146
+
147
+ // True when dir or a parent holds .git (a directory, or a file for worktrees
148
+ // and submodules). No git process is started.
149
+ export function isInsideGitWorkTree(dir, { exists = existsSync } = {}) {
150
+ let current = resolve(dir);
151
+ for (;;) {
152
+ if (exists(join(current, '.git'))) {
153
+ return true;
154
+ }
155
+ const parent = dirname(current);
156
+ if (parent === current) {
157
+ return false;
158
+ }
159
+ current = parent;
160
+ }
161
+ }
162
+
163
+ async function loadReminderRepos() {
164
+ return collectReminderRepos({ config: await loadReminderConfig() });
165
+ }
166
+
63
167
  export function formatCommitsHelp() {
64
168
  return `Usage:
65
169
  logwork commits [today|yesterday|this-week|last-week|this-month|last-month]
66
- [--repo <path>]... [--author <email>] [--no-bookings] [--json]
170
+ [--repo <path>]... [--author <email>] [--no-bookings]
171
+ [--no-prompt] [--json]
67
172
 
68
173
  Reads your commits (all branches, your git user.email) from each repo (default:
69
- the current directory) and drafts logwork lines:
174
+ the current directory; outside a git repo, the repos set with
175
+ \`logwork reminder enable --repo\`) and drafts logwork lines:
70
176
  +<hours> <worklog task> | <description>
71
177
 
72
178
  - Commits are grouped per day by repo, ticket key and kind of work.
@@ -74,9 +180,18 @@ the current directory) and drafts logwork lines:
74
180
  fix -> Bug Fix, refactor/perf/review -> Code Review & Refactoring,
75
181
  test -> Testing Activities, ci/build/release -> Deployment & Release,
76
182
  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.
183
+ - Each group gets an RO project: a mapping whose repos holds the repo, else
184
+ ticket/keyword mappings and that day's bookings.
185
+ - For a repo with no project, an interactive terminal asks once: "Which RO
186
+ project does <repo> belong to?", with the likely project preselected (ticket
187
+ history, repo/remote name, single booked project). The answer is saved as a
188
+ repo mapping and the draft is redone. Skip it to be asked again next time;
189
+ --no-prompt (or --json, or a non-interactive terminal) never asks.
190
+ - Each project's RO remaining time (booked - logged) is split across its
191
+ groups by how much work each holds; groups without a project share the room
192
+ of booked projects no group claimed. With --no-bookings or without login, a
193
+ weekday is 8h split across all groups.
79
194
 
80
- Nothing is written. Review the draft, then paste it into /logwork or give it
195
+ Nothing is written to RO or Jira. Review the draft, then paste it into /logwork or give it
81
196
  to your AI agent (log_work).`;
82
197
  }
@@ -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
- }
@@ -0,0 +1,266 @@
1
+ // `logwork mcp`: with no arguments, the MCP stdio server that every IDE config
2
+ // runs. Nothing may be written to stdout before the server starts, because
3
+ // stdout is the protocol channel. `install`, `status` and `--help` are
4
+ // dispatched first. The modules imported here never write to stdout.
5
+ import { constants as fsConstants, promises as fs } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { writeLauncher } from '../lib/launcher.mjs';
8
+ import { helperHome } from '../lib/paths.mjs';
9
+ import {
10
+ MCP_INSTALL_CLIENT_KEYS,
11
+ applyMcpClientPlan,
12
+ createMcpInstallContext,
13
+ describeMcpPlan,
14
+ inspectMcpClients,
15
+ normalizeMcpInstallClient,
16
+ planMcpInstall
17
+ } from '../lib/mcp-install.mjs';
18
+
19
+ export async function run(argv = []) {
20
+ if (!argv.length) {
21
+ const { startMcpServer } = await import('../lib/mcp-server.mjs');
22
+ return startMcpServer();
23
+ }
24
+ const [sub, ...rest] = argv;
25
+ if (sub === '--help' || sub === '-h' || sub === 'help') {
26
+ console.log(formatMcpHelp());
27
+ return 0;
28
+ }
29
+ if (sub === 'install') {
30
+ if (rest.includes('--help') || rest.includes('-h')) {
31
+ console.log(formatMcpHelp());
32
+ return 0;
33
+ }
34
+ return runMcpInstallCli(parseMcpInstallArgs(rest));
35
+ }
36
+ if (sub === 'status') {
37
+ if (rest.includes('--help') || rest.includes('-h')) {
38
+ console.log(formatMcpHelp());
39
+ return 0;
40
+ }
41
+ if (rest.length) {
42
+ throw new Error(`Unknown mcp status option: ${rest[0]}`);
43
+ }
44
+ return runMcpStatusCli();
45
+ }
46
+ throw new Error(`Unknown mcp command: ${sub}. Run \`logwork mcp --help\`.`);
47
+ }
48
+
49
+ export function formatMcpHelp() {
50
+ return `Usage:
51
+ logwork mcp Run the MCP stdio server (what IDE configs run)
52
+ logwork mcp install [--client claude|cursor|codex|vscode|antigravity]... [--yes] [--dry-run]
53
+ logwork mcp status
54
+
55
+ install Adds or updates the "logwork" server in the MCP clients found on this
56
+ machine (Claude Code, Cursor, Codex, VS Code, Google Antigravity).
57
+ Asks once per client unless --yes. Each edited file is backed up next
58
+ to itself first (<file>.logwork-backup-<timestamp>). An old
59
+ "logwork-helper" entry is removed.
60
+ status Shows each client and whether its "logwork" entry is current, stale
61
+ or missing.
62
+
63
+ Options:
64
+ --client <name> Only this client (repeatable)
65
+ --yes, -y Update every selected client without asking
66
+ --dry-run Show the planned changes without writing anything`;
67
+ }
68
+
69
+ export function parseMcpInstallArgs(args = []) {
70
+ const options = { clients: [], yes: false, dryRun: false };
71
+ for (let index = 0; index < args.length; index += 1) {
72
+ const arg = args[index];
73
+ if (arg === '--yes' || arg === '-y') {
74
+ options.yes = true;
75
+ } else if (arg === '--dry-run') {
76
+ options.dryRun = true;
77
+ } else if (arg === '--client' || arg.startsWith('--client=')) {
78
+ const value = arg === '--client' ? args[++index] : arg.slice('--client='.length);
79
+ const key = normalizeMcpInstallClient(value);
80
+ if (!key) {
81
+ throw new Error(`Unknown MCP client: ${value || '(missing)'}. Use claude, cursor, codex, vscode or antigravity.`);
82
+ }
83
+ if (!options.clients.includes(key)) {
84
+ options.clients.push(key);
85
+ }
86
+ } else {
87
+ throw new Error(`Unknown mcp install option: ${arg}`);
88
+ }
89
+ }
90
+ return options;
91
+ }
92
+
93
+ async function runMcpInstallCli(options) {
94
+ const ctx = createMcpInstallContext();
95
+ const warning = await prepareRuntimeLauncher(helperHome(), { dryRun: options.dryRun });
96
+ if (warning) {
97
+ console.error(warning);
98
+ }
99
+ const result = await runMcpInstall({ ...options, ctx, interactive: isInteractiveTerminal() });
100
+ console.log(result.output);
101
+ return result.results.some((item) => item.status === 'failed') ? 1 : 0;
102
+ }
103
+
104
+ async function runMcpStatusCli() {
105
+ const result = await runMcpStatus({ ctx: createMcpInstallContext() });
106
+ console.log(result.output);
107
+ return 0;
108
+ }
109
+
110
+ // The configs point at <home>/bin/logwork-launch (macOS/Linux), which
111
+ // `logwork setup` writes. Write it here too, so `logwork mcp install` works
112
+ // without a fresh setup. Returns a warning when the runtime itself is missing.
113
+ export async function prepareRuntimeLauncher(home, {
114
+ dryRun = false,
115
+ platform = process.platform,
116
+ access = fs.access,
117
+ write = writeLauncher
118
+ } = {}) {
119
+ const entry = join(home, 'bin', 'logwork.mjs');
120
+ try {
121
+ await access(entry, fsConstants.F_OK);
122
+ } catch {
123
+ return `Warning: the logwork runtime is missing at ${entry}. Run \`logwork setup\` so the configured command can start.`;
124
+ }
125
+ if (!dryRun && platform !== 'win32') {
126
+ await write({ home, platform });
127
+ }
128
+ return '';
129
+ }
130
+
131
+ export function isInteractiveTerminal() {
132
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
133
+ }
134
+
135
+ async function clackConfirm(message) {
136
+ const { confirm, isCancel } = await import('@clack/prompts');
137
+ const answer = await confirm({ message, initialValue: true });
138
+ return !isCancel(answer) && Boolean(answer);
139
+ }
140
+
141
+ // Plans, asks (unless --yes), applies and formats. Returns
142
+ // { plans, results, output, pending } where pending lists the clients that
143
+ // still need a change when nothing was applied (dry run or not interactive).
144
+ export async function runMcpInstall({
145
+ ctx,
146
+ clients = [],
147
+ yes = false,
148
+ dryRun = false,
149
+ interactive = false,
150
+ confirmFn = clackConfirm
151
+ } = {}) {
152
+ const explicit = clients.length > 0;
153
+ const plans = await planMcpInstall(ctx, {
154
+ clients: explicit ? clients : MCP_INSTALL_CLIENT_KEYS,
155
+ explicit
156
+ });
157
+ const header = `MCP server "logwork" runs: ${formatCommandLine(ctx)}`;
158
+ const pending = plans.filter((plan) => plan.action === 'add' || plan.action === 'update');
159
+
160
+ if (!plans.length) {
161
+ return {
162
+ plans,
163
+ results: [],
164
+ pending,
165
+ output: [header, 'No MCP clients found (Claude Code, Cursor, Codex, VS Code, Google Antigravity). Use `/mcp` in the terminal UI for copy-ready configs.'].join('\n')
166
+ };
167
+ }
168
+
169
+ if (dryRun || (!yes && !interactive)) {
170
+ const lines = [header, dryRun ? 'Dry run: nothing was written.' : 'Not an interactive terminal: nothing was written.'];
171
+ for (const plan of plans) {
172
+ lines.push(...describeMcpPlan(plan, ctx));
173
+ }
174
+ if (pending.length && !dryRun) {
175
+ lines.push('Run `logwork mcp install --yes` to apply these changes.');
176
+ }
177
+ return { plans, results: [], pending, output: lines.join('\n') };
178
+ }
179
+
180
+ const results = [];
181
+ for (const plan of plans) {
182
+ if ((plan.action === 'add' || plan.action === 'update') && !yes) {
183
+ const verb = plan.action === 'add' ? 'Add' : 'Update';
184
+ const where = plan.kind === 'claude' && plan.cliPath ? 'with the claude CLI' : plan.file;
185
+ const accepted = await confirmFn(`${verb} the logwork MCP server in ${plan.label} (${where})?`);
186
+ if (!accepted) {
187
+ results.push({ key: plan.key, label: plan.label, file: plan.file, status: 'skipped' });
188
+ continue;
189
+ }
190
+ }
191
+ results.push(await applyMcpClientPlan(plan, ctx));
192
+ }
193
+ return { plans, results, pending, output: [header, ...formatMcpInstallResults(results)].join('\n') };
194
+ }
195
+
196
+ export function formatMcpInstallResults(results) {
197
+ const lines = [];
198
+ for (const result of results) {
199
+ switch (result.status) {
200
+ case 'added':
201
+ case 'updated':
202
+ lines.push(`- ${result.label}: ${result.status}${result.via ? ` (${result.via})` : ` (${result.file})`}${result.backup ? `; backup ${result.backup}` : ''}`);
203
+ break;
204
+ case 'up_to_date':
205
+ lines.push(`- ${result.label}: already up to date`);
206
+ break;
207
+ case 'skipped':
208
+ lines.push(`- ${result.label}: skipped`);
209
+ break;
210
+ case 'not_detected':
211
+ lines.push(`- ${result.label}: not detected; skipped`);
212
+ break;
213
+ default:
214
+ lines.push(`- ${result.label}: could not update automatically (${result.reason || 'unknown error'}). Add this manually${result.file ? ` to ${result.file}` : ''}:`);
215
+ lines.push(...String(result.snippet || '').split('\n').map((line) => ` ${line}`));
216
+ }
217
+ }
218
+ if (results.some((result) => result.status === 'added' || result.status === 'updated')) {
219
+ lines.push('Restart or reload the updated clients to pick up the change.');
220
+ }
221
+ return lines;
222
+ }
223
+
224
+ export async function runMcpStatus({ ctx } = {}) {
225
+ const clients = await inspectMcpClients(ctx);
226
+ const lines = [`MCP server "logwork" should run: ${formatCommandLine(ctx)}`];
227
+ const commandExists = await ctx.fs.access(ctx.launch.command).then(() => true, () => false);
228
+ if (!commandExists) {
229
+ lines.push(`Warning: ${ctx.launch.command} is missing, so these entries cannot start. Run \`logwork setup\`.`);
230
+ }
231
+ for (const client of clients) {
232
+ lines.push(...formatClientStatus(client));
233
+ }
234
+ const needsWork = clients.some((client) => client.detected && (client.status !== 'current' || client.legacy || (client.localScopes || []).length));
235
+ if (!clients.some((client) => client.detected)) {
236
+ lines.push('No MCP clients found. Use `/mcp` in the terminal UI for copy-ready configs.');
237
+ } else if (needsWork) {
238
+ lines.push('Run `logwork mcp install` to fix the entries above.');
239
+ }
240
+ return { clients, output: lines.join('\n') };
241
+ }
242
+
243
+ function formatClientStatus(client) {
244
+ if (!client.detected) {
245
+ return [`- ${client.label}: not detected`];
246
+ }
247
+ const where = client.kind === 'claude' && client.cliPath ? `${client.file}; claude CLI found` : client.file;
248
+ const state = {
249
+ current: 'current',
250
+ missing: 'missing',
251
+ stale: `stale: ${client.reason}`,
252
+ unreadable: `unreadable: ${client.reason}`
253
+ }[client.status] || client.status;
254
+ const lines = [`- ${client.label} (${where}): ${state}`];
255
+ if (client.legacy) {
256
+ lines.push(' leftover "logwork-helper" entry (old runtime)');
257
+ }
258
+ for (const stray of client.localScopes || []) {
259
+ lines.push(` local-scope "${stray.name}" entry for ${stray.project}: ${stray.reason || 'stale'}`);
260
+ }
261
+ return lines;
262
+ }
263
+
264
+ function formatCommandLine(ctx) {
265
+ return [ctx.launch.command, ...ctx.launch.argsPrefix, 'mcp'].join(' ');
266
+ }