engineering-memory 0.2.1 → 0.2.3

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.
@@ -29,8 +29,6 @@ Choose the hook mode through the Codex or Claude native questionnaire before
29
29
  passing a repository. The installer never opens a questionnaire or an
30
30
  authentication web page.`;
31
31
 
32
- // The backend this package was published for. An installation only names one
33
- // when it points at a different, self-hosted backend.
34
32
  async function publishedApiUrl() {
35
33
  const manifest = await readJson(join(packageRoot, 'package.json'));
36
34
  const apiUrl = manifest?.engineeringMemory?.apiUrl;
@@ -107,8 +107,29 @@ export async function installEngineeringMemory(options = {}) {
107
107
  dispatcherSection: dispatcherSections.claude,
108
108
  },
109
109
  };
110
+ const mcpPlans = await planMcpRegistrations({
111
+ selectedClients,
112
+ bridgeEntry,
113
+ nodePath,
114
+ apiUrl,
115
+ clientVersion,
116
+ state: existingState,
117
+ commandRunner,
118
+ clientsWereRequested: options.selectedClients !== undefined,
119
+ });
120
+ const absentClients = mcpPlans
121
+ .filter((plan) => plan.action === 'absent')
122
+ .map((plan) => plan.clientName);
123
+ const installedClients = selectedClients.filter(
124
+ (clientName) => !absentClients.includes(clientName),
125
+ );
126
+ if (installedClients.length === 0) {
127
+ throw new Error(
128
+ `None of the supported agent CLIs are on PATH: ${selectedClients.join(', ')}. Install one, then rerun the installer.`,
129
+ );
130
+ }
110
131
  const clientPlans = await Promise.all(
111
- selectedClients.map(async (clientName) => {
132
+ installedClients.map(async (clientName) => {
112
133
  const target = clientTargets[clientName];
113
134
  return {
114
135
  clientName,
@@ -120,15 +141,6 @@ export async function installEngineeringMemory(options = {}) {
120
141
  };
121
142
  }),
122
143
  );
123
- const mcpPlans = await planMcpRegistrations({
124
- selectedClients,
125
- bridgeEntry,
126
- nodePath,
127
- apiUrl,
128
- clientVersion,
129
- state: existingState,
130
- commandRunner,
131
- });
132
144
  const hookPlan = await planGitHook({
133
145
  repoRoot: options.repoRoot ? resolve(options.repoRoot) : null,
134
146
  mode: options.hookMode ?? 'cancel',
@@ -189,7 +201,8 @@ export async function installEngineeringMemory(options = {}) {
189
201
  statePath,
190
202
  skillPaths: clientPlans.map((plan) => plan.skillPath),
191
203
  dispatcherPaths: clientPlans.map((plan) => plan.dispatcherPath),
192
- selectedClients,
204
+ selectedClients: installedClients,
205
+ absentClients,
193
206
  apiUrl,
194
207
  clientVersion,
195
208
  runtimePath,
@@ -48,6 +48,7 @@ export async function planMcpRegistrations({
48
48
  clientVersion,
49
49
  state,
50
50
  commandRunner,
51
+ clientsWereRequested = true,
51
52
  }) {
52
53
  const plans = [];
53
54
  for (const clientName of selectedClients) {
@@ -55,6 +56,10 @@ export async function planMcpRegistrations({
55
56
  if (!client) throw new Error(`Unsupported client: ${clientName}`);
56
57
  const version = await commandRunner(client.executable, ['--version']);
57
58
  if (version.code !== 0) {
59
+ if (!clientsWereRequested && !state?.mcp?.[clientName]) {
60
+ plans.push({ clientName, client, action: 'absent' });
61
+ continue;
62
+ }
58
63
  throw new Error(
59
64
  `${clientName} CLI is unavailable. Install its native CLI, ensure it is on PATH, then rerun the installer. ${commandFailure(client.executable, version)}`,
60
65
  );
@@ -121,6 +126,7 @@ export async function planMcpRegistrations({
121
126
  export async function applyMcpRegistrations(plans, commandRunner, transaction) {
122
127
  const result = {};
123
128
  for (const plan of plans) {
129
+ if (plan.action === 'absent') continue;
124
130
  if (plan.action === 'add') {
125
131
  await addRegistration(plan, plan.registration, commandRunner);
126
132
  transaction.add(() => removeRegistration(plan, commandRunner));
@@ -211,6 +217,19 @@ function registrationFingerprint(clientName, registration) {
211
217
  .digest('hex');
212
218
  }
213
219
 
220
+ function fingerprintBeforeClientVersion(clientName, registration) {
221
+ return createHash('sha256')
222
+ .update(
223
+ JSON.stringify({
224
+ clientName,
225
+ nodePath: registration.nodePath,
226
+ bridgeEntry: registration.bridgeEntry,
227
+ apiUrl: registration.apiUrl ?? null,
228
+ }),
229
+ )
230
+ .digest('hex');
231
+ }
232
+
214
233
  function sameRegistration(left, right) {
215
234
  return (
216
235
  left?.nodePath === right.nodePath &&
@@ -462,9 +481,13 @@ function isOwnedRegistration(clientName, managed) {
462
481
  typeof managed.bridgeEntry === 'string'
463
482
  );
464
483
  }
484
+ if (typeof managed.apiUrl !== 'string') return false;
485
+ if (managed.fingerprint === registrationFingerprint(clientName, managed)) {
486
+ return true;
487
+ }
465
488
  return (
466
- typeof managed.apiUrl === 'string' &&
467
- managed.fingerprint === registrationFingerprint(clientName, managed)
489
+ managed.clientVersion === undefined &&
490
+ managed.fingerprint === fingerprintBeforeClientVersion(clientName, managed)
468
491
  );
469
492
  }
470
493
 
@@ -3,10 +3,6 @@ import { tmpdir } from 'node:os';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { readJson, resolveProductionPackagePaths } from '../install/files.mjs';
5
5
 
6
- // The published package cannot carry node_modules, because npm excludes it from
7
- // every tarball. The bridge runtime is assembled here from the dependencies npm
8
- // installed for this package, into the exact layout the installer verifies
9
- // against the bridge lockfile.
10
6
  export async function stageBridgeRuntime(packageRoot, resolver) {
11
7
  const source = join(packageRoot, 'runtime');
12
8
  const packageJson = await readJson(join(source, 'package.json'));
@@ -37,8 +33,6 @@ export async function stageBridgeRuntime(packageRoot, resolver) {
37
33
  return staged;
38
34
  }
39
35
 
40
- // A package is free to keep package.json out of its exports map, and several do,
41
- // so the directory is found on disk rather than through module resolution.
42
36
  async function locate(name, resolver) {
43
37
  for (const directory of resolver.resolve.paths(name) ?? []) {
44
38
  const candidate = join(directory, ...name.split('/'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engineering-memory",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Installs the Engineering Memory skill and its local MCP bridge. Sign in after installing; your organization and project are resolved from your account.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -1160,9 +1160,6 @@ export class BridgeService {
1160
1160
  });
1161
1161
  });
1162
1162
  }
1163
- // What the backend expects of this client, and whether the user has already
1164
- // been asked about it. A required update leaves no choice; an optional one is
1165
- // offered once per version and never raised again after a refusal.
1166
1163
  async clientUpdate(authenticated) {
1167
1164
  const installed = this.dependencies.clientVersion;
1168
1165
  if (!authenticated) {
@@ -1799,9 +1796,6 @@ export class BridgeService {
1799
1796
  if (pointer.verificationIntent || pointer.closeIntent) {
1800
1797
  throw new Error('A clean active task snapshot is required for checkpointing');
1801
1798
  }
1802
- // The slug names the task's journal directory, and a directory is the same
1803
- // one whatever case it is typed in. Rejecting KAN-1003 against kan-1003
1804
- // refuses a correct checkpoint over nothing.
1805
1799
  if (pointer.taskSlug.toLowerCase() !== input.taskSlug.toLowerCase()) {
1806
1800
  throw new Error(`Checkpoint task slug ${input.taskSlug} does not match the active task ${pointer.taskSlug}`);
1807
1801
  }
@@ -1,8 +1,5 @@
1
1
  import { join } from 'node:path';
2
2
  import { readJson, removeFile, writeJson } from '../utilities/files.js';
3
- // What a user decided about Engineering Memory in one repository, on one
4
- // machine. It never lives in the repository: a developer switching it off must
5
- // not switch it off for everyone who clones after them.
6
3
  export class RepositoryDecisionStore {
7
4
  root;
8
5
  queues = new Map();
@@ -1,7 +1,5 @@
1
1
  import { join } from 'node:path';
2
2
  import { readJson, writeJson } from '../utilities/files.js';
3
- // Which client version the user was offered and turned down. Asking again for
4
- // the same version is nagging; asking when a newer one arrives is news.
5
3
  export class UpdateChoiceStore {
6
4
  path;
7
5
  root;
@@ -68,6 +68,12 @@ Before proposing a flow record, ask the user what the design cannot answer: whic
68
68
 
69
69
  When the user names a different Figma file or link, treat it as a correction to the `figma_reference` record and propose the revision in the same reply, with the new file key and URL. Ask for approval there and then. Do not carry the new address only in the conversation: the next session reads the record, not the chat.
70
70
 
71
+ ## Running the Application
72
+
73
+ When the change is something a person sees or interacts with and this environment can run the application, ask before launching it rather than starting on your own. A run occupies the user's machine, can take minutes, and they may be using it. Say what you would exercise and what you are looking for, so the answer is about the work rather than about the tooling.
74
+
75
+ Ask again nothing when they decline. Verify what you can without it and state in the handoff that the flow was not exercised in a running application.
76
+
71
77
  ## Correction Scope
72
78
 
73
79
  At a natural checkpoint after recording and fixing a user correction, first state how the correction was classified and why, then offer the scopes that classification allows.