@waishnav/devspace 1.0.0 → 1.0.2

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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +104 -36
  3. package/dist/cli.js +5 -2
  4. package/dist/config.js +16 -7
  5. package/dist/db/client.js +10 -3
  6. package/dist/db/migrations.js +123 -0
  7. package/dist/db/schema.js +24 -1
  8. package/dist/oauth-provider.js +35 -64
  9. package/dist/oauth-store.js +138 -0
  10. package/dist/review-checkpoints.js +4 -4
  11. package/dist/server.js +38 -19
  12. package/dist/ui/.vite/manifest.json +281 -281
  13. package/dist/ui/assets/{angular-html-Di31D1W0.js → angular-html-Dj4LysSE.js} +1 -1
  14. package/dist/ui/assets/{angular-ts-BKuSFrBS.js → angular-ts-CxuC_FFh.js} +1 -1
  15. package/dist/ui/assets/{apl-DgqYbYoh.js → apl-RwuvNFFo.js} +1 -1
  16. package/dist/ui/assets/{astro-B19gi6Os.js → astro-ncoaUbsO.js} +1 -1
  17. package/dist/ui/assets/{blade-BKHjA4oS.js → blade-Dw8-Lzux.js} +1 -1
  18. package/dist/ui/assets/{c-FjyGimBn.js → c-Dpbbo_f2.js} +1 -1
  19. package/dist/ui/assets/{cobol-DlmtgjjX.js → cobol-4ddX3THy.js} +1 -1
  20. package/dist/ui/assets/{coffee-BlTKLwcf.js → coffee-Bn-a5KqK.js} +1 -1
  21. package/dist/ui/assets/{cpp-DtVUwoaa.js → cpp-DoRrlM_T.js} +1 -1
  22. package/dist/ui/assets/{crystal-C0TLGTHr.js → crystal-DiZfCxn-.js} +1 -1
  23. package/dist/ui/assets/{css-FssmIjiX.js → css-DiQoSjDH.js} +1 -1
  24. package/dist/ui/assets/{edge-_m-12TBz.js → edge-wEhCUoEt.js} +1 -1
  25. package/dist/ui/assets/{elixir-RKJED3b7.js → elixir-C3obhm0H.js} +1 -1
  26. package/dist/ui/assets/{elm-CzMSD9Oq.js → elm-DyTBfh1L.js} +1 -1
  27. package/dist/ui/assets/{erb-Cqpp-3vK.js → erb-f4TnXRuY.js} +1 -1
  28. package/dist/ui/assets/{git-rebase-DLS2r2oj.js → git-rebase-vhZbWNfp.js} +1 -1
  29. package/dist/ui/assets/{glimmer-js-B1t-JGK1.js → glimmer-js-BG7puL6Y.js} +1 -1
  30. package/dist/ui/assets/{glimmer-ts-CazygCNf.js → glimmer-ts-CNCfLLL1.js} +1 -1
  31. package/dist/ui/assets/{glsl-gBg2vKZ-.js → glsl-DhT76AL5.js} +1 -1
  32. package/dist/ui/assets/{graphql-C8u6MNtl.js → graphql-CJX08w8y.js} +1 -1
  33. package/dist/ui/assets/{hack-CgKuqejW.js → hack-DY4NGCCL.js} +1 -1
  34. package/dist/ui/assets/{haml-I5WU4_Rg.js → haml-DwSkmVcG.js} +1 -1
  35. package/dist/ui/assets/{handlebars-DGf6O3Q6.js → handlebars-CQ_1dEmi.js} +1 -1
  36. package/dist/ui/assets/{heavy-payload-Bp6srDbj.js → heavy-payload-2rGBiitA.js} +1 -1
  37. package/dist/ui/assets/{html-BrIuweS5.js → html-B_e3Njif.js} +1 -1
  38. package/dist/ui/assets/{html-derivative-CflP_1S6.js → html-derivative-_nHZ_Bk-.js} +1 -1
  39. package/dist/ui/assets/{http-qgGkv9Ds.js → http-CfcvPIru.js} +1 -1
  40. package/dist/ui/assets/{hurl-CantyQ5z.js → hurl-9vWDQIMO.js} +1 -1
  41. package/dist/ui/assets/{java-DqStxnBY.js → java-C4S5r6L5.js} +1 -1
  42. package/dist/ui/assets/{javascript-CJ1ZUMNM.js → javascript-BRwqbMMC.js} +1 -1
  43. package/dist/ui/assets/{jinja-Ta-y-8tW.js → jinja-DHxzMMc5.js} +1 -1
  44. package/dist/ui/assets/{jison-DZStAtdf.js → jison-B1DqeWXb.js} +1 -1
  45. package/dist/ui/assets/{json-Bfn_pgvS.js → json-DcFTkEdd.js} +1 -1
  46. package/dist/ui/assets/{jsx-CFi5D1JI.js → jsx-DgfmYa9f.js} +1 -1
  47. package/dist/ui/assets/{julia-CmuPb_FV.js → julia-3M8SJ8iF.js} +1 -1
  48. package/dist/ui/assets/{just-xoDaYyy4.js → just-UyH7_DNz.js} +1 -1
  49. package/dist/ui/assets/{latex-C7jbQT4s.js → latex-wUbha6EX.js} +1 -1
  50. package/dist/ui/assets/{liquid-DyVckOju.js → liquid-DqNTB9kN.js} +1 -1
  51. package/dist/ui/assets/{lua-CQGEo-kr.js → lua-DrEQ1QTW.js} +1 -1
  52. package/dist/ui/assets/{marko-CF3FIlcL.js → marko-B3GJw9Md.js} +1 -1
  53. package/dist/ui/assets/{mdc-DJfBh8KU.js → mdc-BnThk82m.js} +1 -1
  54. package/dist/ui/assets/{nginx-CmN5T-d3.js → nginx-B5IeSsOX.js} +1 -1
  55. package/dist/ui/assets/{nim-a_705mqZ.js → nim-BJraatnS.js} +1 -1
  56. package/dist/ui/assets/{perl-BFxFi55O.js → perl-BAhrR4gt.js} +1 -1
  57. package/dist/ui/assets/{php-C9Uf0Vo_.js → php-BKRCk_wX.js} +1 -1
  58. package/dist/ui/assets/{pug-DQIrtlYh.js → pug-B8gmSV7z.js} +1 -1
  59. package/dist/ui/assets/{qml-CpI_Asuw.js → qml-DKrxIjHR.js} +1 -1
  60. package/dist/ui/assets/{r-hYZdyIMH.js → r-YASBZvTH.js} +1 -1
  61. package/dist/ui/assets/{razor-CbXx350T.js → razor-P-XFZqh6.js} +1 -1
  62. package/dist/ui/assets/{regexp-BdfO5R2u.js → regexp-CCpiH6nO.js} +1 -1
  63. package/dist/ui/assets/{review-payload-CC-3-Lb9.js → review-payload-BLYwg6t8.js} +1 -1
  64. package/dist/ui/assets/{rst-CP6mAm3Y.js → rst-CFAzckbn.js} +1 -1
  65. package/dist/ui/assets/{ruby-8tjXzrRr.js → ruby-Dyp_Rx-S.js} +1 -1
  66. package/dist/ui/assets/{sas-rqWHY37U.js → sas-YkWZIXxT.js} +1 -1
  67. package/dist/ui/assets/{scss-aGYgGui9.js → scss-Cf89idGl.js} +1 -1
  68. package/dist/ui/assets/{shellscript-CYr5JWBI.js → shellscript-BFMb52q8.js} +1 -1
  69. package/dist/ui/assets/{shellsession-CO3ljRR7.js → shellsession-DuthW-5v.js} +1 -1
  70. package/dist/ui/assets/{soy-C4_rLmHV.js → soy-BohVVlJU.js} +1 -1
  71. package/dist/ui/assets/{sql-2jcbKHJ9.js → sql-CnK9R2Gt.js} +1 -1
  72. package/dist/ui/assets/{stata-Dbr0lM_v.js → stata-D-sV4Vbr.js} +1 -1
  73. package/dist/ui/assets/{surrealql-Cp0eMxMo.js → surrealql-ChAy0gHv.js} +1 -1
  74. package/dist/ui/assets/{svelte-BfsT9lcS.js → svelte-3HBVqG-H.js} +1 -1
  75. package/dist/ui/assets/{templ-BBOwNqeW.js → templ-BixlV4Cp.js} +1 -1
  76. package/dist/ui/assets/{tex-CPmn_RsX.js → tex-DWoJKHvD.js} +1 -1
  77. package/dist/ui/assets/{ts-tags-DMILlUz_.js → ts-tags-DLA8eH13.js} +1 -1
  78. package/dist/ui/assets/{tsx-CUP2Lfiz.js → tsx-BYWa5JUz.js} +1 -1
  79. package/dist/ui/assets/{twig-DEZ_7SDX.js → twig-CnY65vAo.js} +1 -1
  80. package/dist/ui/assets/{typescript-DhaMQEhQ.js → typescript-ByInUsro.js} +1 -1
  81. package/dist/ui/assets/{useFileDiffInstance-Bo6irrE6.js → useFileDiffInstance-Dg0pWcJV.js} +3 -3
  82. package/dist/ui/assets/{vue-CAaS6wtt.js → vue-Qngt2rkz.js} +1 -1
  83. package/dist/ui/assets/{vue-html-D3Etensv.js → vue-html-DDue9upr.js} +1 -1
  84. package/dist/ui/assets/{vue-vine-DNRKj0Lh.js → vue-vine-DBcAKsA6.js} +1 -1
  85. package/dist/ui/assets/workspace-app-DGD9R89D.css +1 -0
  86. package/dist/ui/assets/{workspace-app-BLeU1qC_.js → workspace-app-DuzpU4Hf.js} +6 -6
  87. package/dist/ui/assets/{xml-M-X6I2aP.js → xml-CRLR5jiL.js} +1 -1
  88. package/dist/ui/assets/{xsl-CR0ZlrL7.js → xsl-CmcwyXbm.js} +1 -1
  89. package/dist/ui/assets/{yaml-pI4xDDgj.js → yaml-Dpe6Eg-k.js} +1 -1
  90. package/dist/ui/workspace-app.html +2 -2
  91. package/dist/workspace-store.js +0 -50
  92. package/docs/chatgpt-coding-workflow.md +22 -22
  93. package/docs/configuration.md +13 -10
  94. package/docs/gotchas.md +5 -4
  95. package/package.json +3 -5
  96. package/scripts/dev-server.mjs +120 -0
  97. package/dist/ui/assets/workspace-app-8--oTbCd.css +0 -1
  98. package/scripts/build-release.mjs +0 -89
  99. package/scripts/ensure-native-deps.mjs +0 -29
@@ -0,0 +1,138 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { InvalidRequestError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
3
+ import { openDatabase } from "./db/client.js";
4
+ function redirectHostAllowed(redirectUri, allowedHosts) {
5
+ let parsed;
6
+ try {
7
+ parsed = new URL(redirectUri);
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ if (["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname))
13
+ return true;
14
+ return allowedHosts.includes(parsed.hostname);
15
+ }
16
+ export class SqliteOAuthStore {
17
+ database;
18
+ constructor(stateDir) {
19
+ this.database = openDatabase(stateDir);
20
+ this.deleteExpiredTokens(Math.floor(Date.now() / 1000));
21
+ }
22
+ getClient(clientId) {
23
+ const row = this.database.sqlite
24
+ .prepare("select client_json from oauth_clients where client_id = ?")
25
+ .get(clientId);
26
+ return row ? JSON.parse(row.client_json) : undefined;
27
+ }
28
+ registerClient(client, allowedRedirectHosts) {
29
+ if (!client.redirect_uris.every((uri) => redirectHostAllowed(String(uri), allowedRedirectHosts))) {
30
+ throw new InvalidRequestError("Client redirect_uri is not allowed for this DevSpace server");
31
+ }
32
+ const now = Math.floor(Date.now() / 1000);
33
+ const registered = {
34
+ ...client,
35
+ client_id: `devspace-${randomUUID()}`,
36
+ client_id_issued_at: now,
37
+ token_endpoint_auth_method: client.token_endpoint_auth_method ?? "none",
38
+ grant_types: client.grant_types ?? ["authorization_code", "refresh_token"],
39
+ response_types: client.response_types ?? ["code"],
40
+ };
41
+ this.database.sqlite
42
+ .prepare("insert into oauth_clients (client_id, client_json, issued_at) values (?, ?, ?)")
43
+ .run(registered.client_id, JSON.stringify(registered), now);
44
+ return registered;
45
+ }
46
+ saveAccessToken(tokenHash, record) {
47
+ this.database.sqlite
48
+ .prepare(`insert into oauth_access_tokens (token_hash, client_id, scopes_json, expires_at, resource)
49
+ values (?, ?, ?, ?, ?)
50
+ on conflict(token_hash) do update set
51
+ client_id = excluded.client_id,
52
+ scopes_json = excluded.scopes_json,
53
+ expires_at = excluded.expires_at,
54
+ resource = excluded.resource`)
55
+ .run(tokenHash, record.clientId, JSON.stringify(record.scopes), record.expiresAt, record.resource ?? null);
56
+ }
57
+ getAccessToken(tokenHash) {
58
+ const row = this.database.sqlite
59
+ .prepare("select client_id, scopes_json, expires_at, resource from oauth_access_tokens where token_hash = ?")
60
+ .get(tokenHash);
61
+ return row ? rowToAccessTokenRecord(row) : undefined;
62
+ }
63
+ deleteAccessToken(tokenHash) {
64
+ this.database.sqlite.prepare("delete from oauth_access_tokens where token_hash = ?").run(tokenHash);
65
+ }
66
+ saveRefreshToken(tokenHash, record) {
67
+ this.database.sqlite
68
+ .prepare(`insert into oauth_refresh_tokens (token_hash, client_id, scopes_json, expires_at, resource)
69
+ values (?, ?, ?, ?, ?)
70
+ on conflict(token_hash) do update set
71
+ client_id = excluded.client_id,
72
+ scopes_json = excluded.scopes_json,
73
+ expires_at = excluded.expires_at,
74
+ resource = excluded.resource`)
75
+ .run(tokenHash, record.clientId, JSON.stringify(record.scopes), record.expiresAt, record.resource ?? null);
76
+ }
77
+ saveTokenPair(pair, consumedRefreshTokenHash) {
78
+ const save = this.database.sqlite.transaction(() => {
79
+ if (consumedRefreshTokenHash) {
80
+ const result = this.database.sqlite
81
+ .prepare("delete from oauth_refresh_tokens where token_hash = ?")
82
+ .run(consumedRefreshTokenHash);
83
+ if (result.changes !== 1)
84
+ return false;
85
+ }
86
+ this.saveAccessToken(pair.accessTokenHash, pair.accessToken);
87
+ this.saveRefreshToken(pair.refreshTokenHash, pair.refreshToken);
88
+ return true;
89
+ });
90
+ return save.immediate();
91
+ }
92
+ getRefreshToken(tokenHash) {
93
+ const row = this.database.sqlite
94
+ .prepare("select client_id, scopes_json, expires_at, resource from oauth_refresh_tokens where token_hash = ?")
95
+ .get(tokenHash);
96
+ return row ? rowToRefreshTokenRecord(row) : undefined;
97
+ }
98
+ deleteRefreshToken(tokenHash) {
99
+ this.database.sqlite.prepare("delete from oauth_refresh_tokens where token_hash = ?").run(tokenHash);
100
+ }
101
+ close() {
102
+ this.database.close();
103
+ }
104
+ deleteExpiredTokens(nowSeconds) {
105
+ this.database.sqlite.prepare("delete from oauth_access_tokens where expires_at < ?").run(nowSeconds);
106
+ this.database.sqlite.prepare("delete from oauth_refresh_tokens where expires_at < ?").run(nowSeconds);
107
+ }
108
+ }
109
+ export class SqliteOAuthClientsStore {
110
+ store;
111
+ allowedRedirectHosts;
112
+ constructor(store, allowedRedirectHosts) {
113
+ this.store = store;
114
+ this.allowedRedirectHosts = allowedRedirectHosts;
115
+ }
116
+ getClient(clientId) {
117
+ return this.store.getClient(clientId);
118
+ }
119
+ registerClient(client) {
120
+ return this.store.registerClient(client, this.allowedRedirectHosts);
121
+ }
122
+ }
123
+ function rowToAccessTokenRecord(row) {
124
+ return {
125
+ clientId: row.client_id,
126
+ scopes: JSON.parse(row.scopes_json),
127
+ expiresAt: row.expires_at,
128
+ resource: row.resource ?? undefined,
129
+ };
130
+ }
131
+ function rowToRefreshTokenRecord(row) {
132
+ return {
133
+ clientId: row.client_id,
134
+ scopes: JSON.parse(row.scopes_json),
135
+ expiresAt: row.expires_at,
136
+ resource: row.resource ?? undefined,
137
+ };
138
+ }
@@ -13,7 +13,7 @@ export function createReviewCheckpointManager() {
13
13
  try {
14
14
  const eligibility = await getGitEligibility(root);
15
15
  if (!eligibility.ok || !eligibility.gitRoot) {
16
- state.diagnostic = eligibility.message ?? "review_changes requires a Git workspace in this version.";
16
+ state.diagnostic = eligibility.message ?? "show_changes requires a Git workspace in this version.";
17
17
  return;
18
18
  }
19
19
  state.gitRoot = eligibility.gitRoot;
@@ -25,14 +25,14 @@ export function createReviewCheckpointManager() {
25
25
  state.diagnostic = error instanceof Error ? error.message : String(error);
26
26
  }
27
27
  },
28
- async reviewChanges({ workspaceId, root, since = "last_review", markReviewed = true }) {
28
+ async reviewChanges({ workspaceId, root, since = "last_shown", markReviewed = true }) {
29
29
  let state = states.get(workspaceId);
30
30
  if (!state) {
31
31
  await this.initializeWorkspace({ workspaceId, root });
32
32
  state = states.get(workspaceId);
33
33
  }
34
34
  if (!state?.gitRoot) {
35
- throw new Error(state?.diagnostic ?? "review_changes requires a Git workspace in this version.");
35
+ throw new Error(state?.diagnostic ?? "show_changes requires a Git workspace in this version.");
36
36
  }
37
37
  const baselineRef = since === "workspace_open" ? state.openRef : state.baselineRef;
38
38
  const baseline = (await git(state.gitRoot, ["rev-parse", "--verify", `${baselineRef}^{commit}`])).stdout.trim();
@@ -50,7 +50,7 @@ export function createReviewCheckpointManager() {
50
50
  }
51
51
  return {
52
52
  result: summary.files === 0
53
- ? `No changes since ${since === "workspace_open" ? "workspace open" : "last review"}.`
53
+ ? `No changes since ${since === "workspace_open" ? "workspace open" : "last shown changes"}.`
54
54
  : `Changed ${summary.files} ${summary.files === 1 ? "file" : "files"} (+${summary.additions} -${summary.removals}).`,
55
55
  summary,
56
56
  files,
package/dist/server.js CHANGED
@@ -45,7 +45,7 @@ function shouldAttachWidget(mode, kind) {
45
45
  case "off":
46
46
  return false;
47
47
  case "changes":
48
- return kind === "workspace" || kind === "review_changes";
48
+ return kind === "workspace" || kind === "show_changes";
49
49
  case "full":
50
50
  return true;
51
51
  }
@@ -93,10 +93,10 @@ function serverInstructions(config, toolNames) {
93
93
  ? `When ${toolNames.openWorkspace} returns available skills and a task matches a skill, use ${toolNames.read} to read that skill's path before proceeding. Skill paths may be outside the workspace, but ${toolNames.read} only permits advertised SKILL.md files and files under already-loaded skill directories. `
94
94
  : "";
95
95
  const agentsMd = `Follow instructions returned by ${toolNames.openWorkspace}. Before working under a path listed in availableAgentsFiles, use ${toolNames.read} to inspect that instruction file and follow it. `;
96
- const review = config.widgets === "changes"
97
- ? " After completing a coherent set of file modifications, call review_changes once to show the user an aggregate diff review."
96
+ const showChanges = config.widgets === "changes"
97
+ ? " After creating, editing, or overwriting files, call show_changes once after the related file changes are complete so the user can see the aggregate diff."
98
98
  : "";
99
- return `Use DevSpace as a local coding workspace. Call ${toolNames.openWorkspace} once per project folder or worktree to obtain a workspaceId. Reuse that same workspaceId for all later file, search, edit, write, review, and shell tools in that folder; do not call ${toolNames.openWorkspace} again unless switching folders/worktrees, changing checkout/worktree mode, the workspaceId is rejected as unknown, or the user explicitly asks to reopen. ${agentsMd}${skills}${inspection}Prefer ${toolNames.edit} for targeted modifications, ${toolNames.write} only for new files or complete rewrites, and ${toolNames.shell} for tests, builds, git inspection, package scripts, and commands that are better executed by the shell. Do not create or modify files with ${toolNames.shell}; avoid shell redirection, heredocs, tee, sed -i, perl -i, node/python/ruby scripts, or any command whose purpose is to write project files.${review}`;
99
+ return `Use DevSpace as a local coding workspace. Call ${toolNames.openWorkspace} once per project folder or worktree to obtain a workspaceId. Reuse that same workspaceId for all later file, search, edit, write, show-changes, and shell tools in that folder; do not call ${toolNames.openWorkspace} again unless switching folders/worktrees, changing checkout/worktree mode, the workspaceId is rejected as unknown, or the user explicitly asks to reopen. ${agentsMd}${skills}${inspection}Prefer ${toolNames.edit} for targeted modifications, ${toolNames.write} only for new files or complete rewrites, and ${toolNames.shell} for tests, builds, git inspection, package scripts, and commands that are better executed by the shell. Do not create or modify files with ${toolNames.shell}; avoid shell redirection, heredocs, tee, sed -i, perl -i, node/python/ruby scripts, or any command whose purpose is to write project files.${showChanges}`;
100
100
  }
101
101
  function resultOutputSchema(extra = {}) {
102
102
  return {
@@ -328,7 +328,7 @@ function createMcpServer(config, workspaces, reviewCheckpoints) {
328
328
  });
329
329
  registerAppTool(server, "open_workspace", {
330
330
  title: "Open workspace",
331
- description: "Open a local project directory as a coding workspace. Call this once per project folder or worktree before reading, editing, searching, writing, reviewing, or running commands. Reuse the returned workspaceId for later calls in the same folder; do not call open_workspace again unless switching folders/worktrees, changing checkout/worktree mode, the workspaceId is rejected as unknown, or the user explicitly asks to reopen. By default this opens the actual checkout; set mode=\"worktree\" when the user asks for an isolated or parallel coding session. Returns a workspaceId, loaded root project instructions, and nested instruction file paths the model should read before working in those directories.",
331
+ description: "Open a local project directory as a coding workspace. Call this once per project folder or worktree before reading, editing, searching, writing, showing changes, or running commands. Reuse the returned workspaceId for later calls in the same folder; do not call open_workspace again unless switching folders/worktrees, changing checkout/worktree mode, the workspaceId is rejected as unknown, or the user explicitly asks to reopen. By default this opens the actual checkout; set mode=\"worktree\" when the user asks for an isolated or parallel coding session. Returns a workspaceId, loaded root project instructions, and nested instruction file paths the model should read before working in those directories.",
332
332
  inputSchema: {
333
333
  path: z
334
334
  .string()
@@ -669,24 +669,24 @@ function createMcpServer(config, workspaces, reviewCheckpoints) {
669
669
  };
670
670
  });
671
671
  if (config.widgets === "changes") {
672
- registerAppTool(server, "review_changes", {
673
- title: "Review changes",
674
- description: "Review aggregate file changes in an open workspace since the last review checkpoint or since the workspace was opened. Use this after a coherent set of file modifications to show a single diff review card.",
672
+ registerAppTool(server, "show_changes", {
673
+ title: "Show changes",
674
+ description: "Show aggregate file changes in an open workspace since the last shown checkpoint or since the workspace was opened. After you create, edit, or overwrite files, call this once when the related file changes are complete so the user can inspect the combined diff.",
675
675
  inputSchema: {
676
676
  workspaceId: z
677
677
  .string()
678
678
  .describe("Workspace identifier returned by open_workspace."),
679
679
  since: z
680
- .enum(["last_review", "workspace_open"])
680
+ .enum(["last_shown", "workspace_open"])
681
681
  .optional()
682
- .describe("Defaults to last_review. Use workspace_open to compare against the initial open_workspace checkpoint."),
682
+ .describe("Defaults to last_shown. Use workspace_open to compare against the initial open_workspace checkpoint."),
683
683
  markReviewed: z
684
684
  .boolean()
685
685
  .optional()
686
- .describe("Defaults to true. When true, advances the last_review checkpoint to the current workspace state."),
686
+ .describe("Defaults to true. When true, advances the last shown checkpoint to the current workspace state."),
687
687
  },
688
688
  outputSchema: resultOutputSchema(),
689
- ...toolWidgetDescriptorMeta(config, "review_changes"),
689
+ ...toolWidgetDescriptorMeta(config, "show_changes"),
690
690
  annotations: { readOnlyHint: true },
691
691
  }, async ({ workspaceId, since, markReviewed }) => {
692
692
  const startedAt = performance.now();
@@ -694,12 +694,12 @@ function createMcpServer(config, workspaces, reviewCheckpoints) {
694
694
  const review = await reviewCheckpoints.reviewChanges({
695
695
  workspaceId,
696
696
  root: workspace.root,
697
- since: since ?? "last_review",
697
+ since: since ?? "last_shown",
698
698
  markReviewed: markReviewed ?? true,
699
699
  });
700
700
  const content = [textBlock(review.result)];
701
701
  logToolCall(config, {
702
- tool: "review_changes",
702
+ tool: "show_changes",
703
703
  workspaceId,
704
704
  success: true,
705
705
  durationMs: Math.round(performance.now() - startedAt),
@@ -707,7 +707,7 @@ function createMcpServer(config, workspaces, reviewCheckpoints) {
707
707
  return {
708
708
  content,
709
709
  _meta: {
710
- tool: "review_changes",
710
+ tool: "show_changes",
711
711
  card: {
712
712
  workspaceId,
713
713
  summary: review.summary,
@@ -989,7 +989,7 @@ export function createServer(config = loadConfig()) {
989
989
  const transports = new Map();
990
990
  const mcpUrl = new URL("/mcp", config.publicBaseUrl);
991
991
  const resourceServerUrl = resourceUrlFromServerUrl(mcpUrl);
992
- const oauthProvider = new SingleUserOAuthProvider(config.oauth, mcpUrl);
992
+ const oauthProvider = new SingleUserOAuthProvider(config.oauth, mcpUrl, config.stateDir);
993
993
  const bearerAuth = requireBearerAuth({
994
994
  verifier: oauthProvider,
995
995
  requiredScopes: [config.oauth.scopes[0] ?? "devspace"],
@@ -1125,7 +1125,18 @@ export function createServer(config = loadConfig()) {
1125
1125
  }
1126
1126
  }
1127
1127
  });
1128
- return { app, config };
1128
+ let closed = false;
1129
+ return {
1130
+ app,
1131
+ config,
1132
+ close: () => {
1133
+ if (closed)
1134
+ return;
1135
+ closed = true;
1136
+ oauthProvider.close();
1137
+ workspaceStore.close?.();
1138
+ },
1139
+ };
1129
1140
  }
1130
1141
  async function isMainModule() {
1131
1142
  if (!process.argv[1])
@@ -1135,8 +1146,8 @@ async function isMainModule() {
1135
1146
  return modulePath === entrypointPath;
1136
1147
  }
1137
1148
  if (await isMainModule()) {
1138
- const { app, config } = createServer();
1139
- app.listen(config.port, config.host, () => {
1149
+ const { app, config, close } = createServer();
1150
+ const httpServer = app.listen(config.port, config.host, () => {
1140
1151
  console.log(`devspace listening on http://${config.host}:${config.port}/mcp`);
1141
1152
  console.log(`allowed roots: ${config.allowedRoots.join(", ")}`);
1142
1153
  console.log("auth: oauth owner-token flow required");
@@ -1145,4 +1156,12 @@ if (await isMainModule()) {
1145
1156
  console.log(`asset logging: ${config.logging.assets ? "enabled" : "disabled"}`);
1146
1157
  console.log(`trust proxy: ${config.logging.trustProxy ? "enabled" : "disabled"}`);
1147
1158
  });
1159
+ const shutdown = () => {
1160
+ httpServer.close(() => {
1161
+ close();
1162
+ process.exit(0);
1163
+ });
1164
+ };
1165
+ process.once("SIGINT", shutdown);
1166
+ process.once("SIGTERM", shutdown);
1148
1167
  }