ihow-memory 0.1.0-alpha.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/LICENSE +202 -0
- package/NOTICE +15 -0
- package/README.md +250 -0
- package/TRADEMARK.md +24 -0
- package/bin/ihow-memory.mjs +53 -0
- package/dist/cli.js +1084 -0
- package/dist/core.js +85 -0
- package/dist/engine/fts.js +210 -0
- package/dist/engine/manifest.js +45 -0
- package/dist/engine/retrieval.js +324 -0
- package/dist/governance.js +369 -0
- package/dist/http/console.js +287 -0
- package/dist/mcp/server.js +235 -0
- package/dist/store/events.js +17 -0
- package/dist/store/files.js +61 -0
- package/dist/store/lock.js +35 -0
- package/dist/telemetry.js +98 -0
- package/dist/types.js +3 -0
- package/dist/workspace.js +151 -0
- package/package.json +62 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --experimental-strip-types
|
|
2
|
+
import readline from 'node:readline';
|
|
3
|
+
import { openCore } from '../core.js';
|
|
4
|
+
function parseWorkspaceArgs(argv) {
|
|
5
|
+
const options = {};
|
|
6
|
+
for(let index = 0; index < argv.length; index += 1){
|
|
7
|
+
if (argv[index] === '--space') options.space = argv[++index];
|
|
8
|
+
else if (argv[index] === '--root') options.root = argv[++index];
|
|
9
|
+
else if (argv[index] === '--memory-root') options.memoryRoot = argv[++index];
|
|
10
|
+
else if (argv[index] === '--state-root') options.stateRoot = argv[++index];
|
|
11
|
+
else if (argv[index] === '--cwd') options.cwd = argv[++index];
|
|
12
|
+
else if (argv[index] === '--engine') options.engine = argv[++index];
|
|
13
|
+
else if (argv[index] === '--vector-provider-command') options.vectorProviderCommand = argv[++index];
|
|
14
|
+
else if (argv[index] === '--vector-model') options.vectorModel = argv[++index];
|
|
15
|
+
else if (argv[index] === '--vector-timeout-ms') options.vectorTimeoutMs = Number(argv[++index]);
|
|
16
|
+
}
|
|
17
|
+
return options;
|
|
18
|
+
}
|
|
19
|
+
function send(value) {
|
|
20
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
21
|
+
}
|
|
22
|
+
function result(id, value) {
|
|
23
|
+
send({
|
|
24
|
+
jsonrpc: '2.0',
|
|
25
|
+
id,
|
|
26
|
+
result: value
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function error(id, code, message) {
|
|
30
|
+
send({
|
|
31
|
+
jsonrpc: '2.0',
|
|
32
|
+
id,
|
|
33
|
+
error: {
|
|
34
|
+
code,
|
|
35
|
+
message
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const TOOL_DEFINITIONS = [
|
|
40
|
+
{
|
|
41
|
+
name: 'memory.search',
|
|
42
|
+
description: 'Search local iHow Memory with FTS. Returns citation path and snippet.',
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
query: {
|
|
47
|
+
type: 'string'
|
|
48
|
+
},
|
|
49
|
+
limit: {
|
|
50
|
+
type: 'number'
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
required: [
|
|
54
|
+
'query'
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'memory.read',
|
|
60
|
+
description: 'Read a memory markdown file by path and return exact content plus citation.',
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
ref: {
|
|
65
|
+
type: 'string'
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
required: [
|
|
69
|
+
'ref'
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'memory.write_candidate',
|
|
75
|
+
description: 'Write a memory candidate into the sandbox inbox. Does not write durable memory.',
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
text: {
|
|
80
|
+
type: 'string'
|
|
81
|
+
},
|
|
82
|
+
title: {
|
|
83
|
+
type: 'string'
|
|
84
|
+
},
|
|
85
|
+
sourceAgent: {
|
|
86
|
+
type: 'string'
|
|
87
|
+
},
|
|
88
|
+
metadata: {
|
|
89
|
+
type: 'object'
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
required: [
|
|
93
|
+
'text'
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'memory.promote',
|
|
99
|
+
description: 'Promote a candidate into governed staging with an audit event. Existing workspace mode uses memory/_mcp/promoted only.',
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
candidate: {
|
|
104
|
+
type: 'string'
|
|
105
|
+
},
|
|
106
|
+
target: {
|
|
107
|
+
type: 'object'
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
required: [
|
|
111
|
+
'candidate'
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'memory.durable_promote',
|
|
117
|
+
description: 'Run a governed durable promote. Requires explicit dryRun=true or realWrite=true.',
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: 'object',
|
|
120
|
+
properties: {
|
|
121
|
+
candidate: {
|
|
122
|
+
type: 'string'
|
|
123
|
+
},
|
|
124
|
+
dryRun: {
|
|
125
|
+
type: 'boolean'
|
|
126
|
+
},
|
|
127
|
+
realWrite: {
|
|
128
|
+
type: 'boolean'
|
|
129
|
+
},
|
|
130
|
+
actor: {
|
|
131
|
+
type: 'string'
|
|
132
|
+
},
|
|
133
|
+
target: {
|
|
134
|
+
type: 'object'
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
required: [
|
|
138
|
+
'candidate'
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'memory.status',
|
|
144
|
+
description: 'Return workspace, local FTS provider, index, and sync status.',
|
|
145
|
+
inputSchema: {
|
|
146
|
+
type: 'object',
|
|
147
|
+
properties: {}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
];
|
|
151
|
+
async function main() {
|
|
152
|
+
const core = await openCore(parseWorkspaceArgs(process.argv.slice(2)));
|
|
153
|
+
const rl = readline.createInterface({
|
|
154
|
+
input: process.stdin,
|
|
155
|
+
crlfDelay: Infinity
|
|
156
|
+
});
|
|
157
|
+
rl.on('line', async (line)=>{
|
|
158
|
+
if (!line.trim()) return;
|
|
159
|
+
let request;
|
|
160
|
+
try {
|
|
161
|
+
request = JSON.parse(line);
|
|
162
|
+
} catch {
|
|
163
|
+
error(null, -32700, 'parse_error');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const id = request.id ?? null;
|
|
168
|
+
const params = request.params || {};
|
|
169
|
+
if (request.method === 'initialize') {
|
|
170
|
+
result(id, {
|
|
171
|
+
protocolVersion: '2024-11-05',
|
|
172
|
+
serverInfo: {
|
|
173
|
+
name: 'ihow-memory-core',
|
|
174
|
+
version: '0.0.1-a0.1'
|
|
175
|
+
},
|
|
176
|
+
capabilities: {
|
|
177
|
+
tools: {}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
} else if (request.method?.startsWith('notifications/')) {
|
|
181
|
+
return;
|
|
182
|
+
} else if (request.method === 'tools/list') {
|
|
183
|
+
result(id, {
|
|
184
|
+
tools: TOOL_DEFINITIONS
|
|
185
|
+
});
|
|
186
|
+
} else if (request.method === 'tools/call') {
|
|
187
|
+
const name = String(params.name || '');
|
|
188
|
+
const args = params.arguments || {};
|
|
189
|
+
let payload;
|
|
190
|
+
if (name === 'memory.search') {
|
|
191
|
+
payload = await core.search(String(args.query || ''), {
|
|
192
|
+
limit: Number(args.limit || 5)
|
|
193
|
+
});
|
|
194
|
+
} else if (name === 'memory.read') {
|
|
195
|
+
payload = await core.read(String(args.ref || ''));
|
|
196
|
+
} else if (name === 'memory.write_candidate') {
|
|
197
|
+
payload = await core.write_candidate(args);
|
|
198
|
+
} else if (name === 'memory.promote') {
|
|
199
|
+
payload = await core.promote(String(args.candidate || ''), args.target || {});
|
|
200
|
+
} else if (name === 'memory.durable_promote') {
|
|
201
|
+
payload = await core.durable_promote(String(args.candidate || ''), {
|
|
202
|
+
dryRun: args.dryRun === true,
|
|
203
|
+
realWrite: args.realWrite === true,
|
|
204
|
+
actor: typeof args.actor === 'string' ? args.actor : 'mcp',
|
|
205
|
+
target: args.target || {}
|
|
206
|
+
});
|
|
207
|
+
} else if (name === 'memory.status') {
|
|
208
|
+
payload = await core.status();
|
|
209
|
+
} else {
|
|
210
|
+
throw new Error('unknown_tool');
|
|
211
|
+
}
|
|
212
|
+
result(id, {
|
|
213
|
+
content: [
|
|
214
|
+
{
|
|
215
|
+
type: 'text',
|
|
216
|
+
text: JSON.stringify(payload, null, 2)
|
|
217
|
+
}
|
|
218
|
+
],
|
|
219
|
+
structuredContent: payload
|
|
220
|
+
});
|
|
221
|
+
} else {
|
|
222
|
+
error(id, -32601, 'method_not_found');
|
|
223
|
+
}
|
|
224
|
+
} catch (caught) {
|
|
225
|
+
error(request.id ?? null, -32000, caught instanceof Error ? caught.message : String(caught));
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
main().catch((caught)=>{
|
|
230
|
+
console.error(caught instanceof Error ? caught.message : String(caught));
|
|
231
|
+
process.exitCode = 1;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
//# sourceURL=mcp/server.ts
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import { appendFileAtomic } from './files.js';
|
|
4
|
+
export async function appendEvent(workspace, event) {
|
|
5
|
+
const fullEvent = {
|
|
6
|
+
id: crypto.randomUUID(),
|
|
7
|
+
at: new Date().toISOString(),
|
|
8
|
+
...event
|
|
9
|
+
};
|
|
10
|
+
const day = fullEvent.at.slice(0, 10);
|
|
11
|
+
const logPath = path.join(workspace.eventsDir, `${day}.ndjson`);
|
|
12
|
+
await appendFileAtomic(logPath, `${JSON.stringify(fullEvent)}\n`);
|
|
13
|
+
return fullEvent;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
//# sourceURL=store/events.ts
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { absoluteFromMemoryPath, relativeToSpace } from '../workspace.js';
|
|
5
|
+
export async function atomicWriteFile(filePath, content) {
|
|
6
|
+
await fs.mkdir(path.dirname(filePath), {
|
|
7
|
+
recursive: true
|
|
8
|
+
});
|
|
9
|
+
const tmpPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
10
|
+
await fs.writeFile(tmpPath, content, 'utf8');
|
|
11
|
+
await fs.rename(tmpPath, filePath);
|
|
12
|
+
}
|
|
13
|
+
export async function appendFileAtomic(filePath, line) {
|
|
14
|
+
await fs.mkdir(path.dirname(filePath), {
|
|
15
|
+
recursive: true
|
|
16
|
+
});
|
|
17
|
+
await fs.appendFile(filePath, line, 'utf8');
|
|
18
|
+
}
|
|
19
|
+
export async function readMemoryFile(workspace, ref) {
|
|
20
|
+
const absolutePath = absoluteFromMemoryPath(workspace, ref);
|
|
21
|
+
const content = await fs.readFile(absolutePath, 'utf8');
|
|
22
|
+
return {
|
|
23
|
+
path: relativeToSpace(workspace, absolutePath),
|
|
24
|
+
content
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export async function listMarkdownFiles(root) {
|
|
28
|
+
const results = [];
|
|
29
|
+
async function visit(dir) {
|
|
30
|
+
let entries;
|
|
31
|
+
try {
|
|
32
|
+
entries = await fs.readdir(dir, {
|
|
33
|
+
withFileTypes: true
|
|
34
|
+
});
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error.code === 'ENOENT') return;
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
for (const entry of entries){
|
|
40
|
+
const absolute = path.join(dir, entry.name);
|
|
41
|
+
if (entry.isDirectory()) {
|
|
42
|
+
if (entry.name === '_events') continue;
|
|
43
|
+
await visit(absolute);
|
|
44
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
45
|
+
results.push(absolute);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
await visit(root);
|
|
50
|
+
return results.sort();
|
|
51
|
+
}
|
|
52
|
+
export function nowCompact() {
|
|
53
|
+
return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
|
54
|
+
}
|
|
55
|
+
export function safeFileSlug(input, fallback = 'memory') {
|
|
56
|
+
const slug = input.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/-+/g, '-').replace(/^[-.]+|[-.]+$/g, '');
|
|
57
|
+
return (slug || fallback).slice(0, 80);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
//# sourceURL=store/files.ts
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const LOCK_RETRY_MS = 25;
|
|
4
|
+
const LOCK_TIMEOUT_MS = 5000;
|
|
5
|
+
function sleep(ms) {
|
|
6
|
+
return new Promise((resolve)=>setTimeout(resolve, ms));
|
|
7
|
+
}
|
|
8
|
+
export async function withWorkspaceLock(workspace, fn) {
|
|
9
|
+
await fs.mkdir(path.dirname(workspace.lockPath), {
|
|
10
|
+
recursive: true
|
|
11
|
+
});
|
|
12
|
+
const started = Date.now();
|
|
13
|
+
let handle;
|
|
14
|
+
while(!handle){
|
|
15
|
+
try {
|
|
16
|
+
handle = await fs.open(workspace.lockPath, 'wx');
|
|
17
|
+
await handle.writeFile(`${process.pid}\n${new Date().toISOString()}\n`, 'utf8');
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error.code !== 'EEXIST') throw error;
|
|
20
|
+
if (Date.now() - started > LOCK_TIMEOUT_MS) throw new Error('workspace_lock_timeout');
|
|
21
|
+
await sleep(LOCK_RETRY_MS);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return await fn();
|
|
26
|
+
} finally{
|
|
27
|
+
await handle.close();
|
|
28
|
+
await fs.rm(workspace.lockPath, {
|
|
29
|
+
force: true
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
//# sourceURL=store/lock.ts
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), '.ihow-memory');
|
|
6
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'telemetry.json');
|
|
7
|
+
const EVENTS_PATH = path.join(CONFIG_DIR, 'telemetry-events.jsonl');
|
|
8
|
+
const ALLOWED_PROP_KEYS = [
|
|
9
|
+
'runtime',
|
|
10
|
+
'version',
|
|
11
|
+
'errorType'
|
|
12
|
+
];
|
|
13
|
+
function freshConfig() {
|
|
14
|
+
return {
|
|
15
|
+
enabled: false,
|
|
16
|
+
anonId: crypto.randomBytes(8).toString('hex'),
|
|
17
|
+
asked: false
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export async function readConfig() {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(await fs.readFile(CONFIG_PATH, 'utf8'));
|
|
23
|
+
if (parsed && typeof parsed === 'object') return parsed;
|
|
24
|
+
return null;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function writeConfig(cfg) {
|
|
30
|
+
await fs.mkdir(CONFIG_DIR, {
|
|
31
|
+
recursive: true
|
|
32
|
+
});
|
|
33
|
+
await fs.writeFile(CONFIG_PATH, `${JSON.stringify(cfg, null, 2)}\n`, 'utf8');
|
|
34
|
+
}
|
|
35
|
+
export async function isEnabled() {
|
|
36
|
+
const cfg = await readConfig();
|
|
37
|
+
return cfg?.enabled === true;
|
|
38
|
+
}
|
|
39
|
+
export async function hasAsked() {
|
|
40
|
+
const cfg = await readConfig();
|
|
41
|
+
return cfg?.asked === true;
|
|
42
|
+
}
|
|
43
|
+
export async function setEnabled(enabled) {
|
|
44
|
+
const cfg = await readConfig() || freshConfig();
|
|
45
|
+
cfg.enabled = enabled;
|
|
46
|
+
cfg.asked = true;
|
|
47
|
+
await writeConfig(cfg);
|
|
48
|
+
}
|
|
49
|
+
export async function markAsked() {
|
|
50
|
+
const cfg = await readConfig() || freshConfig();
|
|
51
|
+
cfg.asked = true;
|
|
52
|
+
await writeConfig(cfg);
|
|
53
|
+
}
|
|
54
|
+
export async function track(event, props = {}) {
|
|
55
|
+
try {
|
|
56
|
+
if (!await isEnabled()) return;
|
|
57
|
+
const safe = {
|
|
58
|
+
event: String(event),
|
|
59
|
+
ts: new Date().toISOString()
|
|
60
|
+
};
|
|
61
|
+
for (const key of ALLOWED_PROP_KEYS){
|
|
62
|
+
const value = props[key];
|
|
63
|
+
if (typeof value === 'string' && value.length > 0 && value.length < 64) safe[key] = value;
|
|
64
|
+
}
|
|
65
|
+
await fs.mkdir(CONFIG_DIR, {
|
|
66
|
+
recursive: true
|
|
67
|
+
});
|
|
68
|
+
await fs.appendFile(EVENTS_PATH, `${JSON.stringify(safe)}\n`, 'utf8');
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
export async function status() {
|
|
72
|
+
const cfg = await readConfig();
|
|
73
|
+
return {
|
|
74
|
+
enabled: cfg?.enabled === true,
|
|
75
|
+
asked: cfg?.asked === true,
|
|
76
|
+
anonId: cfg?.anonId ? `${cfg.anonId.slice(0, 4)}…` : null,
|
|
77
|
+
collects: [
|
|
78
|
+
'event',
|
|
79
|
+
'runtime',
|
|
80
|
+
'version',
|
|
81
|
+
'errorType',
|
|
82
|
+
'ts'
|
|
83
|
+
],
|
|
84
|
+
neverCollects: [
|
|
85
|
+
'memory content',
|
|
86
|
+
'file names',
|
|
87
|
+
'queries',
|
|
88
|
+
'paths',
|
|
89
|
+
'prompts',
|
|
90
|
+
'any user data'
|
|
91
|
+
],
|
|
92
|
+
endpoint: 'local-only — not uploaded (opt-in framework; real endpoint TBD)',
|
|
93
|
+
configPath: CONFIG_PATH
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
//# sourceURL=telemetry.ts
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import { defaultFtsManifest } from './engine/manifest.js';
|
|
6
|
+
const DEFAULT_ROOT = path.join(os.homedir(), '.ihow-memory');
|
|
7
|
+
export function defaultRoot() {
|
|
8
|
+
return process.env.IHOW_MEMORY_HOME || DEFAULT_ROOT;
|
|
9
|
+
}
|
|
10
|
+
export function defaultMemoryRoot() {
|
|
11
|
+
return process.env.MEMORY_ROOT || process.env.IHOW_MEMORY_ROOT;
|
|
12
|
+
}
|
|
13
|
+
export function defaultStateRoot() {
|
|
14
|
+
return process.env.IHOW_MEMORY_STATE_ROOT || path.join(process.cwd(), '.state');
|
|
15
|
+
}
|
|
16
|
+
export function slugifySpace(input) {
|
|
17
|
+
const normalized = input.trim().toLowerCase().replace(/\\/g, '/').replace(/[^a-z0-9._/-]+/g, '-').replace(/[/]+/g, '-').replace(/-+/g, '-').replace(/^[-.]+|[-.]+$/g, '');
|
|
18
|
+
if (normalized) return normalized.slice(0, 80);
|
|
19
|
+
return `space-${crypto.createHash('sha256').update(input).digest('hex').slice(0, 12)}`;
|
|
20
|
+
}
|
|
21
|
+
export function resolveSpace(options = {}) {
|
|
22
|
+
if (options.space) return slugifySpace(options.space);
|
|
23
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
24
|
+
const base = path.basename(cwd) || 'workspace';
|
|
25
|
+
const hash = crypto.createHash('sha256').update(cwd).digest('hex').slice(0, 8);
|
|
26
|
+
return slugifySpace(`${base}-${hash}`);
|
|
27
|
+
}
|
|
28
|
+
export function resolveWorkspace(options = {}) {
|
|
29
|
+
const memoryRoot = options.memoryRoot || defaultMemoryRoot();
|
|
30
|
+
if (memoryRoot) return resolveExistingMemoryRootWorkspace(options, memoryRoot);
|
|
31
|
+
const root = path.resolve(options.root || defaultRoot());
|
|
32
|
+
const space = resolveSpace(options);
|
|
33
|
+
const spaceDir = path.join(root, space);
|
|
34
|
+
const memoryDir = path.join(spaceDir, 'memory');
|
|
35
|
+
const mcpDir = path.join(memoryDir, '_mcp');
|
|
36
|
+
const eventsDir = path.join(memoryDir, '_events');
|
|
37
|
+
const historyDir = path.join(spaceDir, 'history');
|
|
38
|
+
return {
|
|
39
|
+
mode: 'managed-space',
|
|
40
|
+
root,
|
|
41
|
+
space,
|
|
42
|
+
spaceDir,
|
|
43
|
+
memoryDir,
|
|
44
|
+
mcpDir,
|
|
45
|
+
candidatesDir: path.join(memoryDir, 'candidate', 'inbox'),
|
|
46
|
+
promotedDir: path.join(mcpDir, 'promoted'),
|
|
47
|
+
eventsDir,
|
|
48
|
+
historyDir,
|
|
49
|
+
indexPath: path.join(spaceDir, 'index.sqlite'),
|
|
50
|
+
indexManifestPath: path.join(spaceDir, 'index-manifest.json'),
|
|
51
|
+
lockPath: path.join(spaceDir, '.lock')
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function resolveExistingMemoryRootWorkspace(options, memoryRoot) {
|
|
55
|
+
const memoryDir = path.resolve(memoryRoot);
|
|
56
|
+
const stateRoot = path.resolve(options.stateRoot || defaultStateRoot());
|
|
57
|
+
const space = resolveSpace({
|
|
58
|
+
space: options.space || path.basename(path.dirname(memoryDir)) || 'workspace-memory',
|
|
59
|
+
cwd: options.cwd
|
|
60
|
+
});
|
|
61
|
+
const spaceDir = path.join(stateRoot, space);
|
|
62
|
+
const mcpDir = path.join(memoryDir, '_mcp');
|
|
63
|
+
return {
|
|
64
|
+
mode: 'existing-memory-root',
|
|
65
|
+
root: stateRoot,
|
|
66
|
+
space,
|
|
67
|
+
spaceDir,
|
|
68
|
+
memoryDir,
|
|
69
|
+
mcpDir,
|
|
70
|
+
candidatesDir: path.join(mcpDir, 'candidates'),
|
|
71
|
+
promotedDir: path.join(mcpDir, 'promoted'),
|
|
72
|
+
eventsDir: path.join(mcpDir, '_events'),
|
|
73
|
+
historyDir: path.join(mcpDir, 'history'),
|
|
74
|
+
indexPath: path.join(spaceDir, 'index.sqlite'),
|
|
75
|
+
indexManifestPath: path.join(spaceDir, 'index-manifest.json'),
|
|
76
|
+
lockPath: path.join(spaceDir, '.lock')
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export async function ensureWorkspace(workspace) {
|
|
80
|
+
await fs.mkdir(workspace.candidatesDir, {
|
|
81
|
+
recursive: true
|
|
82
|
+
});
|
|
83
|
+
await fs.mkdir(workspace.promotedDir, {
|
|
84
|
+
recursive: true
|
|
85
|
+
});
|
|
86
|
+
if (workspace.mode === 'managed-space') {
|
|
87
|
+
await fs.mkdir(path.join(workspace.memoryDir, 'scopes'), {
|
|
88
|
+
recursive: true
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
await fs.mkdir(workspace.eventsDir, {
|
|
92
|
+
recursive: true
|
|
93
|
+
});
|
|
94
|
+
await fs.mkdir(workspace.historyDir, {
|
|
95
|
+
recursive: true
|
|
96
|
+
});
|
|
97
|
+
await fs.mkdir(path.dirname(workspace.indexPath), {
|
|
98
|
+
recursive: true
|
|
99
|
+
});
|
|
100
|
+
await ensureIndexManifest(workspace);
|
|
101
|
+
return workspace;
|
|
102
|
+
}
|
|
103
|
+
export async function ensureIndexManifest(workspace, status = 'ready') {
|
|
104
|
+
try {
|
|
105
|
+
await fs.access(workspace.indexManifestPath);
|
|
106
|
+
return;
|
|
107
|
+
} catch {
|
|
108
|
+
const manifest = defaultFtsManifest(status === 'ready' ? 'ready' : 'missing');
|
|
109
|
+
await fs.writeFile(workspace.indexManifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export function relativeToSpace(workspace, absolutePath) {
|
|
113
|
+
const resolved = path.resolve(absolutePath);
|
|
114
|
+
const memoryDir = path.resolve(workspace.memoryDir);
|
|
115
|
+
if (resolved === memoryDir || resolved.startsWith(`${memoryDir}${path.sep}`)) {
|
|
116
|
+
const rel = path.relative(memoryDir, resolved).split(path.sep).join('/');
|
|
117
|
+
return rel ? `memory/${rel}` : 'memory';
|
|
118
|
+
}
|
|
119
|
+
return path.relative(workspace.spaceDir, resolved).split(path.sep).join('/');
|
|
120
|
+
}
|
|
121
|
+
export function absoluteFromMemoryPath(workspace, ref) {
|
|
122
|
+
const normalized = ref.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
123
|
+
const withoutSpace = normalized.startsWith(`${workspace.space}/`) ? normalized.slice(workspace.space.length + 1) : normalized;
|
|
124
|
+
let rel = withoutSpace;
|
|
125
|
+
if (rel.startsWith('memory/')) {
|
|
126
|
+
rel = rel.slice('memory/'.length);
|
|
127
|
+
} else {
|
|
128
|
+
const memoryDirRef = path.resolve(rel);
|
|
129
|
+
const memoryDir = path.resolve(workspace.memoryDir);
|
|
130
|
+
if (path.isAbsolute(rel) && (memoryDirRef === memoryDir || memoryDirRef.startsWith(`${memoryDir}${path.sep}`))) {
|
|
131
|
+
rel = path.relative(memoryDir, memoryDirRef);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const absolute = path.resolve(workspace.memoryDir, rel);
|
|
135
|
+
const memoryDir = path.resolve(workspace.memoryDir);
|
|
136
|
+
if (absolute !== memoryDir && !absolute.startsWith(`${memoryDir}${path.sep}`)) {
|
|
137
|
+
throw new Error('path_outside_memory_workspace');
|
|
138
|
+
}
|
|
139
|
+
return absolute;
|
|
140
|
+
}
|
|
141
|
+
export function relativeToMemory(workspace, absolutePath) {
|
|
142
|
+
return path.relative(workspace.memoryDir, absolutePath).split(path.sep).join('/');
|
|
143
|
+
}
|
|
144
|
+
export function isMcpSandboxPath(workspace, absolutePath) {
|
|
145
|
+
const resolved = path.resolve(absolutePath);
|
|
146
|
+
const sandbox = path.resolve(workspace.mcpDir);
|
|
147
|
+
return resolved === sandbox || resolved.startsWith(`${sandbox}${path.sep}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
//# sourceURL=workspace.ts
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ihow-memory",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Local-first shared memory for AI agents with zero-dependency FTS, citations, governance, and MCP.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"ai",
|
|
8
|
+
"agent",
|
|
9
|
+
"memory",
|
|
10
|
+
"mcp",
|
|
11
|
+
"local-first",
|
|
12
|
+
"sqlite",
|
|
13
|
+
"fts",
|
|
14
|
+
"codex",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"cursor"
|
|
17
|
+
],
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/iHow1/ihow-memory-core.git"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/iHow1/ihow-memory-core#readme",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/iHow1/ihow-memory-core/issues"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"bin/",
|
|
28
|
+
"dist/cli.js",
|
|
29
|
+
"dist/core.js",
|
|
30
|
+
"dist/types.js",
|
|
31
|
+
"dist/workspace.js",
|
|
32
|
+
"dist/governance.js",
|
|
33
|
+
"dist/telemetry.js",
|
|
34
|
+
"dist/engine/",
|
|
35
|
+
"dist/store/",
|
|
36
|
+
"dist/mcp/",
|
|
37
|
+
"dist/http/console.js",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"NOTICE",
|
|
40
|
+
"TRADEMARK.md",
|
|
41
|
+
"README.md"
|
|
42
|
+
],
|
|
43
|
+
"bin": {
|
|
44
|
+
"ihow-memory": "bin/ihow-memory.mjs",
|
|
45
|
+
"ihow-memory-mcp": "dist/mcp/server.js"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=22.12"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "node scripts/build-dist.mjs",
|
|
52
|
+
"precli": "npm run build",
|
|
53
|
+
"cli": "node bin/ihow-memory.mjs",
|
|
54
|
+
"mcp": "node --experimental-strip-types src/mcp/server.ts",
|
|
55
|
+
"preactivation-proof": "npm run build",
|
|
56
|
+
"activation-proof": "node scripts/activation-proof.mjs",
|
|
57
|
+
"proof": "node scripts/proof.mjs",
|
|
58
|
+
"dogfood": "node scripts/dogfood-proof.mjs",
|
|
59
|
+
"prepack": "npm run build"
|
|
60
|
+
},
|
|
61
|
+
"license": "Apache-2.0"
|
|
62
|
+
}
|