logwork-cli 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,11 +1,21 @@
1
- Proprietary License
1
+ MIT License
2
2
 
3
- Copyright (c) 2026 Truc Tran and contributors.
4
- All rights reserved.
3
+ Copyright (c) 2026 malcohelper and contributors
5
4
 
6
- This project is proprietary and is not open source. Use, copying,
7
- modification, distribution, publication, sublicensing, or sale of this
8
- software or its documentation is permitted only with prior written permission
9
- from the copyright holder or under a separate written agreement.
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
10
11
 
11
- No license is granted by implication, estoppel, or otherwise.
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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,13 +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
- - **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.
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.
130
+
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.
124
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.
125
133
 
126
- Nothing is written. Paste the draft into `/logwork`, or let your AI agent use it.
134
+ Nothing is written to RO or Jira. Paste the draft into `/logwork`, or let your AI agent use it.
127
135
 
128
136
  For agents:
129
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.
130
139
  - **MCP prompt `log_from_commits`** gives an agent the full procedure:
131
140
  1. find gaps with `get_logwork`;
132
141
  2. draft from commits;
@@ -162,53 +171,35 @@ The default tasks shared by all projects are:
162
171
  | 183 | Testing Activities | create |
163
172
  | 111 | Other Activities | other |
164
173
 
165
- ## API Notes
166
-
167
- - Read bookings: `GET /member-logtime/timesheet`
168
- - Read worklog tasks: `GET /logwork/tasks?project_id=<id>` and `GET /logwork/tasks/defaults?project_id=<id>`
169
- - Write an entry: `POST /logwork/entries`
170
- - Delete an entry: `DELETE /logwork/entries/:id`
171
- - Resubmit a rejected entry: `PATCH /logwork/entries/:id/resubmit`
172
-
173
- ```json
174
- {
175
- "worklog_task_id": 71,
176
- "type_of_work": "create",
177
- "effort_hours": 2,
178
- "logdate": "2026-06-01",
179
- "project_id": 720,
180
- "description": "Implement maintenance mode management and status UI (SCB-213)"
181
- }
182
- ```
183
-
184
- `project_id` is sent only when the worklog task is a shared default task.
185
-
186
174
  ## Set Up MCP
187
175
 
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`).
176
+ Run `logwork setup` and answer yes for each client. It finds the clients you have and adds or updates the `logwork` server in each:
189
177
 
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`:
178
+ | Client | What setup changes |
179
+ |---|---|
180
+ | Claude Code | `claude mcp add --scope user logwork ...` (or `~/.claude.json` when the `claude` CLI is not on `PATH`) |
181
+ | Cursor | `~/.cursor/mcp.json` |
182
+ | Codex | `~/.codex/config.toml` |
183
+ | VS Code | user `mcp.json` (`~/Library/Application Support/Code/User` on macOS, `%APPDATA%\Code\User` on Windows) |
184
+ | Google Antigravity | `~/.gemini/antigravity/mcp_config.json` |
191
185
 
192
- ```json
193
- {
194
- "mcpServers": {
195
- "logwork": {
196
- "command": "/Users/you/.nvm/versions/node/v24.1.0/bin/node",
197
- "args": ["/Users/you/.logwork-cli/bin/logwork.mjs", "mcp"]
198
- }
199
- }
200
- }
201
- ```
202
-
203
- For Claude Code, run:
186
+ 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.
204
187
 
205
188
  ```bash
206
- claude mcp add --transport stdio logwork -- "$(which node)" ~/.logwork-cli/bin/logwork.mjs mcp
189
+ logwork mcp status # which clients have a current logwork entry
190
+ logwork mcp install # add or update them later (asks per client)
191
+ logwork mcp install --dry-run # show the changes without writing
192
+ logwork setup --yes # update every detected client without asking
193
+ logwork setup --no-mcp-install
207
194
  ```
208
195
 
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.
196
+ The configs never name a Node version, so Node upgrades need no config change:
197
+ - **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.
198
+ - **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`.
199
+
200
+ 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.
210
201
 
211
- Restart or reload the IDE's MCP tools after saving.
202
+ Restart or reload the IDE's MCP tools after a change.
212
203
 
213
204
  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**:
214
205
  1. Call the tool without `confirm`. It returns a preview with a `batchId` or `previewId` and a summary.
@@ -241,11 +232,11 @@ Options:
241
232
 
242
233
  The long forms `--task-name` and `--description` also work. Project and date are always preserved.
243
234
 
244
- Resource Optimiser has no staff API for editing an entry; the edit endpoint is PM-only. So apply **replaces** the entry:
235
+ Resource Optimiser only lets project managers edit an entry. So apply **replaces** the entry:
245
236
 
246
237
  1. It re-reads the entry and stops if the entry changed after the preview.
247
- 2. It creates the corrected entry with `POST /logwork/entries`.
248
- 3. It deletes the original with `DELETE /logwork/entries/:id`.
238
+ 2. It creates the corrected entry.
239
+ 3. It deletes the original.
249
240
  4. It verifies the new entry.
250
241
 
251
242
  If the create fails, the original is untouched. If the delete fails, the new entry is deleted again, so nothing changes.
@@ -292,7 +283,7 @@ In the terminal UI, `/logwork` opens a target picker. `/logwork ro` writes Resou
292
283
  - `/query this-week`
293
284
  - `/reconcile`
294
285
  - `/projects`
295
- - `/map SCB 5234` (ticket prefix) or `/map --repo . 5234` (this git repo, for commit drafts)
286
+ - `/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)
296
287
  - `/history`
297
288
  - `/diagnostics`
298
289
 
@@ -321,7 +312,7 @@ Mở Terminal và chạy: logwork commits today (bản nháp từ commit)
321
312
 
322
313
  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
314
 
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.
315
+ 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.
325
316
 
326
317
  ## Reconcile RO And Jira
327
318
 
@@ -354,4 +345,4 @@ Updating keeps OS credentials, project mappings, drafts and apply history. Recon
354
345
 
355
346
  ## License
356
347
 
357
- This project is proprietary and not open source. See [LICENSE](LICENSE).
348
+ [MIT](LICENSE) © 2026 malcohelper and contributors.
package/RELEASE.md CHANGED
@@ -64,8 +64,9 @@ The gate must pass all of the following:
64
64
  ## Manual Verification
65
65
 
66
66
  - Install from the packed tarball in a temporary directory.
67
- - Run `logwork setup --no-login`.
68
- - 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.
69
70
  - Run `logwork --help` and `logwork --version`.
70
71
  - Run `logwork update check --force` against the published npm version.
71
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,14 +1,18 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { dirname, join, resolve } from 'node:path';
2
+ import { basename, dirname, join, resolve } from 'node:path';
3
3
  import { draftLogworkFromCommits } from '../lib/commit-workflow.mjs';
4
+ import { upsertProjectMapping } from '../lib/project-mapping-workflow.mjs';
4
5
  import { normalizeReconciliationPeriod } from '../lib/reconciliation-workflow.mjs';
5
6
  import { loadReminderConfig } from '../lib/reminder-config.mjs';
6
7
  import { collectReminderRepos } from '../lib/reminder-workflow.mjs';
8
+ import { chooseRepoProject } from '../lib/ui.mjs';
7
9
 
8
- // `logwork commits [period] [--repo path]... [--author x] [--no-bookings] [--json]`:
9
- // 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.
10
14
  export function parseCommitsArgs(args = []) {
11
- 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 };
12
16
  const positional = [];
13
17
  for (let index = 0; index < args.length; index += 1) {
14
18
  const arg = args[index];
@@ -18,6 +22,8 @@ export function parseCommitsArgs(args = []) {
18
22
  options.json = true;
19
23
  } else if (arg === '--no-bookings') {
20
24
  options.useBookings = false;
25
+ } else if (arg === '--no-prompt') {
26
+ options.prompt = false;
21
27
  } else if (arg === '--repo' || arg === '--author') {
22
28
  const value = args[index + 1];
23
29
  if (!value || value.startsWith('--')) {
@@ -52,7 +58,10 @@ export async function run(args = [], {
52
58
  draft = draftLogworkFromCommits,
53
59
  print = console.log,
54
60
  cwd = process.cwd(),
55
- reminderRepos = loadReminderRepos
61
+ reminderRepos = loadReminderRepos,
62
+ isInteractive = defaultIsInteractive,
63
+ chooseProject = chooseRepoProject,
64
+ saveMapping = upsertProjectMapping
56
65
  } = {}) {
57
66
  const options = parseCommitsArgs(args);
58
67
  if (options.help) {
@@ -71,20 +80,70 @@ export async function run(args = [], {
71
80
  fallbackNote = `Not in a git repo; using the reminder repos: ${fallback.join(', ')}`;
72
81
  }
73
82
  }
74
- const result = await draft({
83
+ const draftOptions = {
75
84
  period: options.period,
76
85
  repos,
77
86
  author: options.author,
78
87
  useBookings: options.useBookings
79
- });
88
+ };
89
+ const result = await draft(draftOptions);
80
90
  if (options.json) {
81
91
  print(JSON.stringify(result, null, 2));
82
- } else {
83
- print(fallbackNote ? `${fallbackNote}\n\n${result.summary}` : result.summary);
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}`);
84
103
  }
85
104
  return 0;
86
105
  }
87
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
+
88
147
  // True when dir or a parent holds .git (a directory, or a file for worktrees
89
148
  // and submodules). No git process is started.
90
149
  export function isInsideGitWorkTree(dir, { exists = existsSync } = {}) {
@@ -108,7 +167,8 @@ async function loadReminderRepos() {
108
167
  export function formatCommitsHelp() {
109
168
  return `Usage:
110
169
  logwork commits [today|yesterday|this-week|last-week|this-month|last-month]
111
- [--repo <path>]... [--author <email>] [--no-bookings] [--json]
170
+ [--repo <path>]... [--author <email>] [--no-bookings]
171
+ [--no-prompt] [--json]
112
172
 
113
173
  Reads your commits (all branches, your git user.email) from each repo (default:
114
174
  the current directory; outside a git repo, the repos set with
@@ -120,14 +180,18 @@ the current directory; outside a git repo, the repos set with
120
180
  fix -> Bug Fix, refactor/perf/review -> Code Review & Refactoring,
121
181
  test -> Testing Activities, ci/build/release -> Deployment & Release,
122
182
  docs/design -> Requirements & Design, chore -> Other Activities.
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.
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.
126
190
  - Each project's RO remaining time (booked - logged) is split across its
127
191
  groups by how much work each holds; groups without a project share the room
128
192
  of booked projects no group claimed. With --no-bookings or without login, a
129
193
  weekday is 8h split across all groups.
130
194
 
131
- 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
132
196
  to your AI agent (log_work).`;
133
197
  }
@@ -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
+ }