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.
- package/bin/engineering-memory.mjs +0 -2
- package/install/installer.mjs +24 -11
- package/install/mcp-registration.mjs +25 -2
- package/lib/stage-runtime.mjs +0 -6
- package/package.json +1 -1
- package/runtime/dist/src/runtime/bridge-service.js +0 -6
- package/runtime/dist/src/runtime/repository-decision-store.js +0 -3
- package/runtime/dist/src/runtime/update-choice-store.js +0 -2
- package/skill/references/questionnaires.md +6 -0
|
@@ -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;
|
package/install/installer.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
467
|
-
managed.fingerprint ===
|
|
489
|
+
managed.clientVersion === undefined &&
|
|
490
|
+
managed.fingerprint === fingerprintBeforeClientVersion(clientName, managed)
|
|
468
491
|
);
|
|
469
492
|
}
|
|
470
493
|
|
package/lib/stage-runtime.mjs
CHANGED
|
@@ -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.
|
|
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.
|