engineering-memory 0.1.0
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 +120 -0
- package/dispatcher/managed-section.mjs +59 -0
- package/dispatcher/sections.mjs +14 -0
- package/install/api-url.mjs +39 -0
- package/install/cli.mjs +93 -0
- package/install/commands.mjs +140 -0
- package/install/files.mjs +416 -0
- package/install/git-hook.mjs +270 -0
- package/install/installer.mjs +279 -0
- package/install/mcp-registration.mjs +457 -0
- package/package.json +28 -0
- package/runtime/dist/src/auth/browser-auth.js +184 -0
- package/runtime/dist/src/auth/credential-store.js +181 -0
- package/runtime/dist/src/cache/etag-cache.js +123 -0
- package/runtime/dist/src/config.js +59 -0
- package/runtime/dist/src/git/git-inspector.js +375 -0
- package/runtime/dist/src/git/pre-commit.js +44 -0
- package/runtime/dist/src/git/verification-gate.js +221 -0
- package/runtime/dist/src/index.js +60 -0
- package/runtime/dist/src/journal/journal-store.js +1300 -0
- package/runtime/dist/src/mcp/server.js +11 -0
- package/runtime/dist/src/mcp/tool-definitions.js +405 -0
- package/runtime/dist/src/project/repository.js +79 -0
- package/runtime/dist/src/runtime/active-context-store.js +356 -0
- package/runtime/dist/src/runtime/api-client.js +229 -0
- package/runtime/dist/src/runtime/bridge-service.js +2226 -0
- package/runtime/dist/src/runtime/offline-outbox.js +274 -0
- package/runtime/dist/src/runtime/principal-state.js +97 -0
- package/runtime/dist/src/types.js +2 -0
- package/runtime/dist/src/utilities/files.js +189 -0
- package/runtime/dist/src/utilities/hash.js +19 -0
- package/runtime/dist/src/utilities/process.js +32 -0
- package/runtime/package-lock.json +137 -0
- package/runtime/package.json +32 -0
- package/skill/SKILL.md +29 -0
- package/skill/agents/openai.yaml +6 -0
- package/skill/references/lifecycle.md +102 -0
- package/skill/references/memory-updates.md +25 -0
- package/skill/references/questionnaires.md +98 -0
- package/skill/references/scaffolding.md +38 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { join, win32 } from 'node:path';
|
|
2
|
+
import { atomicWrite, ensureWithinRoot, isNodeError, readJson, removeFile, } from '../utilities/files.js';
|
|
3
|
+
import { sha256 } from '../utilities/hash.js';
|
|
4
|
+
import { NativeCommandRunner } from '../utilities/process.js';
|
|
5
|
+
class CommandCredentialStore {
|
|
6
|
+
service;
|
|
7
|
+
account;
|
|
8
|
+
runner;
|
|
9
|
+
constructor(service, account, runner) {
|
|
10
|
+
this.service = service;
|
|
11
|
+
this.account = account;
|
|
12
|
+
this.runner = runner ?? new NativeCommandRunner();
|
|
13
|
+
}
|
|
14
|
+
async clear() {
|
|
15
|
+
await Promise.all([
|
|
16
|
+
this.delete('access-token'),
|
|
17
|
+
this.delete('refresh-token'),
|
|
18
|
+
this.delete('browser-session'),
|
|
19
|
+
]);
|
|
20
|
+
}
|
|
21
|
+
credentialAccount(name) {
|
|
22
|
+
return `${this.account}:${name}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class WindowsDpapiCredentialStore extends CommandCredentialStore {
|
|
26
|
+
credentialRoot;
|
|
27
|
+
powerShellPath;
|
|
28
|
+
constructor(options) {
|
|
29
|
+
super(options.service, options.account, options.runner);
|
|
30
|
+
const windowsDirectory = options.windowsDirectory ?? process.env.SystemRoot ?? process.env.WINDIR;
|
|
31
|
+
if (!windowsDirectory || !win32.isAbsolute(windowsDirectory)) {
|
|
32
|
+
throw new Error('A trusted absolute Windows system directory is required');
|
|
33
|
+
}
|
|
34
|
+
this.powerShellPath = win32.join(windowsDirectory, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
35
|
+
const credentialNamespace = sha256(`${options.service}\0${options.account}`);
|
|
36
|
+
this.credentialRoot = ensureWithinRoot(options.stateRoot, join(options.stateRoot, 'credentials', credentialNamespace));
|
|
37
|
+
}
|
|
38
|
+
async get(name) {
|
|
39
|
+
const path = this.credentialPath(name);
|
|
40
|
+
const stored = await readJson(path, this.credentialRoot);
|
|
41
|
+
if (!stored) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const script = [
|
|
45
|
+
"$ErrorActionPreference='Stop'",
|
|
46
|
+
"Add-Type -AssemblyName 'System.Security'",
|
|
47
|
+
'$value=[Console]::In.ReadToEnd()',
|
|
48
|
+
'$bytes=[Convert]::FromBase64String($value)',
|
|
49
|
+
'$plain=[System.Security.Cryptography.ProtectedData]::Unprotect($bytes,$null,[System.Security.Cryptography.DataProtectionScope]::CurrentUser)',
|
|
50
|
+
'[Console]::Out.Write([Text.Encoding]::UTF8.GetString($plain))',
|
|
51
|
+
].join(';');
|
|
52
|
+
const result = await this.runner.run(this.powerShellPath, ['-NoProfile', '-NonInteractive', '-Command', script], { input: stored.protectedValue });
|
|
53
|
+
if (result.exitCode !== 0) {
|
|
54
|
+
throw new Error(`Windows credential lookup failed: ${result.stderr.trim()}`);
|
|
55
|
+
}
|
|
56
|
+
return result.stdout;
|
|
57
|
+
}
|
|
58
|
+
async set(name, value) {
|
|
59
|
+
const script = [
|
|
60
|
+
"$ErrorActionPreference='Stop'",
|
|
61
|
+
"Add-Type -AssemblyName 'System.Security'",
|
|
62
|
+
'$value=[Console]::In.ReadToEnd()',
|
|
63
|
+
'$bytes=[Text.Encoding]::UTF8.GetBytes($value)',
|
|
64
|
+
'$protected=[System.Security.Cryptography.ProtectedData]::Protect($bytes,$null,[System.Security.Cryptography.DataProtectionScope]::CurrentUser)',
|
|
65
|
+
'[Console]::Out.Write([Convert]::ToBase64String($protected))',
|
|
66
|
+
].join(';');
|
|
67
|
+
const result = await this.runner.run(this.powerShellPath, ['-NoProfile', '-NonInteractive', '-Command', script], { input: value });
|
|
68
|
+
if (result.exitCode !== 0 || !result.stdout) {
|
|
69
|
+
throw new Error(`Windows credential storage failed: ${result.stderr.trim()}`);
|
|
70
|
+
}
|
|
71
|
+
await atomicWrite(this.credentialPath(name), `${JSON.stringify({ protectedValue: result.stdout.trim() })}\n`, this.credentialRoot);
|
|
72
|
+
}
|
|
73
|
+
async delete(name) {
|
|
74
|
+
try {
|
|
75
|
+
await removeFile(this.credentialPath(name), this.credentialRoot);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (!isNodeError(error) || error.code !== 'ENOENT') {
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
credentialPath(name) {
|
|
84
|
+
return ensureWithinRoot(this.credentialRoot, join(this.credentialRoot, `${name}.json`));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export class MacOsKeychainCredentialStore extends CommandCredentialStore {
|
|
88
|
+
async get(name) {
|
|
89
|
+
const result = await this.runner.run('/usr/bin/security', [
|
|
90
|
+
'find-generic-password',
|
|
91
|
+
'-s',
|
|
92
|
+
this.service,
|
|
93
|
+
'-a',
|
|
94
|
+
this.credentialAccount(name),
|
|
95
|
+
'-w',
|
|
96
|
+
]);
|
|
97
|
+
if (result.exitCode === 44) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
if (result.exitCode !== 0) {
|
|
101
|
+
throw new Error(`macOS credential lookup failed: ${result.stderr.trim()}`);
|
|
102
|
+
}
|
|
103
|
+
return result.stdout.replace(/\r?\n$/, '');
|
|
104
|
+
}
|
|
105
|
+
async set(name, value) {
|
|
106
|
+
const script = 'IFS= read -r secret; exec /usr/bin/security add-generic-password -U -s "$1" -a "$2" -w "$secret"';
|
|
107
|
+
const result = await this.runner.run('/bin/sh', ['-c', script, 'credential-store', this.service, this.credentialAccount(name)], { input: `${value}\n` });
|
|
108
|
+
if (result.exitCode !== 0) {
|
|
109
|
+
throw new Error(`macOS credential storage failed: ${result.stderr.trim()}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async delete(name) {
|
|
113
|
+
const result = await this.runner.run('/usr/bin/security', [
|
|
114
|
+
'delete-generic-password',
|
|
115
|
+
'-s',
|
|
116
|
+
this.service,
|
|
117
|
+
'-a',
|
|
118
|
+
this.credentialAccount(name),
|
|
119
|
+
]);
|
|
120
|
+
if (result.exitCode !== 0 && result.exitCode !== 44) {
|
|
121
|
+
throw new Error(`macOS credential deletion failed: ${result.stderr.trim()}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export class LinuxSecretToolCredentialStore extends CommandCredentialStore {
|
|
126
|
+
async get(name) {
|
|
127
|
+
const result = await this.runner.run('secret-tool', [
|
|
128
|
+
'lookup',
|
|
129
|
+
'service',
|
|
130
|
+
this.service,
|
|
131
|
+
'account',
|
|
132
|
+
this.credentialAccount(name),
|
|
133
|
+
]);
|
|
134
|
+
if (result.exitCode === 1) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if (result.exitCode !== 0) {
|
|
138
|
+
throw new Error(`Linux credential lookup failed: ${result.stderr.trim()}`);
|
|
139
|
+
}
|
|
140
|
+
return result.stdout.replace(/\r?\n$/, '');
|
|
141
|
+
}
|
|
142
|
+
async set(name, value) {
|
|
143
|
+
const result = await this.runner.run('secret-tool', [
|
|
144
|
+
'store',
|
|
145
|
+
'--label',
|
|
146
|
+
this.service,
|
|
147
|
+
'service',
|
|
148
|
+
this.service,
|
|
149
|
+
'account',
|
|
150
|
+
this.credentialAccount(name),
|
|
151
|
+
], { input: value });
|
|
152
|
+
if (result.exitCode !== 0) {
|
|
153
|
+
throw new Error(`Linux credential storage failed: ${result.stderr.trim()}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async delete(name) {
|
|
157
|
+
const result = await this.runner.run('secret-tool', [
|
|
158
|
+
'clear',
|
|
159
|
+
'service',
|
|
160
|
+
this.service,
|
|
161
|
+
'account',
|
|
162
|
+
this.credentialAccount(name),
|
|
163
|
+
]);
|
|
164
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
165
|
+
throw new Error(`Linux credential deletion failed: ${result.stderr.trim()}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export function createCredentialStore(options, platform = process.platform) {
|
|
170
|
+
if (platform === 'win32') {
|
|
171
|
+
return new WindowsDpapiCredentialStore(options);
|
|
172
|
+
}
|
|
173
|
+
if (platform === 'darwin') {
|
|
174
|
+
return new MacOsKeychainCredentialStore(options.service, options.account, options.runner);
|
|
175
|
+
}
|
|
176
|
+
if (platform === 'linux') {
|
|
177
|
+
return new LinuxSecretToolCredentialStore(options.service, options.account, options.runner);
|
|
178
|
+
}
|
|
179
|
+
throw new Error(`Credential storage is not supported on ${platform}`);
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=credential-store.js.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readJson, writeJson } from '../utilities/files.js';
|
|
3
|
+
import { stableStringify } from '../utilities/hash.js';
|
|
4
|
+
import { redactForCache } from '../runtime/offline-outbox.js';
|
|
5
|
+
export class EtagCache {
|
|
6
|
+
options;
|
|
7
|
+
path;
|
|
8
|
+
entries = new Map();
|
|
9
|
+
loaded = false;
|
|
10
|
+
clock;
|
|
11
|
+
queue = Promise.resolve();
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
this.path = join(options.stateRoot, 'cache', 'etag-cache.json');
|
|
15
|
+
this.clock = options.clock ?? (() => new Date());
|
|
16
|
+
}
|
|
17
|
+
async get(key) {
|
|
18
|
+
return await this.exclusive(async () => {
|
|
19
|
+
await this.load();
|
|
20
|
+
const entry = this.entries.get(key);
|
|
21
|
+
if (!entry) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
if (this.clock().getTime() - Date.parse(entry.storedAt) > this.options.maxAgeMs) {
|
|
25
|
+
this.entries.delete(key);
|
|
26
|
+
await this.persist();
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
entry.lastAccessedAt = this.clock().toISOString();
|
|
30
|
+
this.entries.delete(key);
|
|
31
|
+
this.entries.set(key, entry);
|
|
32
|
+
await this.persist();
|
|
33
|
+
return entry;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async set(key, etag, value) {
|
|
37
|
+
await this.exclusive(async () => {
|
|
38
|
+
await this.load();
|
|
39
|
+
const persistedValue = redactForCache(JSON.parse(JSON.stringify(value)));
|
|
40
|
+
const now = this.clock().toISOString();
|
|
41
|
+
const bytes = persistedValue ? Buffer.byteLength(stableStringify(persistedValue), 'utf8') : 0;
|
|
42
|
+
if (!persistedValue || bytes > this.options.maxBytes) {
|
|
43
|
+
this.entries.delete(key);
|
|
44
|
+
await this.persist();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.entries.delete(key);
|
|
48
|
+
this.entries.set(key, {
|
|
49
|
+
key,
|
|
50
|
+
etag,
|
|
51
|
+
value: persistedValue,
|
|
52
|
+
storedAt: now,
|
|
53
|
+
lastAccessedAt: now,
|
|
54
|
+
bytes,
|
|
55
|
+
});
|
|
56
|
+
this.prune();
|
|
57
|
+
await this.persist();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async delete(key) {
|
|
61
|
+
await this.exclusive(async () => {
|
|
62
|
+
await this.load();
|
|
63
|
+
this.entries.delete(key);
|
|
64
|
+
await this.persist();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async clear() {
|
|
68
|
+
await this.exclusive(async () => {
|
|
69
|
+
await this.load();
|
|
70
|
+
this.entries.clear();
|
|
71
|
+
await this.persist();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async load() {
|
|
75
|
+
if (this.loaded) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const file = await readJson(this.path, this.options.stateRoot);
|
|
79
|
+
for (const entry of file?.entries ?? []) {
|
|
80
|
+
const safeValue = redactForCache(JSON.parse(JSON.stringify(entry.value)));
|
|
81
|
+
if (safeValue) {
|
|
82
|
+
this.entries.set(entry.key, { ...entry, value: safeValue });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
this.loaded = true;
|
|
86
|
+
this.prune();
|
|
87
|
+
}
|
|
88
|
+
prune() {
|
|
89
|
+
while (this.entries.size > this.options.maxEntries ||
|
|
90
|
+
this.totalBytes() > this.options.maxBytes) {
|
|
91
|
+
const oldestKey = this.entries.keys().next().value;
|
|
92
|
+
if (!oldestKey) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.entries.delete(oldestKey);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
totalBytes() {
|
|
99
|
+
return [...this.entries.values()].reduce((total, entry) => total + entry.bytes, 0);
|
|
100
|
+
}
|
|
101
|
+
async persist() {
|
|
102
|
+
await writeJson(this.path, {
|
|
103
|
+
schemaVersion: 1,
|
|
104
|
+
entries: [...this.entries.values()],
|
|
105
|
+
}, this.options.stateRoot);
|
|
106
|
+
}
|
|
107
|
+
async exclusive(action) {
|
|
108
|
+
const previous = this.queue;
|
|
109
|
+
let release = () => undefined;
|
|
110
|
+
const current = new Promise((resolvePromise) => {
|
|
111
|
+
release = resolvePromise;
|
|
112
|
+
});
|
|
113
|
+
this.queue = previous.then(() => current);
|
|
114
|
+
await previous;
|
|
115
|
+
try {
|
|
116
|
+
return await action();
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
release();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=etag-cache.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { sha256 } from './utilities/hash.js';
|
|
4
|
+
function positiveInteger(value, fallback) {
|
|
5
|
+
const parsed = Number(value);
|
|
6
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
7
|
+
}
|
|
8
|
+
export function loadBridgeConfig(env = process.env) {
|
|
9
|
+
return {
|
|
10
|
+
apiBaseUrl: (env.ENGINEERING_MEMORY_API_URL ?? 'http://127.0.0.1:3000').replace(/\/$/, ''),
|
|
11
|
+
stateRoot: resolve(env.ENGINEERING_MEMORY_STATE_DIR ?? join(homedir(), '.engineering-memory')),
|
|
12
|
+
requestTimeoutMs: positiveInteger(env.ENGINEERING_MEMORY_TIMEOUT_MS, 15_000),
|
|
13
|
+
cacheMaxEntries: positiveInteger(env.ENGINEERING_MEMORY_CACHE_MAX_ENTRIES, 64),
|
|
14
|
+
cacheMaxBytes: positiveInteger(env.ENGINEERING_MEMORY_CACHE_MAX_BYTES, 5 * 1024 * 1024),
|
|
15
|
+
cacheMaxAgeMs: positiveInteger(env.ENGINEERING_MEMORY_CACHE_MAX_AGE_MS, 24 * 60 * 60 * 1000),
|
|
16
|
+
markerSchemaVersion: positiveInteger(env.ENGINEERING_MEMORY_MARKER_SCHEMA_VERSION, 1),
|
|
17
|
+
credentialService: env.ENGINEERING_MEMORY_CREDENTIAL_SERVICE ?? 'engineering-memory',
|
|
18
|
+
credentialAccount: env.ENGINEERING_MEMORY_CREDENTIAL_ACCOUNT ?? 'default',
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function apiNamespaceKey(apiBaseUrl) {
|
|
22
|
+
return sha256(new URL(apiBaseUrl).toString().replace(/\/$/, ''));
|
|
23
|
+
}
|
|
24
|
+
export function apiStateRoot(config) {
|
|
25
|
+
return join(config.stateRoot, 'origins', apiNamespaceKey(config.apiBaseUrl));
|
|
26
|
+
}
|
|
27
|
+
export const endpoints = {
|
|
28
|
+
authRefresh: '/auth/refresh',
|
|
29
|
+
authLogout: '/auth/logout',
|
|
30
|
+
authBrowserStart: '/auth/browser/start',
|
|
31
|
+
authBrowserExchange: '/auth/browser/exchange',
|
|
32
|
+
sessionBootstrap: '/runtime/session/bootstrap',
|
|
33
|
+
sessionResume: '/runtime/session/resume',
|
|
34
|
+
contextPrepareChange: '/runtime/context/prepare-change',
|
|
35
|
+
contextRefresh: '/runtime/context/refresh',
|
|
36
|
+
memoryQuery: '/memory/query',
|
|
37
|
+
memoryHistory: '/memory/history',
|
|
38
|
+
memoryScaffoldPlan: '/memory/scaffold-plan',
|
|
39
|
+
memoryArchitectureModule: '/memory/architecture-module',
|
|
40
|
+
memoryProposeRevision: '/memory/proposals',
|
|
41
|
+
memoryListProposals: '/memory/proposals/pending',
|
|
42
|
+
memoryReviewProposal: (proposalId) => `/memory/proposals/${proposalId}/review`,
|
|
43
|
+
taskCheckpoint: '/tasks/checkpoint',
|
|
44
|
+
taskCorrection: '/tasks/record-correction',
|
|
45
|
+
taskReconcile: '/tasks/reconcile',
|
|
46
|
+
taskSelfReview: '/tasks/self-review',
|
|
47
|
+
taskScaffoldApplication: '/tasks/scaffold-application',
|
|
48
|
+
organizationList: '/organizations',
|
|
49
|
+
organizationCreate: '/organizations',
|
|
50
|
+
taskVerify: '/runtime/task/verify',
|
|
51
|
+
taskClose: '/runtime/task/close',
|
|
52
|
+
taskCommitGate: '/runtime/task/commit-gate',
|
|
53
|
+
projectSetup: '/projects/setup',
|
|
54
|
+
projectList: '/projects',
|
|
55
|
+
projectResolve: '/projects/resolve',
|
|
56
|
+
projectBind: (projectId) => `/projects/${projectId}/bind`,
|
|
57
|
+
projectMemberAdd: (projectId) => `/projects/${projectId}/members`,
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=config.js.map
|