memhouse 0.18.12 → 0.20.1
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/AGENTS.md +22 -151
- package/CHANGELOG.md +263 -29
- package/LICENSE +202 -22
- package/LICENSES/agentlytics-MIT.txt +21 -0
- package/LICENSES/memhouse-MIT-until-0.18.11.txt +22 -0
- package/LICENSES/models.dev-MIT.txt +22 -0
- package/NOTICE +28 -0
- package/README.md +42 -20
- package/SECURITY.md +21 -8
- package/bin/memhouse.js +2223 -311
- package/docs/README.md +11 -9
- package/docs/guides/admin-password.md +37 -10
- package/docs/guides/configuration.md +13 -6
- package/docs/guides/daemons.md +64 -3
- package/docs/guides/import-claude-ai.md +204 -0
- package/docs/guides/install.md +11 -6
- package/docs/guides/invite.md +21 -7
- package/docs/guides/operate.md +114 -7
- package/docs/guides/search-and-resume.md +72 -3
- package/docs/guides/share.md +91 -52
- package/docs/guides/storage.md +44 -4
- package/docs/guides/troubleshooting.md +121 -14
- package/docs/guides/uninstall.md +4 -3
- package/docs/guides/update.md +16 -0
- package/docs/guides/upgrade-to-0.20.md +188 -0
- package/docs/releases/README.md +2 -1
- package/docs/releases/v0.18.10.md +5 -5
- package/docs/releases/v0.18.11.md +3 -3
- package/docs/releases/v0.20.0.md +627 -0
- package/docs/releases/v0.20.1.md +114 -0
- package/docs/tutorials/01-first-house.md +10 -7
- package/docs/tutorials/02-join-a-house.md +24 -15
- package/docs/tutorials/03-share-your-memory.md +40 -31
- package/editors/antigravity.js +1 -80
- package/editors/base.js +463 -9
- package/editors/bridge.js +119 -0
- package/editors/claude.js +273 -105
- package/editors/codex.js +278 -52
- package/editors/copilot.js +1 -70
- package/editors/cursor.js +1 -75
- package/editors/derive-fingerprints.json +18 -0
- package/editors/harness-paths.js +69 -0
- package/editors/index.js +1 -20
- package/editors/normalise/agy.js +910 -0
- package/editors/normalise/copilot-cli.js +321 -0
- package/editors/normalise/cursor-agent.js +852 -0
- package/editors/normalise/cursor.js +474 -0
- package/editors/normalise/devin.js +575 -0
- package/editors/normalise/gemini-cli.js +602 -0
- package/editors/normalise/goose.js +673 -0
- package/editors/normalise/opencode.js +633 -0
- package/editors/normalise/vscode.js +549 -0
- package/editors/normalise/zed.js +473 -0
- package/editors/opencode-v4.js +119 -0
- package/editors/scope.js +14 -3
- package/editors/sessions-log.js +349 -0
- package/editors/sqlite.js +28 -1
- package/editors/vscode.js +1 -72
- package/editors/windsurf.js +1 -143
- package/examples/01-local-one-machine/README.md +24 -19
- package/examples/01-local-one-machine/run.sh +1 -1
- package/examples/02-existing-clickhouse/README.md +30 -22
- package/examples/02-existing-clickhouse/run.sh +5 -4
- package/examples/03-team-house/README.md +44 -37
- package/examples/03-team-house/run.sh +35 -18
- package/examples/04-self-hosted-team/README.md +29 -14
- package/examples/04-self-hosted-team/compose.yml +5 -2
- package/examples/04-self-hosted-team/smoke.sh +26 -12
- package/examples/README.md +8 -4
- package/examples/fixtures/README.md +4 -1
- package/examples/fixtures/make-sessions.js +8 -5
- package/memhouse/admin-secret.js +89 -24
- package/memhouse/argv-secrets.js +64 -0
- package/memhouse/channel.js +49 -9
- package/memhouse/codex-plugin.js +194 -0
- package/memhouse/delivery/AGENT-INSTALL-PROMPT.md +2 -1
- package/memhouse/delivery/AGENT-INSTALL.md +22 -11
- package/memhouse/delivery/INVITATION.md.tpl +44 -45
- package/memhouse/delivery/PROMPT.md +53 -5
- package/memhouse/delivery/UPGRADE.md.tpl +1 -1
- package/memhouse/delivery/codex/AGENTS-BLOCK.md +11 -0
- package/memhouse/delivery/codex/SKILL.md +50 -0
- package/memhouse/delivery/kernel-install.md +17 -9
- package/memhouse/delivery/plugin/.claude-plugin/plugin.json +2 -2
- package/memhouse/delivery/plugin/reference/HOUSE.md +168 -41
- package/memhouse/delivery/plugin/skills/access/SKILL.md +13 -5
- package/memhouse/delivery/plugin/skills/admin/SKILL.md +14 -6
- package/memhouse/delivery/plugin/skills/house/SKILL.md +25 -8
- package/memhouse/delivery/plugin/skills/recall/SKILL.md +66 -8
- package/memhouse/delivery/plugin/skills/sql/SKILL.md +66 -7
- package/memhouse/deploy.js +1 -1
- package/memhouse/flags.js +35 -18
- package/memhouse/help.js +37 -0
- package/memhouse/host.js +82 -2
- package/memhouse/house/HOUSE.md +2 -2
- package/memhouse/house/convert-profile.js +638 -0
- package/memhouse/house/convert-v3.js +742 -0
- package/memhouse/house/doctor-v3.js +324 -0
- package/memhouse/house/host-alias.js +85 -0
- package/memhouse/house/house.js +3 -3
- package/memhouse/house/migrate.js +2 -2
- package/memhouse/house/profile-dedupe.js +183 -0
- package/memhouse/house/profile-edges.js +554 -0
- package/memhouse/house/profile-house.js +430 -0
- package/memhouse/house/profile-rollup.js +190 -0
- package/memhouse/house/q-cli.js +863 -0
- package/memhouse/house/queries-v3.js +1013 -0
- package/memhouse/house/reconcile-v3.js +192 -0
- package/memhouse/house/records-to-profile.js +174 -0
- package/memhouse/house/records-to-rows.js +169 -0
- package/memhouse/house/relocate.js +2 -1
- package/memhouse/house/schema-choice.js +121 -0
- package/memhouse/house/schema-profile.js +290 -0
- package/memhouse/house/schema-profile.sql.tpl +398 -0
- package/memhouse/house/schema-v3.js +267 -0
- package/memhouse/house/schema-v3.sql.tpl +213 -0
- package/memhouse/import/claude-ai.js +420 -0
- package/memhouse/import/import.js +372 -0
- package/memhouse/import/pyjson.js +206 -0
- package/memhouse/import/records.js +64 -0
- package/memhouse/import/writer-profile.js +192 -0
- package/memhouse/import/writer-v3.js +240 -0
- package/memhouse/import/zip.js +112 -0
- package/memhouse/invitation.js +102 -0
- package/memhouse/invitefile.js +5 -2
- package/memhouse/plugin-copies.js +176 -0
- package/memhouse/provision.js +7 -2
- package/memhouse/release-hint.js +24 -0
- package/memhouse/resume.js +4 -4
- package/memhouse/server/api-v3.js +632 -0
- package/memhouse/server/dashboard-auth.js +181 -0
- package/memhouse/server/dashboard-port.js +91 -0
- package/memhouse/server/queries.js +30 -0
- package/memhouse/server/server.js +118 -20
- package/memhouse/service.js +1 -1
- package/memhouse/share.js +687 -1
- package/memhouse/shipper/adapter-capture.js +756 -0
- package/memhouse/shipper/capture.js +2571 -0
- package/memhouse/shipper/dedupe-profile.js +237 -0
- package/memhouse/shipper/dedupe-v3.js +1027 -0
- package/memhouse/shipper/forks.js +233 -0
- package/memhouse/shipper/log-adapter.js +219 -0
- package/memhouse/shipper/maintenance.js +49 -0
- package/memhouse/shipper/normalise-run.js +148 -0
- package/memhouse/shipper/passlog.js +68 -5
- package/memhouse/shipper/profile-capture.js +903 -0
- package/memhouse/shipper/ship.js +91 -8
- package/memhouse/shipper/snapshots.js +193 -0
- package/memhouse/shipper/source-roots.js +111 -0
- package/memhouse/version-notice.js +66 -0
- package/package.json +14 -7
- package/public/THIRD_PARTY_NOTICES.txt +2 -2
- package/public/assets/index-CcNnhfM3.js +78 -0
- package/public/index.html +1 -1
- package/ui/package-lock.json +56 -37
- package/ui/package.json +1 -0
- package/ui/src/App.jsx +29 -14
- package/ui/src/components/ChatSidebar.jsx +69 -12
- package/ui/src/lib/api.js +76 -0
- package/ui/src/lib/dashboard-token.js +41 -0
- package/ui/src/main.jsx +22 -5
- package/ui/src/pages/Dashboard.jsx +22 -36
- package/ui/third-party-notices.js +119 -0
- package/ui/vite.config.js +5 -1
- package/docs/releases/v0.18.12.md +0 -59
- package/memhouse/COMPETITION.md +0 -221
- package/memhouse/DESIGN.md +0 -222
- package/memhouse/RESUME-AND-UPDATE.md +0 -182
- package/public/assets/index-CnWzrjF3.js +0 -78
package/memhouse/shipper/ship.js
CHANGED
|
@@ -675,7 +675,7 @@ async function recordHouseState(client, rooms, host) {
|
|
|
675
675
|
* record at all.)
|
|
676
676
|
*
|
|
677
677
|
* Releases before 0.10.0 never read this — for them the floor is enforced by the
|
|
678
|
-
*
|
|
678
|
+
* user's GRANTs, not by code.
|
|
679
679
|
*/
|
|
680
680
|
/**
|
|
681
681
|
* A house from before the one-layout holds plain rooms — `messages`, not `<member>_messages`.
|
|
@@ -749,9 +749,13 @@ async function assertWriterSupported(client, rooms) {
|
|
|
749
749
|
+ ' Bring the house forward: memhouse migrate');
|
|
750
750
|
}
|
|
751
751
|
if (floor > mine) {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
752
|
+
// A house fenced for schema 3 (memhouse migrate --v3-prepare): this release has the schema-3
|
|
753
|
+
// capture, so the remedy is to switch, not to update.
|
|
754
|
+
throw new Error(floor === 3
|
|
755
|
+
? 'this house moved to schema 3 (its 0.18 rooms are fenced off and converted).\n'
|
|
756
|
+
+ ' Switch THIS machine to it: memhouse migrate --v3-switch (then: memhouse stop && memhouse start)'
|
|
757
|
+
: `this house requires writers at schema ${floor}+; this memhouse supports ${SUPPORTED_SCHEMAS.join(', ')}.\n`
|
|
758
|
+
+ ' Update THIS machine: memhouse update');
|
|
755
759
|
}
|
|
756
760
|
}
|
|
757
761
|
|
|
@@ -1531,7 +1535,31 @@ async function printStats(client) {
|
|
|
1531
1535
|
|
|
1532
1536
|
async function main() {
|
|
1533
1537
|
const argv = process.argv.slice(2);
|
|
1538
|
+
// A shipper that starts (or re-execs) on a new version refreshes the /mem skills bound to its
|
|
1539
|
+
// home, as any CLI call does: an npm upgrade restarts the daemons, not `update`.
|
|
1540
|
+
{
|
|
1541
|
+
const root = require('path').join(__dirname, '..', '..');
|
|
1542
|
+
const r = require('../plugin-copies').refreshIfStale({
|
|
1543
|
+
home: process.env.MEMHOUSE_HOME || require('path').join(require('os').homedir(), '.memhouse'),
|
|
1544
|
+
version: require(require('path').join(root, 'package.json')).version,
|
|
1545
|
+
deliveryDir: require('path').join(root, 'memhouse', 'delivery'), repoRoot: root,
|
|
1546
|
+
log: (l) => console.error(`[memhouse] ${l}`),
|
|
1547
|
+
});
|
|
1548
|
+
for (const e of r.errors) console.error(`[memhouse] could not refresh the /mem skills: ${e}`);
|
|
1549
|
+
}
|
|
1534
1550
|
let client = makeClient();
|
|
1551
|
+
// This daemon re-read its env file: it adopted a moved house or a newer credential.
|
|
1552
|
+
// `memhouse status` calls a shipper stale when the env file changed after it last read it
|
|
1553
|
+
// (bin/memhouse.js shipperStale), so record when that was. Only for the daemon the pid file
|
|
1554
|
+
// names: a one-shot pass adopting for itself leaves a running daemon as stale as it is.
|
|
1555
|
+
const noteEnvRead = () => {
|
|
1556
|
+
try {
|
|
1557
|
+
const p = require('path'); const fs = require('fs');
|
|
1558
|
+
const run = p.join(process.env.MEMHOUSE_HOME || p.join(require('os').homedir(), '.memhouse'), 'run');
|
|
1559
|
+
if (fs.readFileSync(p.join(run, 'shipper.pid'), 'utf-8').trim() !== String(process.pid)) return;
|
|
1560
|
+
fs.writeFileSync(p.join(run, 'shipper.env-read'), `${new Date().toISOString()}\n`);
|
|
1561
|
+
} catch { /* no pid file: not the daemon */ }
|
|
1562
|
+
};
|
|
1535
1563
|
// The operator may have converted the house to the one layout since this env was
|
|
1536
1564
|
// written: the member's rooms moved from <name>.* to mem.<name>_*. Adopt that here —
|
|
1537
1565
|
// rewrite the env, point this process at `mem` — instead of refusing the old address on
|
|
@@ -1546,10 +1574,17 @@ async function main() {
|
|
|
1546
1574
|
const rs = await probe.query({ query: `SELECT database, name FROM system.tables WHERE (database = '${db.replace(/'/g, "\\'")}' AND name IN ('messages', '${mine}')) OR (database = 'mem' AND name = '${mine}')`, format: 'JSONEachRow' });
|
|
1547
1575
|
const rows = await rs.json(); const has = (d, n) => rows.some((r) => r.database === d && r.name === n);
|
|
1548
1576
|
if (!has(db, mine) && !has(db, 'messages') && has('mem', mine)) {
|
|
1549
|
-
|
|
1550
|
-
|
|
1577
|
+
// --stats only reads: it adopts the move for this process and leaves the env file to
|
|
1578
|
+
// a pass or `memhouse update`. The /mem skills run `memhouse stats` without asking.
|
|
1579
|
+
const readOnly = argv.includes('--stats');
|
|
1580
|
+
if (!readOnly) {
|
|
1581
|
+
const envPath = require('path').join(process.env.MEMHOUSE_HOME || require('path').join(require('os').homedir(), '.memhouse'), 'env');
|
|
1582
|
+
try { const fs = require('fs'); fs.writeFileSync(envPath, require('../envfile').setKey(fs.readFileSync(envPath, 'utf-8'), 'MEMHOUSE_DB', 'mem')); noteEnvRead(); } catch { /* env not writable: this process still adopts */ }
|
|
1583
|
+
}
|
|
1551
1584
|
process.env.MEMHOUSE_DB = 'mem'; client = makeClient();
|
|
1552
|
-
console.error(
|
|
1585
|
+
console.error(readOnly
|
|
1586
|
+
? `[memhouse] your rooms moved: ${db}.messages is now mem.${mine} — reading mem; memhouse update records it in the env`
|
|
1587
|
+
: `[memhouse] your rooms moved: ${db}.messages is now mem.${mine} — env now says MEMHOUSE_DB=mem`);
|
|
1553
1588
|
}
|
|
1554
1589
|
}
|
|
1555
1590
|
} catch { /* unreachable or unreadable: the pass will say */ }
|
|
@@ -1618,6 +1653,7 @@ async function main() {
|
|
|
1618
1653
|
for (const k of drift.changed) process.env[k] = drift.values[k];
|
|
1619
1654
|
client = makeClient();
|
|
1620
1655
|
adoptedCredential = true;
|
|
1656
|
+
noteEnvRead();
|
|
1621
1657
|
console.error(`[memhouse] ${envPath} holds a newer ${drift.changed.map((k) => k.replace('MEMHOUSE_', '').toLowerCase()).join('/')} — adopting it and retrying now`);
|
|
1622
1658
|
} else {
|
|
1623
1659
|
console.error(`[memhouse] the credential this shipper holds was rejected by ${process.env.MEMHOUSE_URL} (and ${envPath} says the same)`);
|
|
@@ -1660,7 +1696,54 @@ async function main() {
|
|
|
1660
1696
|
|
|
1661
1697
|
module.exports = { runShip, ensureSchema, refreshStats, templateColumns, decideEpoch, tailRows, tailSafe, rowsForChat, buildRows, SUBAGENT_SEQ_BASE, SUBAGENT_SEQ_STRIDE };
|
|
1662
1698
|
|
|
1663
|
-
|
|
1699
|
+
// Schema v3 (docs/design/source-identity.md): the capture engine replaces this shipper's pass.
|
|
1700
|
+
// `--v3`, or MEMHOUSE_SCHEMA=3 in the environment, hands the whole run to capture.js — except
|
|
1701
|
+
// the two non-shipping modes the CLI runs through this entry point, which must NOT turn into a
|
|
1702
|
+
// capture pass on a schema-3 install (`memhouse update` runs --ensure-schema with a 120 s
|
|
1703
|
+
// timeout; `memhouse stats` runs --stats):
|
|
1704
|
+
// --ensure-schema create what this credential may of the member's eight profile tables
|
|
1705
|
+
// (harness-sessions-sql), then verify all eight (keys included); exit 1 when any is
|
|
1706
|
+
// missing or wrong
|
|
1707
|
+
// --stats per-harness counts from sessions FINAL
|
|
1708
|
+
const V3 = require.main === module && (process.argv.includes('--v3') || process.env.MEMHOUSE_SCHEMA === '3');
|
|
1709
|
+
async function v3NonShipping(argv) {
|
|
1710
|
+
const env = process.env;
|
|
1711
|
+
const { httpTransport, statsV3, renderStats } = require('../house/q-cli');
|
|
1712
|
+
let password = env.MEMHOUSE_PASSWORD || '';
|
|
1713
|
+
if (!password && env.MEMHOUSE_PASSWORD_FILE) password = require('fs').readFileSync(env.MEMHOUSE_PASSWORD_FILE, 'utf-8').replace(/\r?\n$/, '');
|
|
1714
|
+
const db = env.MEMHOUSE_DB || 'mem';
|
|
1715
|
+
const t = httpTransport({ url: env.MEMHOUSE_URL || 'http://localhost:8123', user: env.MEMHOUSE_USER || 'default', password, db: '', timeout: 110000 });
|
|
1716
|
+
const member = env.MEMHOUSE_MEMBER || (await t.q.rows('SELECT currentUser() AS u'))[0].u;
|
|
1717
|
+
if (argv.includes('--stats')) {
|
|
1718
|
+
const Q = require('../house/queries-v3');
|
|
1719
|
+
for (const l of renderStats(await statsV3(t.exec, Q.v3Rooms(member, db)))) console.log(l);
|
|
1720
|
+
return 0;
|
|
1721
|
+
}
|
|
1722
|
+
// A member on the 0.20 development layout is moved by its admin, never by laying the profile
|
|
1723
|
+
// beside <m>_turns here (`memhouse update` runs this): that would make it look converted.
|
|
1724
|
+
const sc = require('../house/schema-choice');
|
|
1725
|
+
const dev = sc.devLayoutRefusal(await sc.detectHouseSchema((sql) => t.q.rows(sql), db, member), member);
|
|
1726
|
+
if (dev) { console.error(`[memhouse] --ensure-schema: ${dev}`); return 1; }
|
|
1727
|
+
const sp = require('../house/schema-profile');
|
|
1728
|
+
const s = await sp.ensureProfileSchema(t.q, { db, member });
|
|
1729
|
+
const bad = Object.values(s.tables).filter((x) => x.problem);
|
|
1730
|
+
if (s.unhealed.length && env.MEMHOUSE_QUIET_DENIED !== '1') console.error(`[memhouse] ${member}: ${s.unhealed.join(', ')} — capture writes the old types until an admin retypes them (memhouse install --schema 3 --admin-user …)`);
|
|
1731
|
+
if (!bad.length) { console.log(`[memhouse] schema ensured (schema 3, harness-sessions-sql's profile: ${Object.values(s.tables).map((x) => x.fq).join(', ')})`); return 0; }
|
|
1732
|
+
if (env.MEMHOUSE_QUIET_DENIED !== '1') {
|
|
1733
|
+
console.error(`[memhouse] schema-3 tables not ready for ${member}: ${bad.map((x) => `${x.fq} ${x.problem}`).join('; ')}`);
|
|
1734
|
+
if (s.denied.length) console.error(`[memhouse] not permitted for ${member}: ${s.denied.join('; ')} — an admin creates them (memhouse install --schema 3 --admin-user …)`);
|
|
1735
|
+
}
|
|
1736
|
+
return 1;
|
|
1737
|
+
}
|
|
1738
|
+
if (V3 && (process.argv.includes('--ensure-schema') || process.argv.includes('--stats'))) {
|
|
1739
|
+
v3NonShipping(process.argv.slice(2)).then((code) => { process.exitCode = code; },
|
|
1740
|
+
(e) => { console.error(`[memhouse] fatal: ${e.message}`); process.exit(1); });
|
|
1741
|
+
} else if (V3) {
|
|
1742
|
+
// The capture loop checks for an upgrade after every interval, as the 0.18 loop does below.
|
|
1743
|
+
require('./capture').main(process.argv.slice(2).filter((a) => a !== '--v3'), {
|
|
1744
|
+
afterSleep: () => selfUpdate.maybeRestart({ snap: selfSnap, name: 'shipper' }),
|
|
1745
|
+
}).catch((e) => { console.error(`[memhouse] fatal: ${e.message}`); process.exit(1); });
|
|
1746
|
+
} else if (require.main === module) {
|
|
1664
1747
|
main().catch((e) => {
|
|
1665
1748
|
// A permissions failure here is not a crash — the CALLER (install, doctor) catches the
|
|
1666
1749
|
// same condition and prints a refusal naming both routes. Printing "[memhouse] fatal:"
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Snapshots: the source side of the bridge (editors/bridge.js). Every editor without a
|
|
3
|
+
// transcript-file capture keeps its 0.18 reader; this module runs those readers and writes each
|
|
4
|
+
// session they return as a snapshot log, $MEMHOUSE_HOME/snapshots/<harness>/<session>.jsonl,
|
|
5
|
+
// which the capture engine then reads like any transcript.
|
|
6
|
+
//
|
|
7
|
+
// The log stays append-only whenever the session only grew: a message already written keeps its
|
|
8
|
+
// line byte for byte (and so its timestamp), new messages are appended, and a changed name or
|
|
9
|
+
// folder is appended as a new meta line. Only when the editor's own history changed (a message
|
|
10
|
+
// edited, removed or reordered) is the file rewritten — atomically, through a temp file — and the
|
|
11
|
+
// engine sees a rewrite: new fgen, vanished keys tombstoned.
|
|
12
|
+
//
|
|
13
|
+
// A session is re-read only when its editor reports a different lastUpdatedAt or message count
|
|
14
|
+
// than the last pass recorded (index.json beside the logs), so a pass over unchanged editors costs
|
|
15
|
+
// one getChats() per editor.
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { fileNameFor } = require('../../editors/bridge');
|
|
20
|
+
|
|
21
|
+
const TEXT_MAX = 50000; // as 0.18's messages.text, so dedupe compares like with like
|
|
22
|
+
const TOOL_CALL_RE = /\[tool-call: ([^(\]]+)/g;
|
|
23
|
+
const NATIVE = new Set(['claude', 'codex']); // adapters with their own file capture
|
|
24
|
+
|
|
25
|
+
function truncate(s, max) {
|
|
26
|
+
if (s.length <= max) return s;
|
|
27
|
+
const cut = s.slice(0, max);
|
|
28
|
+
const last = cut.charCodeAt(cut.length - 1);
|
|
29
|
+
return (last >= 0xD800 && last <= 0xDBFF) ? cut.slice(0, -1) : cut;
|
|
30
|
+
}
|
|
31
|
+
function ownTs(m) { const at = Number(m && m._ts); return Number.isFinite(at) && at > 0 ? (at < 1e11 ? at * 1000 : at) : null; }
|
|
32
|
+
function interpTs(chat, pos, total) {
|
|
33
|
+
const start = chat.createdAt || chat.lastUpdatedAt || Date.now();
|
|
34
|
+
const end = chat.lastUpdatedAt || chat.createdAt || start;
|
|
35
|
+
if (total <= 1) return start;
|
|
36
|
+
return start + Math.round((end - start) * (pos / (total - 1)));
|
|
37
|
+
}
|
|
38
|
+
function num(v) { const x = Math.trunc(Number(v)); return Number.isFinite(x) && x > 0 ? x : 0; }
|
|
39
|
+
|
|
40
|
+
/** The snapshot line for message `pos` of a parse — the same text and calls 0.18's buildRows produced. */
|
|
41
|
+
function messageLine(chat, m, pos, total) {
|
|
42
|
+
const raw = typeof m.content === 'string' ? m.content : (JSON.stringify(m.content) ?? '');
|
|
43
|
+
const text = truncate(raw, TEXT_MAX);
|
|
44
|
+
const own = ownTs(m);
|
|
45
|
+
const line = { k: 'msg', i: pos, role: String(m.role || ''), text, ts: own ?? interpTs(chat, pos, total), tsk: own ? 'own' : 'interp' };
|
|
46
|
+
if (m._model) line.model = String(m._model);
|
|
47
|
+
const tok = { in: num(m._inputTokens), out: num(m._outputTokens), cr: num(m._cacheRead), cw: num(m._cacheWrite) };
|
|
48
|
+
if (tok.in || tok.out || tok.cr || tok.cw) line.tok = tok;
|
|
49
|
+
if (m.role === 'assistant') {
|
|
50
|
+
let calls;
|
|
51
|
+
if (Array.isArray(m._toolCalls) && m._toolCalls.length) calls = m._toolCalls.map((tc) => ({ name: String(tc.name || 'unknown'), args: tc.args || {} }));
|
|
52
|
+
else { calls = []; for (const x of text.matchAll(TOOL_CALL_RE)) calls.push({ name: x[1].trim(), args: {} }); }
|
|
53
|
+
if (calls.length) line.tools = calls;
|
|
54
|
+
}
|
|
55
|
+
return line;
|
|
56
|
+
}
|
|
57
|
+
function metaOf(chat) {
|
|
58
|
+
return { k: 'meta', name: chat.name || '', mode: chat.mode || '', folder: chat.folder || '', created: Number(chat.createdAt) || 0, updated: Number(chat.lastUpdatedAt) || 0, branch: chat._gitBranch || '' };
|
|
59
|
+
}
|
|
60
|
+
const sameMeta = (a, b) => a && b && a.name === b.name && a.mode === b.mode && a.folder === b.folder && a.branch === b.branch;
|
|
61
|
+
|
|
62
|
+
function readLines(file) {
|
|
63
|
+
let s;
|
|
64
|
+
try { s = fs.readFileSync(file, 'utf-8'); } catch { return null; }
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const l of s.split('\n')) { if (!l) continue; try { out.push({ raw: l, o: JSON.parse(l) }); } catch { return null; } }
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Bring one session's snapshot up to date. Returns 'new' | 'append' | 'rewrite' | 'same'.
|
|
72
|
+
*/
|
|
73
|
+
function writeSession(file, chat, messages) {
|
|
74
|
+
const total = messages.length;
|
|
75
|
+
const old = readLines(file);
|
|
76
|
+
const oldMsgs = old ? old.filter((x) => x.o.k === 'msg') : [];
|
|
77
|
+
const lastMeta = old ? [...old].reverse().find((x) => x.o.k === 'meta') : null;
|
|
78
|
+
// Keep every already-written message line whose content is unchanged (text, role, model,
|
|
79
|
+
// tokens, calls): its timestamp was fixed when it was first written.
|
|
80
|
+
const fresh = messages.map((m, pos) => messageLine(chat, m, pos, total));
|
|
81
|
+
let keep = 0;
|
|
82
|
+
while (keep < oldMsgs.length && keep < fresh.length) {
|
|
83
|
+
const a = oldMsgs[keep].o, b = fresh[keep];
|
|
84
|
+
const same = a.role === b.role && a.text === b.text && (a.model || '') === (b.model || '')
|
|
85
|
+
&& JSON.stringify(a.tok || null) === JSON.stringify(b.tok || null) && JSON.stringify(a.tools || null) === JSON.stringify(b.tools || null);
|
|
86
|
+
if (!same) break;
|
|
87
|
+
keep++;
|
|
88
|
+
}
|
|
89
|
+
const meta = metaOf(chat);
|
|
90
|
+
if (old && keep === oldMsgs.length) {
|
|
91
|
+
// Only grew (or nothing changed): append new messages, and a meta line if the session's name,
|
|
92
|
+
// mode, folder or branch changed. New messages without an own time are dated by the span now.
|
|
93
|
+
const add = [];
|
|
94
|
+
if (!sameMeta(lastMeta && lastMeta.o, meta)) add.push(JSON.stringify(meta));
|
|
95
|
+
for (let i = keep; i < fresh.length; i++) add.push(JSON.stringify(fresh[i]));
|
|
96
|
+
if (!add.length) return 'same';
|
|
97
|
+
fs.appendFileSync(file, add.map((l) => `${l}\n`).join(''));
|
|
98
|
+
return 'append';
|
|
99
|
+
}
|
|
100
|
+
// New, or the editor's history changed: write the whole log, keeping the lines that still hold.
|
|
101
|
+
const lines = [JSON.stringify(meta)];
|
|
102
|
+
for (let i = 0; i < fresh.length; i++) lines.push(i < keep ? oldMsgs[i].raw : JSON.stringify(fresh[i]));
|
|
103
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
104
|
+
fs.writeFileSync(tmp, lines.map((l) => `${l}\n`).join(''));
|
|
105
|
+
fs.renameSync(tmp, file);
|
|
106
|
+
return old ? 'rewrite' : 'new';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Run every bridged editor's reader and update the snapshot logs.
|
|
111
|
+
* @param {object} o
|
|
112
|
+
* @param {string} o.home $MEMHOUSE_HOME
|
|
113
|
+
* @param {object[]} [o.adapters] 0.18 adapters (default: editors/index.js, narrowed by MEMHOUSE_EDITORS)
|
|
114
|
+
* @param {string} [o.editors] the MEMHOUSE_EDITORS value to narrow by (default: the environment's)
|
|
115
|
+
* @param {Function} [o.getMessages]
|
|
116
|
+
* @returns {{ dir, harnesses: string[], stats }}
|
|
117
|
+
*/
|
|
118
|
+
function materialize(o = {}) {
|
|
119
|
+
const dir = path.join(o.home, 'snapshots');
|
|
120
|
+
const idx = o.adapters ? null : require('../../editors/index');
|
|
121
|
+
let adapters = o.adapters;
|
|
122
|
+
if (!adapters) {
|
|
123
|
+
const scope = require('../../editors/scope');
|
|
124
|
+
adapters = scope.selectEditors(idx.editors, o.editors !== undefined ? o.editors : process.env.MEMHOUSE_EDITORS);
|
|
125
|
+
}
|
|
126
|
+
adapters = adapters.filter((a) => !NATIVE.has(a.name) && !(a.capture && !a.capture.bridged));
|
|
127
|
+
const getMessages = o.getMessages || (idx ? idx.getMessages : (chat) => chat._adapter.getMessages(chat));
|
|
128
|
+
const indexFile = path.join(dir, 'index.json');
|
|
129
|
+
let index = {};
|
|
130
|
+
try { index = JSON.parse(fs.readFileSync(indexFile, 'utf-8')) || {}; } catch { index = {}; }
|
|
131
|
+
const stats = { editors: 0, chats: 0, read: 0, new: 0, append: 0, rewrite: 0, same: 0, errors: 0, ms: 0 };
|
|
132
|
+
const t0 = Date.now();
|
|
133
|
+
const seen = new Set();
|
|
134
|
+
for (const a of adapters) {
|
|
135
|
+
let chats = [];
|
|
136
|
+
try { chats = a.getChats() || []; } catch (e) { stats.errors++; (o.log || (() => {}))(`[memhouse] bridge: ${a.name}: ${e.message}`); continue; }
|
|
137
|
+
if (chats.length) stats.editors++;
|
|
138
|
+
// One session id reported twice (the same chat under two roots or stores): keep the longer,
|
|
139
|
+
// then the newer — as discover() does for a file under two roots. Otherwise the two would
|
|
140
|
+
// overwrite each other's log on every pass.
|
|
141
|
+
const best = new Map();
|
|
142
|
+
for (const chat of chats) {
|
|
143
|
+
const k = `${chat.source || a.name}\t${chat.composerId}`;
|
|
144
|
+
const b = best.get(k);
|
|
145
|
+
const better = !b || (Number(chat.bubbleCount) || 0) > (Number(b.bubbleCount) || 0)
|
|
146
|
+
|| ((Number(chat.bubbleCount) || 0) === (Number(b.bubbleCount) || 0) && (Number(chat.lastUpdatedAt) || 0) > (Number(b.lastUpdatedAt) || 0));
|
|
147
|
+
if (b) stats.duplicates = (stats.duplicates || 0) + 1;
|
|
148
|
+
if (better) best.set(k, chat);
|
|
149
|
+
}
|
|
150
|
+
for (const chat of best.values()) {
|
|
151
|
+
const harness = String(chat.source || a.name);
|
|
152
|
+
if (!/^[a-z0-9][a-z0-9-]{0,40}$/.test(harness) || chat.composerId == null) continue;
|
|
153
|
+
stats.chats++;
|
|
154
|
+
const k = `${harness}\t${chat.composerId}`;
|
|
155
|
+
seen.add(harness);
|
|
156
|
+
const hdir = path.join(dir, harness);
|
|
157
|
+
const file = path.join(hdir, fileNameFor(chat.composerId));
|
|
158
|
+
const prev = index[k];
|
|
159
|
+
if (prev && prev.updated === (Number(chat.lastUpdatedAt) || 0) && prev.count === (Number(chat.bubbleCount) || 0) && fs.existsSync(file)) { stats.same++; continue; }
|
|
160
|
+
let messages;
|
|
161
|
+
try { messages = (o.adapters ? a.getMessages(chat) : getMessages(chat)) || []; } catch (e) { stats.errors++; continue; }
|
|
162
|
+
if (messages._defer) continue;
|
|
163
|
+
stats.read++;
|
|
164
|
+
fs.mkdirSync(hdir, { recursive: true });
|
|
165
|
+
const r = writeSession(file, chat, messages);
|
|
166
|
+
stats[r]++;
|
|
167
|
+
index[k] = { updated: Number(chat.lastUpdatedAt) || 0, count: Number(chat.bubbleCount) || 0 };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
171
|
+
const tmp = `${indexFile}.tmp-${process.pid}`;
|
|
172
|
+
fs.writeFileSync(tmp, JSON.stringify(index));
|
|
173
|
+
fs.renameSync(tmp, indexFile);
|
|
174
|
+
// Every harness with a snapshot directory is a root, including one whose editor returned nothing
|
|
175
|
+
// this pass: its logs stay captured (nothing is deleted here).
|
|
176
|
+
let harnesses = [];
|
|
177
|
+
try { harnesses = fs.readdirSync(dir).filter((n) => { try { return fs.statSync(path.join(dir, n)).isDirectory(); } catch { return false; } }); } catch { /* none */ }
|
|
178
|
+
stats.ms = Date.now() - t0;
|
|
179
|
+
return { dir, harnesses, stats };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The capture roots and adapters for the snapshot directories under `dir`. */
|
|
183
|
+
function bridgeRoots(dir, harnesses) {
|
|
184
|
+
const { bridgeCapture } = require('../../editors/bridge');
|
|
185
|
+
// harness_path: the editor's data folder (editors/harness-paths.js), the same store key its v4
|
|
186
|
+
// normaliser will use, so switching a harness off the bridge keeps its sessions' keys.
|
|
187
|
+
const { dataFolder } = require('../../editors/harness-paths');
|
|
188
|
+
const roots = harnesses.map((h) => ({ harness: h, name: 'default', dir: path.join(dir, h), harness_path: dataFolder(h, { fallback: path.join(dir, h) }) }));
|
|
189
|
+
const adapters = Object.fromEntries(harnesses.map((h) => [h, bridgeCapture(h)]));
|
|
190
|
+
return { roots, adapters };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = { materialize, bridgeRoots, writeSession, messageLine, metaOf, NATIVE };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// The default MEMHOUSE_SOURCE_ROOTS — what the schema-3 capture reads when nothing names it.
|
|
2
|
+
//
|
|
3
|
+
// The rule: exactly what the 0.18 shipper reads on this machine, for the two harnesses the
|
|
4
|
+
// capture engine has adapters for. Two scopes that disagreed would make a schema-3 install
|
|
5
|
+
// silently ship less (or more) than the same machine shipped the day before.
|
|
6
|
+
//
|
|
7
|
+
// claude-code every Claude Code config dir the legacy adapter finds
|
|
8
|
+
// (editors/claude.js discoverClaudeRoots: ~/.claude, each
|
|
9
|
+
// ~/.claude-playbooks/<name>[/playbook]; MEMHOUSE_CLAUDE_ROOTS REPLACES that
|
|
10
|
+
// list, as it does for the legacy shipper) — only those holding a projects/
|
|
11
|
+
// dir, since that is where the transcripts are. Each root is named by its
|
|
12
|
+
// directory (`default` for ~/.claude, the playbook name otherwise), the same
|
|
13
|
+
// rule the conversion uses for `root`.
|
|
14
|
+
// codex <codex home>/sessions and <codex home>/archived_sessions, whichever exist.
|
|
15
|
+
// The codex home is MEMHOUSE_CODEX_ROOTS, else CODEX_HOME, else ~/.codex —
|
|
16
|
+
// the legacy adapter's order.
|
|
17
|
+
//
|
|
18
|
+
// MEMHOUSE_EDITORS narrows it the same way it narrows the legacy shipper: `claude` and
|
|
19
|
+
// `codex` are the adapter names; an editors list that names neither yields no roots.
|
|
20
|
+
//
|
|
21
|
+
// Pure apart from the filesystem reads; `home`/`env` are injectable so a test can point it
|
|
22
|
+
// at a synthetic tree (the Claude discovery itself is the adapter's, which reads $HOME).
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const os = require('os');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
function isDir(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
|
|
31
|
+
function parseList(v) { return String(v || '').split(',').map((s) => s.trim()).filter(Boolean); }
|
|
32
|
+
function expandHome(p, home) { return p === '~' ? home : p.startsWith('~/') ? path.join(home, p.slice(2)) : p; }
|
|
33
|
+
|
|
34
|
+
/** ~/.claude is `default`; a playbook install is its own name (draft 5 §3 `root`). */
|
|
35
|
+
function claudeRootName(configDir) {
|
|
36
|
+
const b = path.basename(configDir);
|
|
37
|
+
if (b === '.claude' || b === 'claude') return 'default';
|
|
38
|
+
if (b === 'playbook') return path.basename(path.dirname(configDir));
|
|
39
|
+
return b.replace(/^\./, '') || 'default';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The Claude Code config dirs, as the legacy adapter finds them. `discover` is injectable
|
|
44
|
+
* for tests; by default it is editors/claude.js, so the two cannot drift.
|
|
45
|
+
*/
|
|
46
|
+
function claudeConfigDirs({ discover = null } = {}) {
|
|
47
|
+
const fn = discover || require('../../editors/claude').discoverClaudeRoots;
|
|
48
|
+
return fn();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function codexHome({ env = process.env, home = os.homedir() } = {}) {
|
|
52
|
+
const override = parseList(env.MEMHOUSE_CODEX_ROOTS).map((p) => expandHome(p, home));
|
|
53
|
+
if (override.length) return override[0];
|
|
54
|
+
if (env.CODEX_HOME && env.CODEX_HOME.trim()) return path.resolve(env.CODEX_HOME.trim());
|
|
55
|
+
return path.join(home, '.codex');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @returns {{ roots: [{harness, dir, name}], spec: string, skipped: [{dir, why}] }}
|
|
60
|
+
* `roots` is what capture.js reads (the shape parseSourceRoots returns); `spec` the same as
|
|
61
|
+
* a MEMHOUSE_SOURCE_ROOTS value, for display and for pinning it in an env file.
|
|
62
|
+
*/
|
|
63
|
+
function defaultSourceRoots({ env = process.env, home = os.homedir(), discover = null } = {}) {
|
|
64
|
+
const editors = parseList(env.MEMHOUSE_EDITORS);
|
|
65
|
+
const want = (name) => !editors.length || editors.includes(name) || (name === 'claude' && editors.includes('claude-code'));
|
|
66
|
+
const roots = [];
|
|
67
|
+
const skipped = [];
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
const add = (r) => {
|
|
70
|
+
let rp; try { rp = fs.realpathSync(r.dir); } catch { rp = r.dir; }
|
|
71
|
+
if (seen.has(rp)) return;
|
|
72
|
+
seen.add(rp);
|
|
73
|
+
roots.push(r);
|
|
74
|
+
};
|
|
75
|
+
if (want('claude')) {
|
|
76
|
+
for (const d of claudeConfigDirs({ discover })) {
|
|
77
|
+
const projects = path.join(d, 'projects');
|
|
78
|
+
if (!isDir(projects)) { skipped.push({ dir: d, why: 'no projects/ (history.jsonl only)' }); continue; }
|
|
79
|
+
add({ harness: 'claude-code', dir: projects, name: claudeRootName(d) });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (want('codex')) {
|
|
83
|
+
const ch = codexHome({ env, home });
|
|
84
|
+
for (const sub of ['sessions', 'archived_sessions']) {
|
|
85
|
+
const d = path.join(ch, sub);
|
|
86
|
+
if (isDir(d)) add({ harness: 'codex', dir: d, name: 'default' });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const spec = roots.map((r) => `${r.harness === 'codex' ? 'codex' : 'claude'}:${r.name}=${r.dir}`).join(',');
|
|
90
|
+
return { roots, spec, skipped };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The roots a schema-3 capture will read under this environment: MEMHOUSE_SOURCE_ROOTS when
|
|
95
|
+
* set (parsed by capture.js's own parser), the default otherwise.
|
|
96
|
+
* @returns {{ roots, spec, defaulted: boolean, error: string|null }}
|
|
97
|
+
*/
|
|
98
|
+
function effectiveSourceRoots({ env = process.env, home = os.homedir(), discover = null } = {}) {
|
|
99
|
+
if (env.MEMHOUSE_SOURCE_ROOTS) {
|
|
100
|
+
try {
|
|
101
|
+
const { parseSourceRoots } = require('./capture');
|
|
102
|
+
return { roots: parseSourceRoots(env.MEMHOUSE_SOURCE_ROOTS), spec: env.MEMHOUSE_SOURCE_ROOTS, defaulted: false, error: null };
|
|
103
|
+
} catch (e) { return { roots: [], spec: env.MEMHOUSE_SOURCE_ROOTS, defaulted: false, error: e.message }; }
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const d = defaultSourceRoots({ env, home, discover });
|
|
107
|
+
return { roots: d.roots, spec: d.spec, defaulted: true, error: null, skipped: d.skipped };
|
|
108
|
+
} catch (e) { return { roots: [], spec: '', defaulted: true, error: e.message }; }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { defaultSourceRoots, effectiveSourceRoots, claudeRootName, codexHome, claudeConfigDirs };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// A new minor (or major) version's first run says so, once, and points at its release notes.
|
|
2
|
+
//
|
|
3
|
+
// `npm install -g memhouse` never runs `update`, so nothing told a machine that crossed a minor
|
|
4
|
+
// version (0.20 is a different house layout from 0.18) what had changed or what to do. A stamp in
|
|
5
|
+
// the home records the last version that ran here; when the line (major.minor) is newer, the
|
|
6
|
+
// first CLI call prints one notice to stderr (so --json stays clean) and moves the stamp. A patch
|
|
7
|
+
// release and a downgrade move the stamp silently.
|
|
8
|
+
//
|
|
9
|
+
// Versions before this notice wrote no stamp, so with none the previous version is inferred:
|
|
10
|
+
// skills-checked the version 0.20.0's /mem skills refresh recorded (memhouse/plugin-copies.js);
|
|
11
|
+
// read before that refresh moves it, so this runs first
|
|
12
|
+
// the env file MEMHOUSE_SCHEMA=3 means a 0.20 install; any other install ran 0.18 or earlier
|
|
13
|
+
// neither a fresh install: stamped silently, there is nothing to compare with
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const STAMP = 'version-seen';
|
|
19
|
+
const SKILLS_STAMP = 'skills-checked';
|
|
20
|
+
const lineOf = (v) => { const m = /^(\d+)\.(\d+)/.exec(String(v || '')); return m ? `${m[1]}.${m[2]}` : null; };
|
|
21
|
+
const lineNum = (l) => l.split('.').map(Number);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The notice for going from `seen` to `version`, or null: only for a newer major.minor. `seenLabel`
|
|
25
|
+
* is how the previous version is named when it was inferred ("0.18 or earlier"). Pure.
|
|
26
|
+
*/
|
|
27
|
+
function noticeFor({ seen, version, notesPath = null, seenLabel = null }) {
|
|
28
|
+
const a = lineOf(seen); const b = lineOf(version);
|
|
29
|
+
if (!a || !b || a === b || seen === version) return null;
|
|
30
|
+
const [am, an] = lineNum(a); const [bm, bn] = lineNum(b);
|
|
31
|
+
if (bm < am || (bm === am && bn < an)) return null; // a downgrade is not news about a release
|
|
32
|
+
return `memhouse ${version} is new on this machine (it ran ${seenLabel || seen} before). What changed and what to do: ${notesPath || `docs/releases/v${version}.md`}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The version that ran here before, when no stamp says it: { seen, label } or null (fresh). */
|
|
36
|
+
function inferPrevious(home) {
|
|
37
|
+
try { const v = fs.readFileSync(path.join(home, SKILLS_STAMP), 'utf-8').trim(); if (lineOf(v)) return { seen: v, label: null }; } catch { /* none */ }
|
|
38
|
+
let env;
|
|
39
|
+
try { env = fs.readFileSync(path.join(home, 'env'), 'utf-8'); } catch { return null; }
|
|
40
|
+
if (/^MEMHOUSE_SCHEMA=['"]?3['"]?\s*$/m.test(env)) return { seen: '0.20.0', label: '0.20' };
|
|
41
|
+
return { seen: '0.18.0', label: '0.18 or earlier' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check, print once, and stamp. `home` is the memhouse home; nothing happens when it does not
|
|
46
|
+
* exist (no install here) or from a git checkout. Never throws.
|
|
47
|
+
*/
|
|
48
|
+
function checkVersionNotice({ home, version, repoRoot, log = () => {} }) {
|
|
49
|
+
try {
|
|
50
|
+
if (!home || !fs.existsSync(home)) return null;
|
|
51
|
+
// From a git checkout, nothing: a development run must not stamp a real home with its version.
|
|
52
|
+
if (repoRoot && fs.existsSync(path.join(repoRoot, '.git'))) return null;
|
|
53
|
+
const stamp = path.join(home, STAMP);
|
|
54
|
+
let prev = null;
|
|
55
|
+
try { const v = fs.readFileSync(stamp, 'utf-8').trim(); if (v) prev = { seen: v, label: null }; } catch { /* none yet */ }
|
|
56
|
+
if (!prev) prev = inferPrevious(home);
|
|
57
|
+
if (prev && prev.seen === version) return null;
|
|
58
|
+
const notes = repoRoot ? path.join(repoRoot, 'docs', 'releases', `v${String(version).split('-')[0]}.md`) : null;
|
|
59
|
+
const msg = prev ? noticeFor({ seen: prev.seen, version, seenLabel: prev.label, notesPath: notes && fs.existsSync(notes) ? notes : null }) : null;
|
|
60
|
+
if (msg) log(msg);
|
|
61
|
+
try { fs.writeFileSync(stamp, `${version}\n`); } catch { /* not writable: asked again next run */ }
|
|
62
|
+
return msg;
|
|
63
|
+
} catch { return null; }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { STAMP, SKILLS_STAMP, lineOf, noticeFor, inferPrevious, checkVersionNotice };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memhouse",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "memhouse
|
|
3
|
+
"version": "0.20.1",
|
|
4
|
+
"description": "memhouse — agent conversation memory across 17 editors: parse-on-client shippers, a typed ClickHouse store with one set of rooms per member, and the agentlytics dashboard",
|
|
5
5
|
"main": "bin/memhouse.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"memhouse": "./bin/memhouse.js"
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
|
11
11
|
"memhouse/",
|
|
12
|
+
"!memhouse/DESIGN.md",
|
|
13
|
+
"!memhouse/COMPETITION.md",
|
|
14
|
+
"!memhouse/RESUME-AND-UPDATE.md",
|
|
12
15
|
"misc/syntax-check.js",
|
|
13
16
|
"pricing.js",
|
|
14
17
|
"pricing.json",
|
|
@@ -18,9 +21,12 @@
|
|
|
18
21
|
"ui/package.json",
|
|
19
22
|
"ui/package-lock.json",
|
|
20
23
|
"ui/vite.config.js",
|
|
24
|
+
"ui/third-party-notices.js",
|
|
21
25
|
"ui/eslint.config.js",
|
|
22
26
|
"public/",
|
|
23
27
|
"README.md",
|
|
28
|
+
"NOTICE",
|
|
29
|
+
"LICENSES/",
|
|
24
30
|
"docs/README.md",
|
|
25
31
|
"docs/tutorials/",
|
|
26
32
|
"docs/guides/",
|
|
@@ -34,7 +40,7 @@
|
|
|
34
40
|
"start": "node bin/memhouse.js",
|
|
35
41
|
"build": "cd ui && npm install && npm run build",
|
|
36
42
|
"dev": "cd ui && npm run dev",
|
|
37
|
-
"test": "node misc/syntax-check.js && node misc/unit-test.js && node misc/env-guard-test.js && node misc/skill-allowlist-test.js",
|
|
43
|
+
"test": "node misc/syntax-check.js && node misc/unit-test.js && node misc/v3-read-unit.js && node misc/v3-cli-unit.js && node misc/capture-test.js && node misc/dedupe-test.js && node misc/dedupe-profile-test.js && node misc/profile-edges-test.js && node misc/profile-routing-test.js && node misc/log-adapter-test.js && node misc/bridge-test.js && node misc/layout32-test.js && node misc/v4-opencode-test.js && node misc/normalise/run-all.js && node misc/maintenance-test.js && node misc/no-credentials-test.js && node misc/kav-test.js && node misc/publish-guard-test.js && node misc/records-to-rows-test.js && node misc/adapter-capture-test.js && node misc/import-claude-ai-test.js && node misc/records-to-profile-test.js && node misc/profile-capture-twin-test.js && node misc/profile-vanished-test.js && node misc/env-guard-test.js && node misc/skill-allowlist-test.js && node misc/plugin-copies-test.js && node misc/codex-plugin-test.js && node misc/host-alias-test.js && node misc/registry-test.js && node misc/doctor-tail-test.js && node misc/dashboard-auth-test.js",
|
|
38
44
|
"test:house": "node misc/house-test.js",
|
|
39
45
|
"prepack": "cd ui && npm install --no-audit --no-fund && npm run build",
|
|
40
46
|
"prepublishOnly": "node misc/publish-guard.js"
|
|
@@ -51,15 +57,16 @@
|
|
|
51
57
|
"memhouse"
|
|
52
58
|
],
|
|
53
59
|
"author": "Ramazan Polat (built on agentlytics by fkadev)",
|
|
54
|
-
"license": "
|
|
60
|
+
"license": "Apache-2.0",
|
|
55
61
|
"engines": {
|
|
56
62
|
"node": ">=24.0.0"
|
|
57
63
|
},
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
"
|
|
64
|
+
"homepage": "https://memhouse.io",
|
|
65
|
+
"bugs": {
|
|
66
|
+
"email": "security@memhouse.io"
|
|
61
67
|
},
|
|
62
68
|
"dependencies": {
|
|
69
|
+
"@agent-realm/harness-session-adapters": "1.0.0",
|
|
63
70
|
"@clickhouse/client": "^1.23.1",
|
|
64
71
|
"express": "^4.22.1"
|
|
65
72
|
}
|
|
@@ -1578,7 +1578,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
1578
1578
|
SOFTWARE.
|
|
1579
1579
|
|
|
1580
1580
|
========================================================================
|
|
1581
|
-
react-router 7.18.
|
|
1581
|
+
react-router 7.18.4 — MIT
|
|
1582
1582
|
https://github.com/remix-run/react-router
|
|
1583
1583
|
------------------------------------------------------------------------
|
|
1584
1584
|
MIT License
|
|
@@ -1606,7 +1606,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
1606
1606
|
SOFTWARE.
|
|
1607
1607
|
|
|
1608
1608
|
========================================================================
|
|
1609
|
-
react-router-dom 7.18.
|
|
1609
|
+
react-router-dom 7.18.4 — MIT
|
|
1610
1610
|
https://github.com/remix-run/react-router
|
|
1611
1611
|
------------------------------------------------------------------------
|
|
1612
1612
|
MIT License
|