@jamestesting888/tum-mcp 0.6.0 → 0.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changes
2
2
 
3
+ ## 0.6.1 — 2026-10-25
4
+
5
+ - Make `tum-mcp update` download and validate the latest npm release before
6
+ replacing the invoked installation and refreshing configured clients.
7
+ Preserve newer installed versions; `--local` refreshes without contacting npm.
8
+ - Make `tum-mcp uninstall` remove managed client setup and the npm package in
9
+ one command. Keep saved data by default; stop package removal on conflicts.
10
+ Client-only removal retains npm, and `--keep-package` supports managed cleanup.
11
+ - Run npm through Node on all platforms, serialize package operations, and use
12
+ an external worker while Windows replaces package files.
13
+ - Test actual global installations against a private synthetic registry, including
14
+ failed downloads/preparation/replacement, customized files, and data preservation.
15
+
3
16
  ## 0.6.0 — 2026-10-25
4
17
 
5
18
  - Merge course setup and updates into `sync-study`, with recovery for incomplete
@@ -20,8 +33,8 @@
20
33
  Reuse isolated test fixtures,
21
34
  retain native/platform coverage and expose slow tests and failures in CI.
22
35
  - Preserve Windows CRLF study instructions through repeated synchronization.
23
- - Group Artemis and Zulip implementation by provider, retaining read-only campus
24
- tools and Moodle compatibility. Fix OCR with aliased macOS temporary paths.
36
+ - Group Artemis and Zulip implementation by provider, retaining read-only
37
+ course tools. Fix OCR with aliased macOS temporary paths.
25
38
 
26
39
  Upgrading does not rebuild personal study sites automatically. Ask your agent to
27
40
  update the prepared website; customized renderer files are preserved for review.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Connect your coding agent to Moodle, Artemis and Zulip. Read course content and
4
4
  build a private, source-backed study workspace. Unofficial; independent of TUM.
5
5
 
6
- **Latest release: [0.6.0](https://www.npmjs.com/package/@jamestesting888/tum-mcp).**
6
+ **Latest release: [0.6.1](https://www.npmjs.com/package/@jamestesting888/tum-mcp).**
7
7
  [Release notes](CHANGELOG.md).
8
8
 
9
9
  ## Agent setup
@@ -36,13 +36,9 @@ Select services with **Space**, then **Enter**. Choose an installed browser and
36
36
  complete sign-in there. Only selected tabs open; Zulip needs no API key.
37
37
  Enable the [course and channel scopes](docs/tum-mcp.md#connections-and-scopes) you want to use.
38
38
 
39
- | Client | Setup |
40
- | --- | --- |
41
- | Codex | `--client codex` · plugin and skills; enable/restart when prompted |
42
- | Claude Code | `--client claude` · MCP registration and skills |
43
- | Cursor | `--client cursor` · MCP registration and all five skills; restart |
44
- | Google Antigravity | `--client antigravity` · MCP registration and all five skills; restart |
45
- | Other MCP clients | `--client generic` · configuration for manual installation |
39
+ Choose a client with `--client`: `codex`, `claude`, `cursor` or `antigravity`.
40
+ Setup configures MCP and installs all five skills; follow any activation or
41
+ restart prompts. Use `generic` for manual MCP setup.
46
42
 
47
43
  For existing Cursor and Antigravity setups, run `tum-mcp update` to add the skills.
48
44
  Updates and uninstall preserve customized files and report them for review.
@@ -51,7 +47,7 @@ Supports macOS, Windows, glibc Linux x86_64/ARM64 and WSL 3.x. In WSL, sign-in
51
47
  opens a Windows browser. Add `--host windows` to setup for a Windows client that
52
48
  uses the WSL backend. [Browser and WSL setup](docs/setup.md#wsl).
53
49
 
54
- Setup supplies a private Python runtime. New connections use encrypted local
50
+ Setup supplies a private Python runtime. Connections use encrypted local
55
51
  files and need no native keyring, Git or Docker.
56
52
 
57
53
  ## Read and study
@@ -167,11 +163,13 @@ stay local. Study progress is saved in your browser.
167
163
  ## Update
168
164
 
169
165
  ```sh
170
- npm install --global @jamestesting888/tum-mcp@latest
171
166
  tum-mcp update
172
167
  ```
173
168
 
174
- `update` prepares the invoked package's runtime and refreshes configured clients.
169
+ `update` downloads the latest release, validates its backend, installs it in the
170
+ same npm location and refreshes configured clients. It never downgrades a newer
171
+ installation. Use `tum-mcp update --local` to refresh the installed version without checking npm.
172
+ From 0.6.0 or earlier, upgrade the npm package once to obtain this command.
175
173
  Sessions, scopes and downloads carry forward. User-edited entries and skills are
176
174
  preserved and reported for review. Reload the client; refresh its cached plugin
177
175
  if needed. Study sites are updated separately through the study workflows.
@@ -179,14 +177,14 @@ if needed. Study sites are updated separately through the study workflows.
179
177
  ## Remove
180
178
 
181
179
  ```sh
182
- tum-mcp uninstall --all
183
- npm uninstall --global @jamestesting888/tum-mcp
180
+ tum-mcp uninstall
184
181
  ```
185
182
 
186
- Managed registrations and unchanged skills are removed. Settings, sessions,
183
+ The npm package, managed registrations and unchanged skills are removed. Settings, sessions,
187
184
  downloads and study projects are preserved. After closing clients, add
188
185
  `--purge-runtime` to remove generated runtimes or `--forget-credentials` to
189
- remove saved local sessions and historical Git tokens. [Removal details](docs/setup.md#update-and-remove).
186
+ remove saved local credentials. [Removal details](docs/setup.md#update-and-remove).
187
+ Use `--client NAME` to remove just one client, or `--keep-package` to retain npm.
190
188
 
191
189
  [Guide](docs/tum-mcp.md) · [Setup](docs/setup.md) · [Study workflows](docs/study.md) · [Development](docs/development.md)
192
190
 
@@ -2,8 +2,6 @@
2
2
 
3
3
  The npm package is the distribution. Its wheel contains one Python package,
4
4
  `src/tum_mcp/`, with peer `moodle/`, `artemis/` and `zulip/` provider packages.
5
- The retired standalone Moodle plugin and its checkout installer are no longer
6
- maintained.
7
5
  Client adapters still generate the configuration each host needs.
8
6
 
9
7
  ## Checks
@@ -19,6 +17,7 @@ npm test
19
17
  npm pack --ignore-scripts --pack-destination dist
20
18
  npm run test:consumer
21
19
  npm run test:upgrade
20
+ npm run test:package-lifecycle
22
21
  ```
23
22
 
24
23
  Python 3.13 is the managed runtime. Install Tesseract for OCR tests. Default tests
@@ -57,8 +56,11 @@ CI runs macOS, Windows and Linux, plus both distribution engines on WSL 3.x.
57
56
  Consumer checks install the actual npm artifact, discover tools, build a study
58
57
  site and test available Chromium browsers with temporary profiles and synthetic
59
58
  pages. WSL verifies Windows browser transport. No live account is used.
60
- Upgrade checks cover public 0.4.1/0.5.0, client refresh, customized-file conflicts,
59
+ Upgrade checks cover public 0.6.0, client refresh, customized-file conflicts,
61
60
  failed preparation, runtime repair and removal that preserves work/credentials.
61
+ The package-lifecycle check uses a local synthetic registry and real isolated
62
+ global npm prefixes to verify downloads, backend validation, replacement,
63
+ failure ordering and one-command removal. It does not publish a test release.
62
64
  All five jobs must pass on the exact pushed commit before completion is claimed.
63
65
 
64
66
  Live Moodle checks are opt-in: `TUM_MOODLE_LIVE=1 uv run pytest tests/live -q`.
@@ -97,14 +99,7 @@ tag and artifact integrity before pushing the release documentation.
97
99
 
98
100
  Client setup installs managed copies of the repository's skills; those copies are
99
101
  outputs, not another source tree. Use `skills customize` for a personal override.
100
- The Codex adapter generates a small host plugin because that client uses it to
101
- load skills and MCP together; it is separate from the removed top-level Moodle
102
- plugin distribution.
103
-
104
- Keep Moodle's existing native-store service/account identity, session paths and
105
- download locations stable during migrations. Environment variable names and the
106
- `tum-moodle` CLI alias remain compatible; its implementation now resides inside
107
- `tum_mcp.moodle`. No automatic native-store secret extraction occurs.
102
+ The Codex adapter generates a small host plugin to load skills and MCP together.
108
103
 
109
104
  Extraction uses bounded subprocesses. POSIX enforces CPU/process-group limits;
110
105
  Windows terminates the worker and observed children. Study imports and preview
package/docs/setup.md CHANGED
@@ -35,11 +35,10 @@ Run **`tum-mcp connect`**, with no arguments, in an interactive terminal:
35
35
  3. Complete TUM sign-in/MFA in the opened window. Only selected service tabs open.
36
36
  4. Return to the terminal for the saved-connection result.
37
37
 
38
- Moodle uses exactly the same selector and storage flow as Artemis and Zulip.
39
- No API key or Keychain prompt is needed. A temporary browser profile keeps the
40
- connection separate from your everyday browser. Ctrl+C closes it and preserves
38
+ All three services use the same sign-in and encrypted session storage flow.
39
+ No API key or native credential-store prompt is needed. A temporary browser
40
+ profile keeps the connection separate from your everyday browser. Ctrl+C closes it and preserves
41
41
  connections that already completed. A dumb terminal uses numbered prompts.
42
- Old service arguments and `--service`/`--auth` flags are no longer accepted.
43
42
 
44
43
  Chrome, Edge, Brave, Chromium, Vivaldi and Opera are detected in their standard
45
44
  locations. Selected preview channels and Opera GX are detected where installed.
@@ -113,19 +112,25 @@ browser discovery still requires functioning interop.
113
112
  Sessions are encrypted with owner-restricted local key files. This avoids native
114
113
  credential-store prompts, but anyone obtaining both key and ciphertext can
115
114
  decrypt them. [Storage locations and recovery](tum-mcp.md#connections-and-scopes)
116
- explain migration and backup. Existing Moodle data and downloads are preserved.
115
+ explain where sessions are saved and how to back them up.
117
116
 
118
117
  ## Update and remove
119
118
 
120
119
  ```sh
121
- npm install --global @jamestesting888/tum-mcp@latest
122
120
  tum-mcp update
123
121
  ```
124
122
 
125
- `update` activates the backend bundled in the invoked npm package and refreshes
126
- previously configured clients. It does not fetch a newer npm version itself,
127
- connect services, change scopes or register additional clients. `setup --client
128
- NAME` explicitly registers one client and can also refresh that client's managed
123
+ From 0.6.1, `update` retrieves the latest npm release, validates its backend in a
124
+ temporary installation, replaces the invoked package and refreshes previously
125
+ configured clients. It preserves the installation prefix and global/local mode.
126
+ It does not connect services, change scopes or register additional clients.
127
+ It never downgrades a newer installation. `update --local` skips npm and repairs
128
+ or refreshes the installed version. Source/pinned builds use this local mode;
129
+ temporary npx and linked source installations are not replaced automatically.
130
+ Automatic replacement also stops if the package directory contains configured
131
+ sessions, downloads or course work; move that data outside the package first.
132
+ For 0.6.0 and earlier, first install the newer npm package once.
133
+ `setup --client NAME` explicitly registers one client and can also refresh that client's managed
129
134
  files. WSL Windows registrations keep their distribution/user/data paths.
130
135
 
131
136
  Ownership receipts track the entries and files installed by TUM MCP. Unchanged
@@ -138,57 +143,40 @@ not selected, and a damaged runtime is repaired into a fresh generation. A newer
138
143
  managed client is not silently downgraded by an older package; intentional
139
144
  rollback starts by unregistering that client, then registering the reviewed version.
140
145
 
141
- 0.4.1/0.5.0 installations without receipts are adopted only after matching their
142
- retained runtime's canonical client output. The seven former workflow skills and the three website skills are replaced by
143
- `read-content`, `sync-study` and `prepare-study`; `audio-notes` adds
144
- spoken lessons derived from the prepared website. Personal overrides
145
- under retired names remain in the config `skills/` directory; review and merge
146
- retrieval guidance into `read-content` before exporting a replacement pack.
147
146
  Reload MCP servers in Cursor/Antigravity, restart Claude as appropriate, and
148
147
  update/reinstall TUM MCP from the personal source in Codex Plugins if its cached
149
148
  copy still shows the older skills. Writing source files does not activate a host.
150
149
 
151
150
  Remove a single client with `tum-mcp uninstall --client cursor` (replace the
152
151
  client as needed; add `--host windows` from WSL for a Windows client). To remove
153
- all managed registrations before uninstalling npm:
152
+ the npm package and all managed registrations:
154
153
 
155
154
  ```sh
156
- tum-mcp uninstall --all
157
- npm uninstall --global @jamestesting888/tum-mcp
155
+ tum-mcp uninstall
158
156
  ```
159
157
 
160
158
  Custom edits and user-added files are preserved. Codex's local source/catalog
161
159
  entry can be removed, but its cached/active plugin must also be removed or
162
- disabled through Codex Plugins. No npm uninstall hook deletes user data.
160
+ disabled through Codex Plugins. Package removal runs only after successful
161
+ client cleanup; conflicts preserve npm so you can review and retry. `--all`
162
+ is accepted explicitly, and `--keep-package` removes client setup without npm.
163
+ No npm lifecycle hook performs cleanup or reads credentials.
163
164
 
164
165
  For explicit cleanup, **close coding clients first**, then run:
165
166
 
166
167
  ```sh
167
- tum-mcp uninstall --all --purge-runtime --forget-credentials
168
- npm uninstall --global @jamestesting888/tum-mcp
168
+ tum-mcp uninstall --purge-runtime --forget-credentials
169
169
  ```
170
170
 
171
171
  `--purge-runtime` deletes verified managed runtime generations after the Python
172
172
  helper exits, and removes unchanged managed bootstrap/browser-driver caches.
173
173
  Active tracked backends or unrecognized runtime files stop automatic removal.
174
174
  Paths overlapping configured assignment work, downloads or Moodle data are
175
- protected. `--forget-credentials` removes local encrypted service sessions, keys
176
- and participation Git tokens; it does not revoke remote credentials. Native
177
- Keychain/Credential Manager/Secret Service records and legacy Moodle sessions
178
- stay intact. Settings, custom skills, course cache, downloads and assignment
179
- work are retained by these commands. No command recursively deletes the campus
175
+ protected. `--forget-credentials` removes saved local credentials and session
176
+ keys; it does not revoke remote credentials. Settings, custom skills, course
177
+ cache, downloads and assignment work are retained by these commands.
178
+ No command recursively deletes the campus
180
179
  data root. Course audio and optional shared speech models/runtimes also remain.
181
180
  Remove a shared speech cache only when explicitly requested and no other course
182
181
  needs it; its host paths are documented in the audio skill. If removal is
183
- interrupted, run npm `setup` to recover before reuse.
184
-
185
- ## Existing Moodle-only installations
186
-
187
- The older `tum-moodle` commands and sixteen `moodle_*` tools remain compatible.
188
- The separate source/plugin installer retains its original native credential
189
- store: macOS Keychain, Windows Credential Manager, or Linux Secret Service.
190
- Its native-store identity, existing session paths and downloads remain intact.
191
- The combined `tum-mcp serve` uses local-file storage and directs all login
192
- requests to `tum-mcp connect`. Reconnect once through that selector to create the
193
- new session; no automatic extraction of older native-store secrets occurs.
194
- Disable the older plugin when activating TUM MCP to avoid duplicate tools.
182
+ interrupted, run `tum-mcp setup --client NAME` to recover before reuse.
package/docs/tum-mcp.md CHANGED
@@ -1,19 +1,19 @@
1
1
  # TUM MCP guide
2
2
 
3
3
  Package: `@jamestesting888/tum-mcp`, CLI: `tum-mcp`, backend: Python 3.13.
4
- Version **0.6.0** provides read-only course retrieval and five local study workflows.
4
+ Development version **0.6.1 (unreleased)** adds npm package management to the
5
+ read-only course retrieval and five local study workflows in 0.6.0.
5
6
 
6
7
  ## Current capabilities
7
8
 
8
- All sixteen `moodle_*` compatibility tools remain available, including materials,
9
- announcements, forums and explicitly scoped private-record reads. Artemis adds
9
+ The sixteen `moodle_*` tools provide materials, announcements, forums and
10
+ explicitly scoped private-record reads. Artemis adds
10
11
  available-course metadata, lectures and learning units, student PDF attachments,
11
12
  released non-quiz statements and permitted exact-commit feedback. Zulip provides
12
13
  scoped channel/topic search and cited discussion reads.
13
14
 
14
15
  Website access is read-only. There are no public exercise workspace, test-run,
15
- submission, start, deployment, rebuild or push workflows. The retained internal
16
- remote gate rejects every action even if an old verification flag is enabled.
16
+ submission, start, deployment, rebuild or push workflows.
17
17
  Local downloads, source inventories, study-site builds and checkpoint storage
18
18
  are local operations; they do not post, submit or mark campus content as read.
19
19
 
@@ -67,22 +67,19 @@ and Python 3.13.15; an existing matching uv, including the verified cached downl
67
67
  is reused. Source checkouts and system
68
68
  Python are unnecessary for consumers. Supported bootstrap targets are macOS
69
69
  arm64/x64, Windows arm64/x64, and glibc Linux arm64/x64; hands-on verification is
70
- recorded separately. New campus sessions use permission-restricted local keys and need no Secret Service.
71
- Clean consumer bootstrap, idempotent setup, diagnostics, and skill validation
72
- passed on macOS ARM64 and in Linux ARM64 and x86_64 containers. The x86_64 check
73
- ran through Docker amd64 emulation on an ARM64 Mac; packaged MCP protocol checks
74
- and an encrypted credential round trip through a real GNOME Secret Service in an
75
- isolated D-Bus session also passed. Real Linux desktop browser sign-in, macOS x64,
76
- and Windows hands-on checks remain open. New storage and terminal behavior need
77
- the current checkout’s test evidence; earlier native-store results do not certify them. The installer targets glibc Linux
78
- x86_64/amd64 and aarch64/ARM64.
79
-
80
- `update` prepares the backend bundled in the invoked package, refreshes
81
- previously configured clients and preserves older runtime generations.
70
+ recorded separately. Campus sessions use permission-restricted local keys.
71
+ CI checks clean installation, updates, removal, MCP discovery and synthetic
72
+ browser workflows on macOS, Windows, Linux and both WSL engines. See the
73
+ [development guide](development.md#checks) for the verification layers.
74
+
75
+ `update` downloads and validates the latest release, replaces the invoked npm
76
+ installation and refreshes previously configured clients. `update --local`
77
+ refreshes the installed version without npm. Retained runtimes keep pinned
78
+ clients working, and newer installed versions are not silently downgraded.
82
79
  [Update and removal behavior](setup.md#update-and-remove) covers ownership
83
- receipts, legacy adoption, conflict handling and explicit cleanup. To obtain a newer public package after publication,
84
- invoke its reviewed version explicitly. It does not publish a release or replace
85
- customized client skills silently. If interrupted setup leaves `setup.lock`,
80
+ receipts, conflict handling and explicit cleanup. `uninstall` removes managed
81
+ clients and npm; `--client NAME` or `--keep-package` retains the package.
82
+ Customized files and saved data are preserved. If interrupted setup leaves `setup.lock`,
86
83
  confirm that no setup process is running before removing that directory locally.
87
84
  An invalid or incomplete runtime is never selected for serving. Failed validation
88
85
  clears its ready marker so explicit setup can rebuild it; older runtimes remain
@@ -108,10 +105,9 @@ tum-mcp scope zulip ACCOUNT_ID --channel CHANNEL_ID
108
105
  `connect` takes no arguments or connection options. Move with up/down, toggle
109
106
  services with Space, and press Enter. Choose a browser when more than one is
110
107
  installed. Only selected tabs open in one temporary browser context. Moodle,
111
- Artemis and Zulip all follow this flow; no separate Moodle login is needed.
108
+ Artemis and Zulip all follow this flow.
112
109
  Sign-in/MFA stays in the browser. `TERM=dumb` uses numbered prompts. Ctrl+C
113
- cancels and retains completed connections. Old service/campus arguments and
114
- `--service`/`--auth` flags are rejected before any browser opens.
110
+ cancels and retains completed connections.
115
111
 
116
112
  Detection covers Chrome, Edge, Brave, Chromium, Vivaldi, Opera and selected
117
113
  preview channels. Set `TUM_MCP_BROWSER` to an executable path for another
@@ -121,41 +117,30 @@ Only service-owned cookies are saved; identity-provider cookies are discarded
121
117
  when the temporary browser closes. Existing browser profiles are never imported.
122
118
 
123
119
  Zulip uses its read-only `/json/` routes with the browser session; no API key is
124
- required. Reconnect through the same selector when a session expires. Existing
125
- API-key records remain readable for compatibility, but key entry is not part of
126
- onboarding. See [Zulip's dispatcher](https://github.com/zulip/zulip/blob/12.0/zerver/lib/rest.py).
120
+ required. Reconnect through the same selector when a session expires.
121
+ See [Zulip's dispatcher](https://github.com/zulip/zulip/blob/12.0/zerver/lib/rest.py).
127
122
  Existing scopes survive reconnection; new identities start with empty scopes.
128
123
  Private Zulip channels require `--private-channel ID` as well as `--channel ID`.
129
124
  `--sensitive` enables own Artemis participation/feedback reads. It grants no
130
125
  website mutation or local execution permission.
131
126
 
132
127
  Settings live outside the installed package in `TUMMCP/config/preferences.json`.
133
- The default data directory is beside the legacy `TUMMoodle` directory on each OS.
134
- Existing Moodle data and its native-store identity are retained in place. New
135
- campus connections deliberately do not read or migrate native-store secrets;
136
- reconnect each service once to use local-file storage. The combined MCP process
137
- uses that storage for all `moodle_*` compatibility tools. The separate Moodle-only
138
- installer keeps its native-store default.
139
- Historical Git credentials remain local data for legacy cleanup; the current
140
- package provides no assignment Git workflow.
141
-
142
- New Moodle sessions live under `TUMMoodle/local-session/`; campus credentials live
143
- under `TUMMCP/credentials/local/`. Each directory has an independent `session.key`.
128
+ Moodle sessions live under `TUMMoodle/local-session/`; Artemis and Zulip credentials
129
+ live under `TUMMCP/credentials/local/`. Each directory has an independent `session.key`.
144
130
  Encrypted records and keys have owner-only permissions: 0700 directories / 0600
145
131
  keys on POSIX; protected current-user DACLs on Windows through PowerShell. No
146
- administrator privileges are requested. This avoids Keychain/Secret Service
132
+ administrator privileges are requested. This avoids native credential-store
147
133
  prompts but protects less than a native store. A copy of both ciphertext and key
148
134
  is sufficient to decrypt a session. Do not share, sync publicly, or package them.
149
135
 
150
136
  Missing or damaged keys fail closed. Restore the matching key from a private
151
137
  backup. To start fresh, stop clients and move the affected **local storage folder**
152
- to a private backup before reconnecting; this also resets its stored Git tokens.
153
- Never remove the legacy Moodle directory or downloads to repair a login.
154
- Native-store records remain available to the older installation for rollback.
138
+ to a private backup before reconnecting. Keep downloaded course materials when
139
+ repairing a login.
155
140
 
156
141
  Zulip message text is retrieved live and is not persisted. Retrieved excerpts may
157
- be transmitted to the coding agent's configured AI provider. Moodle's existing
158
- document cache retains its documented plaintext/expiry behavior.
142
+ be transmitted to the coding agent's configured AI provider. Moodle caches
143
+ documents and extracted text locally in plaintext.
159
144
 
160
145
  Course nicknames can be mapped in `mappings` using explicit per-service IDs.
161
146
  The tools resolve one exact mapping; ambiguity never grants broader access.
@@ -191,11 +176,9 @@ progress are preserved during package updates/removal.
191
176
  ## Client and skill setup
192
177
 
193
178
  `setup --client codex` prepares a local plugin/marketplace entry using the
194
- portable Agent Plugins schemas. Enable it in
195
- Codex Plugins, restart, and verify real tool and skill loading. Disable the legacy
196
- TUM Moodle plugin when switching so duplicate tools/skills are not active.
197
- The old installation remains available for rollback. Setup reports required UI
198
- steps and never equates writing files with activation.
179
+ portable Agent Plugins schemas. Enable it in Codex Plugins, restart, and verify
180
+ real tool and skill loading. Setup reports required UI steps and never equates
181
+ writing files with activation.
199
182
 
200
183
  The Codex adapter uses a contained Node launcher to pin the managed Python in
201
184
  isolated mode; portable stdio manifests require a bare or contained executable.
@@ -291,5 +274,5 @@ it is described as verified. Test data and public fixtures are synthetic. Real
291
274
  course material, local agent notes and screenshots stay under ignored local paths.
292
275
  Maintainer checks include a reviewed tarball inventory, integrity checks and an
293
276
  isolated consumer install. Never publish or stage npm packages as a test. Registry
294
- publication requires separate user authorization and remains paused for this work.
277
+ publication is an explicit maintainer action.
295
278
  `tum-mcp status` reports registry state as `not_checked`; it reads local configuration.
@@ -1,5 +1,5 @@
1
1
  {
2
- "wheel": "tum_mcp-0.6.0-py3-none-any.whl",
3
- "wheelSha256": "8437422504920753d1d6781cd87c57065cfbb8c0630ff0c5fd1de237b2d00426",
2
+ "wheel": "tum_mcp-0.6.1-py3-none-any.whl",
3
+ "wheelSha256": "59ecf435716e6c0ab68ed9720f2c137b3236f64dd5e962db4c3bf82b76c45ba2",
4
4
  "requirementsSha256": "25f9efa7a4ffb0b97abcfeb9d4706d04808273ab1b35abcfaa1ef0b66e87591b"
5
5
  }
package/npm/lock.mjs ADDED
@@ -0,0 +1,43 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import { mkdir, rename, rm, rmdir, writeFile } from 'node:fs/promises';
4
+ import { hostname } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ export async function acquireLock(root, name = 'setup') {
8
+ await mkdir(root, { recursive: true, mode: 0o700 });
9
+ const lock = join(root, `${name}.lock`);
10
+ const owner = { pid: process.pid, host: hostname(), nonce: randomUUID() };
11
+ const guard = join(root, `${name}.claim`);
12
+ try { await mkdir(guard, { mode: 0o700 }); }
13
+ catch { throw new Error(`Another ${name} is claiming the lock. Retry; an interrupted ${name}.claim needs local review.`); }
14
+ try {
15
+ try { await mkdir(lock, { mode: 0o700 }); }
16
+ catch (error) {
17
+ if (error.code !== 'EEXIST') throw error;
18
+ let previous;
19
+ try { previous = JSON.parse(readFileSync(join(lock, 'owner.json'), 'utf8')); }
20
+ catch { throw new Error(`${name} lock has no readable owner. Confirm no operation is running before removing ${name}.lock.`); }
21
+ if (!Number.isSafeInteger(previous.pid) || previous.pid <= 0 || previous.host !== hostname()) {
22
+ throw new Error(`${name} lock belongs to an unknown owner; preserve it for local review.`);
23
+ }
24
+ let alive = true;
25
+ try { process.kill(previous.pid, 0); }
26
+ catch (failure) { if (failure.code === 'ESRCH') alive = false; }
27
+ if (alive) throw new Error(`Another ${name} is running; retry when it finishes.`);
28
+ const abandoned = `${lock}.${randomUUID()}.abandoned`;
29
+ await rename(lock, abandoned);
30
+ await rm(abandoned, { recursive: true, force: true });
31
+ try { await mkdir(lock, { mode: 0o700 }); }
32
+ catch { throw new Error(`Another ${name} acquired the lock; retry when it finishes.`); }
33
+ }
34
+ const temporary = join(lock, `${owner.nonce}.pending`);
35
+ await writeFile(temporary, JSON.stringify(owner), { mode: 0o600, flag: 'wx' });
36
+ await rename(temporary, join(lock, 'owner.json'));
37
+ } finally { await rmdir(guard); }
38
+ return async () => {
39
+ const current = JSON.parse(readFileSync(join(lock, 'owner.json'), 'utf8'));
40
+ if (current.nonce !== owner.nonce) throw new Error(`${name} lock ownership changed; it was preserved.`);
41
+ await rm(lock, { recursive: true, force: true });
42
+ };
43
+ }
@@ -0,0 +1,96 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { delimiter, dirname, join, basename, resolve, relative, isAbsolute, sep } from 'node:path';
5
+
6
+ export const packageName = '@jamestesting888/tum-mcp';
7
+
8
+ function canonical(value) {
9
+ let path = resolve(value.replace(/^~(?=$|[/\\])/, homedir()));
10
+ const tail = [];
11
+ while (!existsSync(path)) { tail.unshift(basename(path)); path = dirname(path); }
12
+ return join(realpathSync(path), ...tail);
13
+ }
14
+
15
+ export function packageStorage(root, dataDirectory) {
16
+ const home = homedir();
17
+ const base = process.platform === 'darwin' ? join(home, 'Library/Application Support')
18
+ : process.platform === 'win32' ? process.env.LOCALAPPDATA || join(home, 'AppData/Local')
19
+ : process.env.XDG_DATA_HOME || join(home, '.local/share');
20
+ const storage = {
21
+ TUM_MCP_DATA_DIR: canonical(dataDirectory),
22
+ TUM_MCP_CONFIG_DIR: canonical(process.env.TUM_MCP_CONFIG_DIR || join(dataDirectory, 'config')),
23
+ TUM_MOODLE_DATA_DIR: canonical(process.env.TUM_MOODLE_DATA_DIR || join(base, 'TUMMoodle')),
24
+ TUM_MOODLE_DOWNLOAD_DIR: canonical(process.env.TUM_MOODLE_DOWNLOAD_DIR || join(home, 'Downloads/TUM Moodle')),
25
+ };
26
+ const config = storage.TUM_MCP_CONFIG_DIR;
27
+ const settings = join(config, 'preferences.json');
28
+ let workspace = join(home, 'University');
29
+ if (existsSync(settings)) {
30
+ try {
31
+ const prefs = JSON.parse(readFileSync(settings, 'utf8'));
32
+ if (prefs.workspace_root !== undefined) {
33
+ if (typeof prefs.workspace_root !== 'string') throw new Error('Invalid storage path');
34
+ workspace = prefs.workspace_root;
35
+ }
36
+ } catch { throw new Error('Storage settings could not be verified; package files and saved data were preserved.'); }
37
+ }
38
+ for (const path of [...Object.values(storage), workspace]) {
39
+ const inside = relative(root, canonical(path));
40
+ if (inside === '' || (inside.split(sep)[0] !== '..' && !isAbsolute(inside))) {
41
+ throw new Error('Package installation overlaps saved data or course work. Move that data outside the package before update/removal.');
42
+ }
43
+ }
44
+ return storage;
45
+ }
46
+
47
+ export function npmCli() {
48
+ const paths = (process.env.PATH || '').split(delimiter);
49
+ const candidates = [process.env.npm_execpath,
50
+ join(dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js'),
51
+ join(dirname(dirname(process.execPath)), 'lib/node_modules/npm/bin/npm-cli.js'),
52
+ ...paths.flatMap(path => [join(path, 'npm'), join(path, 'node_modules/npm/bin/npm-cli.js')])];
53
+ for (const path of candidates) {
54
+ if (!path || !existsSync(path)) continue;
55
+ const resolved = realpathSync(path);
56
+ if (basename(resolved) !== 'npm-cli.js') continue;
57
+ const manifest = join(dirname(dirname(resolved)), 'package.json');
58
+ if (existsSync(manifest) && JSON.parse(readFileSync(manifest, 'utf8')).name === 'npm') return resolved;
59
+ }
60
+ throw new Error('npm could not be located beside Node or on PATH. Install Node.js with npm, then retry.');
61
+ }
62
+
63
+ export function runNpm(cli, args, capture = false) {
64
+ const result = spawnSync(process.execPath, [cli, ...args, '--ignore-scripts', '--no-audit', '--no-fund', '--no-update-notifier'], {
65
+ shell: false, stdio: capture ? ['ignore', 'pipe', 'inherit'] : 'inherit', encoding: 'utf8', timeout: 600000,
66
+ });
67
+ if (result.error || result.status !== 0) throw new Error('npm operation failed. Saved data and retained client runtimes were preserved; resolve the npm error and retry.');
68
+ return capture ? result.stdout.trim() : '';
69
+ }
70
+
71
+ export function installationTarget(root) {
72
+ const modules = dirname(dirname(root));
73
+ if (basename(modules) !== 'node_modules' || modules.split(/[\\/]/).some(part => ['.pnpm', '_npx'].includes(part))) {
74
+ throw new Error('Package management requires an npm installation. Use update --local for a source/pinned build.');
75
+ }
76
+ if (`${basename(dirname(root))}/${basename(root)}` !== packageName) {
77
+ throw new Error('Automatic package management requires the original package name; aliases were preserved.');
78
+ }
79
+ const parent = dirname(modules);
80
+ const prefix = process.platform === 'win32' ? parent : dirname(parent);
81
+ const shim = process.platform === 'win32' ? join(prefix, 'tum-mcp.cmd') : join(prefix, 'bin/tum-mcp');
82
+ if (existsSync(shim)) {
83
+ const owned = process.platform === 'win32'
84
+ ? readFileSync(shim, 'utf8').replaceAll('\\', '/').includes('node_modules/@jamestesting888/tum-mcp/npm/tum-mcp.mjs')
85
+ : realpathSync(shim) === join(root, 'npm/tum-mcp.mjs');
86
+ if (owned && (process.platform === 'win32' || basename(parent) === 'lib')) return { prefix, global: true };
87
+ }
88
+ const manifest = join(parent, 'package.json');
89
+ if (existsSync(manifest)) {
90
+ const project = JSON.parse(readFileSync(manifest, 'utf8'));
91
+ if (['dependencies', 'devDependencies', 'optionalDependencies'].some(key => project[key]?.[packageName])) {
92
+ return { prefix: parent, global: false };
93
+ }
94
+ }
95
+ throw new Error('The npm installation prefix could not be verified; package files were preserved.');
96
+ }
@@ -0,0 +1,109 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
3
+ import { copyFile, mkdtemp, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { basename, dirname, isAbsolute, join, relative, sep } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { acquireLock } from './lock.mjs';
8
+ import { installationTarget, npmCli, packageName, packageStorage, runNpm } from './npm-command.mjs';
9
+
10
+ function runLauncher(root, args, quiet = false) {
11
+ const result = spawnSync(process.execPath, [join(root, 'npm/tum-mcp.mjs'), ...args], {
12
+ stdio: quiet ? ['ignore', 'pipe', 'pipe'] : 'inherit', encoding: 'utf8', shell: false, timeout: 600000,
13
+ });
14
+ if (result.error || result.status !== 0) {
15
+ if (quiet) process.stderr.write((result.stdout || '') + (result.stderr || ''));
16
+ throw new Error(`${args[0]} did not complete. Saved data and customized client files were preserved; review the details and retry.`);
17
+ }
18
+ }
19
+
20
+ function versionParts(version) {
21
+ const match = typeof version === 'string' && version.match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?(?:\+[\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*)?$/);
22
+ if (!match) throw new Error('npm returned an invalid package version.');
23
+ const prerelease = match[4]?.split('.') || [];
24
+ if (prerelease.some(part => /^0\d+$/.test(part))) throw new Error('npm returned an invalid prerelease version.');
25
+ return { core: match.slice(1, 4).map(BigInt), prerelease };
26
+ }
27
+
28
+ export function newerVersion(latest, current) {
29
+ const left = versionParts(latest), right = versionParts(current);
30
+ for (let i = 0; i < left.core.length; i++) if (left.core[i] !== right.core[i]) return left.core[i] > right.core[i];
31
+ const a = left.prerelease, b = right.prerelease;
32
+ if (!a.length || !b.length) return !a.length && Boolean(b.length);
33
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
34
+ if (a[i] === undefined) return false;
35
+ if (b[i] === undefined) return true;
36
+ if (a[i] === b[i]) continue;
37
+ const numericA = /^\d+$/.test(a[i]), numericB = /^\d+$/.test(b[i]);
38
+ if (numericA && numericB) return BigInt(a[i]) > BigInt(b[i]);
39
+ if (numericA !== numericB) return numericB;
40
+ return a[i] > b[i];
41
+ }
42
+ return false;
43
+ }
44
+
45
+ async function operate(job) {
46
+ const release = await acquireLock(job.target.prefix, 'tum-mcp-package');
47
+ try {
48
+ const scope = ['--prefix', job.target.prefix, `--global=${job.target.global}`];
49
+ if (job.action === 'uninstall') {
50
+ const configured = existsSync(join(job.dataDirectory, 'runtime.json')) ||
51
+ existsSync(join(job.configDirectory, 'clients'));
52
+ if (configured || job.args.some(arg => ['--purge-runtime', '--forget-credentials'].includes(arg))) {
53
+ runLauncher(job.root, ['uninstall', '--keep-package', ...job.args]);
54
+ }
55
+ runNpm(job.npm, ['uninstall', ...scope, packageName]);
56
+ console.log('TUM MCP package and managed client setup removed. Saved user data was retained unless explicitly requested.');
57
+ return;
58
+ }
59
+ const current = JSON.parse(readFileSync(join(job.root, 'package.json'), 'utf8')).version;
60
+ const latest = JSON.parse(runNpm(job.npm, ['view', `${packageName}@latest`, 'version', '--json', ...scope], true));
61
+ if (!newerVersion(latest, current)) {
62
+ console.log(`Installed ${current}; no newer npm release. Refreshing configured clients.`);
63
+ runLauncher(job.root, ['update', '--local']);
64
+ return;
65
+ }
66
+ console.log(`Preparing TUM MCP ${latest}.`);
67
+ const packed = JSON.parse(runNpm(job.npm, ['pack', `${packageName}@${latest}`, '--pack-destination', job.temporary, '--json', ...scope], true));
68
+ if (!Array.isArray(packed) || packed.length !== 1 || packed[0].name !== packageName || packed[0].version !== latest || basename(packed[0].filename) !== packed[0].filename) {
69
+ throw new Error('npm package metadata did not match the selected release.');
70
+ }
71
+ const archive = join(job.temporary, packed[0].filename);
72
+ const staging = join(job.temporary, 'prepared');
73
+ runNpm(job.npm, ['install', '--prefix', staging, '--global=false', archive], true);
74
+ const prepared = join(staging, 'node_modules', packageName);
75
+ runLauncher(prepared, ['setup'], true);
76
+ // Only a healthy prepared release may replace the user's npm installation.
77
+ runNpm(job.npm, ['install', ...scope, job.target.global ? archive : `${packageName}@${latest}`]);
78
+ if (JSON.parse(readFileSync(join(job.root, 'package.json'), 'utf8')).version !== latest) throw new Error('npm did not install the selected version; client activation was stopped.');
79
+ runLauncher(job.root, ['update', '--local']);
80
+ console.log(`Updated TUM MCP to ${latest}. Restart clients to load refreshed skills.`);
81
+ } finally { await release(); }
82
+ }
83
+
84
+ export async function managePackage(action, args, dataDirectory) {
85
+ const root = dirname(dirname(fileURLToPath(import.meta.url)));
86
+ const inside = relative(root, process.cwd());
87
+ if (inside === '' || (inside.split(sep)[0] !== '..' && !isAbsolute(inside))) throw new Error('Run package update/removal from outside the installed package directory.');
88
+ const target = installationTarget(root);
89
+ const storage = packageStorage(root, dataDirectory);
90
+ const configDirectory = storage.TUM_MCP_CONFIG_DIR;
91
+ const npm = npmCli();
92
+ const temporary = await mkdtemp(join(tmpdir(), 'tum-mcp-package-'));
93
+ try {
94
+ for (const name of ['package-manager.mjs', 'npm-command.mjs', 'lock.mjs']) await copyFile(join(root, 'npm', name), join(temporary, name));
95
+ const job = { action, args, root, dataDirectory, configDirectory, target, npm, temporary };
96
+ const path = join(temporary, 'job.json');
97
+ await writeFile(path, JSON.stringify(job), { mode: 0o600, flag: 'wx' });
98
+ // This worker and its cwd survive Windows replacing/removing the package files.
99
+ const result = spawnSync(process.execPath, [join(temporary, 'package-manager.mjs'), '--job', path], {
100
+ cwd: temporary, stdio: 'inherit', shell: false, env: { ...process.env, ...storage },
101
+ });
102
+ return result.status ?? (result.signal === 'SIGINT' ? 130 : 2);
103
+ } finally { await rm(temporary, { recursive: true, force: true }); }
104
+ }
105
+
106
+ if (process.argv[1] && process.argv[2] === '--job' && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
107
+ try { await operate(JSON.parse(readFileSync(process.argv[3], 'utf8'))); }
108
+ catch (error) { console.error(error.message); process.exitCode = 2; }
109
+ }
package/npm/runtime.mjs CHANGED
@@ -2,10 +2,11 @@ import { spawnSync } from 'node:child_process';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
4
4
  import { chmod, mkdir, mkdtemp, rename, rm, rmdir, writeFile } from 'node:fs/promises';
5
- import { homedir, hostname } from 'node:os';
5
+ import { homedir } from 'node:os';
6
6
  import { basename, dirname, join, resolve } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { download } from './download.mjs';
9
+ import { acquireLock } from './lock.mjs';
9
10
 
10
11
  const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
11
12
  const pins = JSON.parse(readFileSync(join(packageRoot, 'npm/uv-artifacts.json'), 'utf8'));
@@ -109,40 +110,7 @@ async function uvExecutable() {
109
110
  return cached;
110
111
  }
111
112
 
112
- export async function acquireSetupLock() {
113
- const lock = join(dataRoot(), 'setup.lock');
114
- const owner = { pid: process.pid, host: hostname(), nonce: randomUUID() };
115
- const guard = join(dataRoot(), 'setup.claim');
116
- try { await mkdir(guard, { mode: 0o700 }); }
117
- catch { throw new Error('Another setup is claiming the lock. Retry; an interrupted setup.claim needs local review.'); }
118
- try {
119
- try { await mkdir(lock, { mode: 0o700 }); }
120
- catch (error) {
121
- if (error.code !== 'EEXIST') throw error;
122
- let previous;
123
- try { previous = JSON.parse(readFileSync(join(lock, 'owner.json'), 'utf8')); }
124
- catch { throw new Error('Setup lock has no readable owner. Confirm no setup is running before removing setup.lock.'); }
125
- if (!Number.isSafeInteger(previous.pid) || previous.pid <= 0 || previous.host !== hostname()) {
126
- throw new Error('Setup lock belongs to an unknown owner; preserve it for local review.');
127
- }
128
- let alive = true;
129
- try { process.kill(previous.pid, 0); }
130
- catch (failure) { if (failure.code === 'ESRCH') alive = false; }
131
- if (alive) throw new Error('Another setup is running; retry when it finishes.');
132
- const abandoned = `${lock}.${randomUUID()}.abandoned`;
133
- await rename(lock, abandoned);
134
- await rm(abandoned, { recursive: true, force: true });
135
- try { await mkdir(lock, { mode: 0o700 }); }
136
- catch { throw new Error('Another setup acquired the lock; retry when it finishes.'); }
137
- }
138
- await atomicWrite(join(lock, 'owner.json'), JSON.stringify(owner));
139
- } finally { await rmdir(guard); }
140
- return async () => {
141
- const current = JSON.parse(readFileSync(join(lock, 'owner.json'), 'utf8'));
142
- if (current.nonce !== owner.nonce) throw new Error('Setup lock ownership changed; it was preserved.');
143
- await rm(lock, { recursive: true, force: true });
144
- };
145
- }
113
+ export function acquireSetupLock() { return acquireLock(dataRoot()); }
146
114
 
147
115
  export async function ensureRuntime() {
148
116
  const backend = join(packageRoot, 'npm/backend');
package/npm/tum-mcp.mjs CHANGED
@@ -1,33 +1,58 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import { readFileSync } from 'node:fs';
4
- import { acquireSetupLock, currentPython, ensureRuntime, removeRuntimeFiles } from './runtime.mjs';
4
+ import { parseArgs } from 'node:util';
5
+ import { acquireSetupLock, currentPython, dataRoot, ensureRuntime, removeRuntimeFiles } from './runtime.mjs';
6
+ import { managePackage } from './package-manager.mjs';
5
7
 
6
- const args = process.argv.slice(2);
8
+ let args = process.argv.slice(2);
7
9
  const version = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
8
10
  try {
9
11
  if (args.length === 0 || (args.length === 1 && ['--help', '-h'].includes(args[0]))) {
10
12
  console.log(`TUM MCP ${version}\nCommands: setup [--client codex|claude|cursor|antigravity|generic], connect, scope, discover, status, doctor [--json] [--live], serve, skills, study, logout, uninstall, update\nRun connect without arguments to select services and an installed Chromium browser. Run setup explicitly to provision the managed Python backend.`);
11
13
  } else if (['--version', 'version'].includes(args[0])) {
12
14
  console.log(version);
13
- } else if (['setup', 'update'].includes(args[0]) && args.length === 2 && ['--help', '-h'].includes(args[1])) {
14
- console.log(args[0] === 'update'
15
- ? 'Usage: tum-mcp update\nRefresh previously configured clients for the installed npm version.\nDownload a newer version first: npm install --global @jamestesting888/tum-mcp@latest'
16
- : 'Usage: tum-mcp setup [--client codex|claude|cursor|antigravity|generic] [--host local|windows]\nProvision the managed runtime and register explicitly selected clients.');
15
+ } else if (['setup', 'update', 'uninstall'].includes(args[0]) && args.some(arg => ['--help', '-h'].includes(arg))) {
16
+ const help = {
17
+ update: 'Usage: tum-mcp update [--local]\nInstall the latest npm release and refresh configured clients.\n--local refreshes this installed version without contacting npm.',
18
+ setup: 'Usage: tum-mcp setup [--client codex|claude|cursor|antigravity|generic] [--host local|windows]\nProvision the managed runtime and register explicitly selected clients.',
19
+ uninstall: 'Usage: tum-mcp uninstall [--all] [--purge-runtime] [--forget-credentials]\nRemove managed client setup and this npm installation. Saved data is retained by default.\n--client NAME removes one client only. --keep-package keeps npm installed.',
20
+ };
21
+ console.log(help[args[0]]);
17
22
  } else {
18
- if (args[0] === 'update' && args.length !== 1) throw new Error('Usage: tum-mcp update (no arguments). Use --help for the update workflow.');
19
- const setup = ['setup', 'update'].includes(args[0]);
20
- const python = setup ? await ensureRuntime() : currentPython();
21
- if (!python) throw new Error('SETUP_REQUIRED: Run tum-mcp setup first.');
22
- const forwarded = args;
23
- const env = { ...process.env, TUM_MCP_NPM_LAUNCHER: '1' }; delete env.PYTHONPATH; delete env.PYTHONHOME;
24
- const purge = args[0] === 'uninstall' && args.includes('--purge-runtime');
25
- const release = purge ? await acquireSetupLock() : null;
26
- try {
27
- const child = spawnSync(python, ['-I', '-m', 'tum_mcp', ...forwarded], { env, stdio: 'inherit', shell: false });
28
- if (child.error) throw new Error('Managed backend could not start; run setup again.');
29
- process.exitCode = child.status ?? (child.signal === 'SIGINT' ? 130 : 1);
30
- if (purge && child.status === 0) await removeRuntimeFiles();
31
- } finally { if (release) await release(); }
23
+ let packageAction = args[0] === 'update';
24
+ if (args[0] === 'update') {
25
+ if (args.length > 2 || (args.length === 2 && args[1] !== '--local')) throw new Error('Usage: tum-mcp update [--local]. Use --help for details.');
26
+ if (args[1] === '--local') { packageAction = false; args = ['update']; }
27
+ }
28
+ if (args[0] === 'uninstall') {
29
+ const { values } = parseArgs({ args: args.slice(1), options: {
30
+ all: { type: 'boolean' }, client: { type: 'string' }, host: { type: 'string' },
31
+ 'keep-package': { type: 'boolean' }, 'purge-runtime': { type: 'boolean' }, 'forget-credentials': { type: 'boolean' },
32
+ } });
33
+ if (values.host && !['local', 'windows'].includes(values.host)) throw new Error('Choose --host local or windows.');
34
+ if (values.client !== undefined && !['codex', 'claude', 'cursor', 'antigravity', 'generic'].includes(values.client)) throw new Error('Choose a supported --client name.');
35
+ if (values.client && (values.all || values['purge-runtime'] || values['forget-credentials'])) throw new Error('Client-only removal cannot use --all or data/runtime cleanup options.');
36
+ if (!values.client && !values.all) args.push('--all');
37
+ packageAction = !values.client && !values['keep-package'];
38
+ args = args.filter(arg => arg !== '--keep-package');
39
+ }
40
+ if (packageAction) {
41
+ process.exitCode = await managePackage(args[0], args.slice(1), dataRoot());
42
+ } else {
43
+ let python = ['setup', 'update'].includes(args[0]) ? await ensureRuntime() : currentPython();
44
+ if (args[0] === 'uninstall' && !python) python = await ensureRuntime();
45
+ if (!python) throw new Error('SETUP_REQUIRED: Run tum-mcp setup first.');
46
+ const forwarded = args;
47
+ const env = { ...process.env, TUM_MCP_NPM_LAUNCHER: '1' }; delete env.PYTHONPATH; delete env.PYTHONHOME;
48
+ const purge = args[0] === 'uninstall' && args.includes('--purge-runtime');
49
+ const release = purge ? await acquireSetupLock() : null;
50
+ try {
51
+ const child = spawnSync(python, ['-I', '-m', 'tum_mcp', ...forwarded], { env, stdio: 'inherit', shell: false });
52
+ if (child.error) throw new Error('Managed backend could not start; run setup again.');
53
+ process.exitCode = child.status ?? (child.signal === 'SIGINT' ? 130 : 1);
54
+ if (purge && child.status === 0) await removeRuntimeFiles();
55
+ } finally { if (release) await release(); }
56
+ }
32
57
  }
33
58
  } catch (error) { console.error(error.message); process.exitCode = 2; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jamestesting888/tum-mcp",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Read-only TUM Moodle, Artemis and Zulip integration with source-backed study workflows",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,6 +14,9 @@
14
14
  "npm/tum-mcp.mjs",
15
15
  "npm/runtime.mjs",
16
16
  "npm/download.mjs",
17
+ "npm/lock.mjs",
18
+ "npm/npm-command.mjs",
19
+ "npm/package-manager.mjs",
17
20
  "npm/uv-artifacts.json",
18
21
  "npm/backend/*.whl",
19
22
  "npm/backend/requirements.txt",
@@ -31,7 +34,8 @@
31
34
  "prepack": "node npm/build.mjs",
32
35
  "test": "node --test npm/test/*.test.mjs",
33
36
  "test:consumer": "node npm/smoke-consumer.mjs",
34
- "test:upgrade": "node npm/upgrade-consumer.mjs"
37
+ "test:upgrade": "node npm/upgrade-consumer.mjs",
38
+ "test:package-lifecycle": "node npm/package-consumer.mjs"
35
39
  },
36
40
  "publishConfig": {
37
41
  "access": "public"