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.
Files changed (169) hide show
  1. package/AGENTS.md +22 -151
  2. package/CHANGELOG.md +263 -29
  3. package/LICENSE +202 -22
  4. package/LICENSES/agentlytics-MIT.txt +21 -0
  5. package/LICENSES/memhouse-MIT-until-0.18.11.txt +22 -0
  6. package/LICENSES/models.dev-MIT.txt +22 -0
  7. package/NOTICE +28 -0
  8. package/README.md +42 -20
  9. package/SECURITY.md +21 -8
  10. package/bin/memhouse.js +2223 -311
  11. package/docs/README.md +11 -9
  12. package/docs/guides/admin-password.md +37 -10
  13. package/docs/guides/configuration.md +13 -6
  14. package/docs/guides/daemons.md +64 -3
  15. package/docs/guides/import-claude-ai.md +204 -0
  16. package/docs/guides/install.md +11 -6
  17. package/docs/guides/invite.md +21 -7
  18. package/docs/guides/operate.md +114 -7
  19. package/docs/guides/search-and-resume.md +72 -3
  20. package/docs/guides/share.md +91 -52
  21. package/docs/guides/storage.md +44 -4
  22. package/docs/guides/troubleshooting.md +121 -14
  23. package/docs/guides/uninstall.md +4 -3
  24. package/docs/guides/update.md +16 -0
  25. package/docs/guides/upgrade-to-0.20.md +188 -0
  26. package/docs/releases/README.md +2 -1
  27. package/docs/releases/v0.18.10.md +5 -5
  28. package/docs/releases/v0.18.11.md +3 -3
  29. package/docs/releases/v0.20.0.md +627 -0
  30. package/docs/releases/v0.20.1.md +114 -0
  31. package/docs/tutorials/01-first-house.md +10 -7
  32. package/docs/tutorials/02-join-a-house.md +24 -15
  33. package/docs/tutorials/03-share-your-memory.md +40 -31
  34. package/editors/antigravity.js +1 -80
  35. package/editors/base.js +463 -9
  36. package/editors/bridge.js +119 -0
  37. package/editors/claude.js +273 -105
  38. package/editors/codex.js +278 -52
  39. package/editors/copilot.js +1 -70
  40. package/editors/cursor.js +1 -75
  41. package/editors/derive-fingerprints.json +18 -0
  42. package/editors/harness-paths.js +69 -0
  43. package/editors/index.js +1 -20
  44. package/editors/normalise/agy.js +910 -0
  45. package/editors/normalise/copilot-cli.js +321 -0
  46. package/editors/normalise/cursor-agent.js +852 -0
  47. package/editors/normalise/cursor.js +474 -0
  48. package/editors/normalise/devin.js +575 -0
  49. package/editors/normalise/gemini-cli.js +602 -0
  50. package/editors/normalise/goose.js +673 -0
  51. package/editors/normalise/opencode.js +633 -0
  52. package/editors/normalise/vscode.js +549 -0
  53. package/editors/normalise/zed.js +473 -0
  54. package/editors/opencode-v4.js +119 -0
  55. package/editors/scope.js +14 -3
  56. package/editors/sessions-log.js +349 -0
  57. package/editors/sqlite.js +28 -1
  58. package/editors/vscode.js +1 -72
  59. package/editors/windsurf.js +1 -143
  60. package/examples/01-local-one-machine/README.md +24 -19
  61. package/examples/01-local-one-machine/run.sh +1 -1
  62. package/examples/02-existing-clickhouse/README.md +30 -22
  63. package/examples/02-existing-clickhouse/run.sh +5 -4
  64. package/examples/03-team-house/README.md +44 -37
  65. package/examples/03-team-house/run.sh +35 -18
  66. package/examples/04-self-hosted-team/README.md +29 -14
  67. package/examples/04-self-hosted-team/compose.yml +5 -2
  68. package/examples/04-self-hosted-team/smoke.sh +26 -12
  69. package/examples/README.md +8 -4
  70. package/examples/fixtures/README.md +4 -1
  71. package/examples/fixtures/make-sessions.js +8 -5
  72. package/memhouse/admin-secret.js +89 -24
  73. package/memhouse/argv-secrets.js +64 -0
  74. package/memhouse/channel.js +49 -9
  75. package/memhouse/codex-plugin.js +194 -0
  76. package/memhouse/delivery/AGENT-INSTALL-PROMPT.md +2 -1
  77. package/memhouse/delivery/AGENT-INSTALL.md +22 -11
  78. package/memhouse/delivery/INVITATION.md.tpl +44 -45
  79. package/memhouse/delivery/PROMPT.md +53 -5
  80. package/memhouse/delivery/UPGRADE.md.tpl +1 -1
  81. package/memhouse/delivery/codex/AGENTS-BLOCK.md +11 -0
  82. package/memhouse/delivery/codex/SKILL.md +50 -0
  83. package/memhouse/delivery/kernel-install.md +17 -9
  84. package/memhouse/delivery/plugin/.claude-plugin/plugin.json +2 -2
  85. package/memhouse/delivery/plugin/reference/HOUSE.md +168 -41
  86. package/memhouse/delivery/plugin/skills/access/SKILL.md +13 -5
  87. package/memhouse/delivery/plugin/skills/admin/SKILL.md +14 -6
  88. package/memhouse/delivery/plugin/skills/house/SKILL.md +25 -8
  89. package/memhouse/delivery/plugin/skills/recall/SKILL.md +66 -8
  90. package/memhouse/delivery/plugin/skills/sql/SKILL.md +66 -7
  91. package/memhouse/deploy.js +1 -1
  92. package/memhouse/flags.js +35 -18
  93. package/memhouse/help.js +37 -0
  94. package/memhouse/host.js +82 -2
  95. package/memhouse/house/HOUSE.md +2 -2
  96. package/memhouse/house/convert-profile.js +638 -0
  97. package/memhouse/house/convert-v3.js +742 -0
  98. package/memhouse/house/doctor-v3.js +324 -0
  99. package/memhouse/house/host-alias.js +85 -0
  100. package/memhouse/house/house.js +3 -3
  101. package/memhouse/house/migrate.js +2 -2
  102. package/memhouse/house/profile-dedupe.js +183 -0
  103. package/memhouse/house/profile-edges.js +554 -0
  104. package/memhouse/house/profile-house.js +430 -0
  105. package/memhouse/house/profile-rollup.js +190 -0
  106. package/memhouse/house/q-cli.js +863 -0
  107. package/memhouse/house/queries-v3.js +1013 -0
  108. package/memhouse/house/reconcile-v3.js +192 -0
  109. package/memhouse/house/records-to-profile.js +174 -0
  110. package/memhouse/house/records-to-rows.js +169 -0
  111. package/memhouse/house/relocate.js +2 -1
  112. package/memhouse/house/schema-choice.js +121 -0
  113. package/memhouse/house/schema-profile.js +290 -0
  114. package/memhouse/house/schema-profile.sql.tpl +398 -0
  115. package/memhouse/house/schema-v3.js +267 -0
  116. package/memhouse/house/schema-v3.sql.tpl +213 -0
  117. package/memhouse/import/claude-ai.js +420 -0
  118. package/memhouse/import/import.js +372 -0
  119. package/memhouse/import/pyjson.js +206 -0
  120. package/memhouse/import/records.js +64 -0
  121. package/memhouse/import/writer-profile.js +192 -0
  122. package/memhouse/import/writer-v3.js +240 -0
  123. package/memhouse/import/zip.js +112 -0
  124. package/memhouse/invitation.js +102 -0
  125. package/memhouse/invitefile.js +5 -2
  126. package/memhouse/plugin-copies.js +176 -0
  127. package/memhouse/provision.js +7 -2
  128. package/memhouse/release-hint.js +24 -0
  129. package/memhouse/resume.js +4 -4
  130. package/memhouse/server/api-v3.js +632 -0
  131. package/memhouse/server/dashboard-auth.js +181 -0
  132. package/memhouse/server/dashboard-port.js +91 -0
  133. package/memhouse/server/queries.js +30 -0
  134. package/memhouse/server/server.js +118 -20
  135. package/memhouse/service.js +1 -1
  136. package/memhouse/share.js +687 -1
  137. package/memhouse/shipper/adapter-capture.js +756 -0
  138. package/memhouse/shipper/capture.js +2571 -0
  139. package/memhouse/shipper/dedupe-profile.js +237 -0
  140. package/memhouse/shipper/dedupe-v3.js +1027 -0
  141. package/memhouse/shipper/forks.js +233 -0
  142. package/memhouse/shipper/log-adapter.js +219 -0
  143. package/memhouse/shipper/maintenance.js +49 -0
  144. package/memhouse/shipper/normalise-run.js +148 -0
  145. package/memhouse/shipper/passlog.js +68 -5
  146. package/memhouse/shipper/profile-capture.js +903 -0
  147. package/memhouse/shipper/ship.js +91 -8
  148. package/memhouse/shipper/snapshots.js +193 -0
  149. package/memhouse/shipper/source-roots.js +111 -0
  150. package/memhouse/version-notice.js +66 -0
  151. package/package.json +14 -7
  152. package/public/THIRD_PARTY_NOTICES.txt +2 -2
  153. package/public/assets/index-CcNnhfM3.js +78 -0
  154. package/public/index.html +1 -1
  155. package/ui/package-lock.json +56 -37
  156. package/ui/package.json +1 -0
  157. package/ui/src/App.jsx +29 -14
  158. package/ui/src/components/ChatSidebar.jsx +69 -12
  159. package/ui/src/lib/api.js +76 -0
  160. package/ui/src/lib/dashboard-token.js +41 -0
  161. package/ui/src/main.jsx +22 -5
  162. package/ui/src/pages/Dashboard.jsx +22 -36
  163. package/ui/third-party-notices.js +119 -0
  164. package/ui/vite.config.js +5 -1
  165. package/docs/releases/v0.18.12.md +0 -59
  166. package/memhouse/COMPETITION.md +0 -221
  167. package/memhouse/DESIGN.md +0 -222
  168. package/memhouse/RESUME-AND-UPDATE.md +0 -182
  169. package/public/assets/index-CnWzrjF3.js +0 -78
@@ -13,13 +13,16 @@ const envfile = require('./envfile');
13
13
 
14
14
  const REQUIRED = ['MEMHOUSE_URL', 'MEMHOUSE_USER', 'MEMHOUSE_PASSWORD', 'MEMHOUSE_DB'];
15
15
 
16
+ /** The names bare `memhouse` recognises as an invite file: `invite-<name>.env`. */
17
+ function isInviteFileName(n) { return /^invite-[A-Za-z0-9_]+\.env$/.test(String(n)); }
18
+
16
19
  /** `invite-<name>.env` files in the given directories, first directory first. */
17
20
  function findInvites(dirs) {
18
21
  const out = [];
19
22
  for (const dir of dirs) {
20
23
  let names = [];
21
24
  try { names = fs.readdirSync(dir); } catch { continue; }
22
- for (const n of names.sort()) if (/^invite-[A-Za-z0-9_]+\.env$/.test(n)) out.push({ file: path.join(dir, n), dir });
25
+ for (const n of names.sort()) if (isInviteFileName(n)) out.push({ file: path.join(dir, n), dir });
23
26
  }
24
27
  return out;
25
28
  }
@@ -35,4 +38,4 @@ function describeInvite(text) {
35
38
  };
36
39
  }
37
40
 
38
- module.exports = { findInvites, describeInvite, REQUIRED };
41
+ module.exports = { isInviteFileName, findInvites, describeInvite, REQUIRED };
@@ -0,0 +1,176 @@
1
+ // The /mem plugin's installed copies, and keeping them in step with the memhouse that runs them.
2
+ //
3
+ // `plugins install claude` copies memhouse/delivery/plugin into a Claude Code config dir and
4
+ // binds the dir to this instance (memhouse/binding.js). Only `memhouse update` refreshed those
5
+ // copies — and an upgrade with `npm install -g` never runs it, nor can update run behind a
6
+ // registry mirror. A 0.18 copy then kept teaching the 0.18 rooms to agents on a schema-3 house,
7
+ // where every documented query fails. So the copies are refreshed on the new version's first
8
+ // run instead: any CLI call, or the shipper's start. A stamp in the home records the version
9
+ // that last checked, so every later call costs one small read.
10
+ //
11
+ // Only copies BOUND TO THIS HOME are touched: a machine can run two instances on two versions,
12
+ // and another instance's copy belongs to that instance's binary. A copy is never downgraded.
13
+ // The Codex copy (memhouse/codex-plugin.js) is refreshed the same way, and only where
14
+ // `plugins install codex` put it: its skill carries memhouse's ownership file and this home.
15
+ // From a git checkout nothing happens unless MEMHOUSE_REFRESH_SKILLS=1: a checkout run with a
16
+ // real MEMHOUSE_HOME would otherwise copy work-in-progress skills over the installed ones.
17
+
18
+ const fs = require('fs');
19
+ const os = require('os');
20
+ const path = require('path');
21
+
22
+ const PLUGIN_MARK = path.join('skills', 'mem', '.claude-plugin', 'plugin.json');
23
+ const STAMP = 'skills-checked';
24
+ const isPluginInstalled = (dir) => fs.existsSync(path.join(dir, PLUGIN_MARK));
25
+
26
+ /** -1, 0, 1 for x.y.z versions; a prerelease sorts before its release. Unparseable -> null. */
27
+ function compareVersions(a, b) {
28
+ const p = (v) => { const m = /^(\d+)\.(\d+)\.(\d+)(-.+)?$/.exec(String(v || '').trim()); return m ? [+m[1], +m[2], +m[3], m[4] ? 0 : 1] : null; };
29
+ const x = p(a); const y = p(b);
30
+ if (!x || !y) return null;
31
+ for (let i = 0; i < 4; i++) if (x[i] !== y[i]) return x[i] < y[i] ? -1 : 1;
32
+ return 0;
33
+ }
34
+
35
+ // Every Claude Code instance on this machine, not just the default one: CLAUDE_CONFIG_DIR, the
36
+ // stock ~/.claude, and the playbook installs the adapter discovers (editors/claude.js).
37
+ function claudeTargets() {
38
+ const seen = new Map(); // realpath -> { dir, why }
39
+ const add = (dir, why) => {
40
+ if (!dir) return;
41
+ let isDir = false;
42
+ try { isDir = fs.statSync(dir).isDirectory(); } catch { /* missing */ }
43
+ if (!isDir) return;
44
+ let rp; try { rp = fs.realpathSync(dir); } catch { rp = dir; }
45
+ if (!seen.has(rp)) seen.set(rp, { dir, why });
46
+ };
47
+ // The instance the caller is running under comes first, and is NOT subject to the
48
+ // adapter's "has this been used?" test — a fresh config dir has no projects/ yet, and it
49
+ // is exactly where the skills are wanted.
50
+ add(process.env.CLAUDE_CONFIG_DIR, 'CLAUDE_CONFIG_DIR');
51
+ add(path.join(os.homedir(), '.claude'), 'default');
52
+ try {
53
+ const { discoverClaudeRoots } = require(path.join(__dirname, '..', 'editors', 'claude'));
54
+ for (const r of discoverClaudeRoots()) add(r, 'playbook');
55
+ } catch { /* adapter unavailable — the two above still stand */ }
56
+ return [...seen.values()];
57
+ }
58
+
59
+ /**
60
+ * Replace the plugin copy in `dir` with `deliveryDir/plugin`, stamped with `version`.
61
+ * @param {(line: string) => void} [log]
62
+ * @returns {string} the copy's directory
63
+ */
64
+ function installInto(dir, { deliveryDir, version, log = () => {} }) {
65
+ // The plugin was named `memhouse` until 0.10.0. A leftover copy under the old name
66
+ // would load BESIDE the new one — /memhouse:search and /mem:recall both resolving, one
67
+ // of them stale forever. Remove it only when it is provably OURS (it carries our
68
+ // plugin.json); a directory someone else named `memhouse` is not ours to delete.
69
+ const legacy = path.join(dir, 'skills', 'memhouse');
70
+ const legacyManifest = path.join(legacy, '.claude-plugin', 'plugin.json');
71
+ if (fs.existsSync(legacyManifest)) {
72
+ let lname = null;
73
+ try { lname = JSON.parse(fs.readFileSync(legacyManifest, 'utf-8')).name; } catch { /* unreadable */ }
74
+ if (lname === 'memhouse' || lname === 'mem') {
75
+ fs.rmSync(legacy, { recursive: true });
76
+ log(`removed the pre-0.10 plugin at ${legacy} (renamed to 'mem')`);
77
+ }
78
+ }
79
+ const dst = path.join(dir, 'skills', 'mem');
80
+ // REPLACE, not overlay. cpSync over an existing install refreshes the skills and
81
+ // leaves anything else standing — a machine that once had a build with extra skills
82
+ // kept offering /mem:replay and /mem:house forever, stale, beside the real ones.
83
+ // Ownership means OUR MANIFEST, checked by name — "any plugin.json" would have deleted
84
+ // an unrelated plugin that happened to pick the same directory name.
85
+ const dstManifest = path.join(dst, '.claude-plugin', 'plugin.json');
86
+ if (fs.existsSync(dstManifest)) {
87
+ let name = null;
88
+ try { name = JSON.parse(fs.readFileSync(dstManifest, 'utf-8')).name; } catch { /* unreadable */ }
89
+ if (name === 'mem' || name === 'memhouse') fs.rmSync(dst, { recursive: true });
90
+ else throw new Error(`skills/mem in ${dir} belongs to plugin '${name || '(unreadable manifest)'}' — refusing to replace it`);
91
+ }
92
+ fs.mkdirSync(dst, { recursive: true });
93
+ fs.cpSync(path.join(deliveryDir, 'plugin'), dst, { recursive: true });
94
+ // Stamp the manifest with THIS memhouse's version. The source plugin.json carries a
95
+ // FROZEN number — it sat at 0.11.0 through several releases, so every install
96
+ // advertised the wrong version (`memhouse --version` said one thing, the plugin
97
+ // another). The package version is the single truth; write it into the copy.
98
+ try {
99
+ const m = JSON.parse(fs.readFileSync(dstManifest, 'utf-8'));
100
+ if (m.version !== version) {
101
+ m.version = version;
102
+ fs.writeFileSync(dstManifest, `${JSON.stringify(m, null, 2)}\n`);
103
+ }
104
+ } catch { /* manifest unreadable — leave the copied one rather than guess */ }
105
+ return dst;
106
+ }
107
+
108
+ const real = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
109
+
110
+ /** The installed copy's version, or null. */
111
+ function copyVersion(dir) {
112
+ try { return JSON.parse(fs.readFileSync(path.join(dir, PLUGIN_MARK), 'utf-8')).version || null; } catch { return null; }
113
+ }
114
+
115
+ /** The dir's binding to a memhouse home, or null (memhouse/binding.js). */
116
+ function boundHome(dir) {
117
+ const { boundTo, readSettings } = require('./binding');
118
+ const b = boundTo(readSettings(dir));
119
+ return b && b.home ? b.home : null;
120
+ }
121
+
122
+ /**
123
+ * Copies bound to `home` and older than `version`.
124
+ * @returns {{dir: string, have: string|null}[]}
125
+ */
126
+ function staleCopies({ home, version, targets = claudeTargets() }) {
127
+ const out = [];
128
+ for (const t of targets) {
129
+ if (!isPluginInstalled(t.dir)) continue;
130
+ const bound = boundHome(t.dir);
131
+ if (!bound || real(bound) !== real(home)) continue;
132
+ const have = copyVersion(t.dir);
133
+ const c = compareVersions(have, version);
134
+ if (have === null || c === -1) out.push({ dir: t.dir, have });
135
+ }
136
+ return out;
137
+ }
138
+
139
+ /** Is this memhouse running from a git checkout rather than an installed package? */
140
+ function fromCheckout(repoRoot) {
141
+ return fs.existsSync(path.join(repoRoot, '.git'));
142
+ }
143
+
144
+ /**
145
+ * On a new version's first run: refresh the stale copies bound to this home. Cheap when the
146
+ * stamp already names this version. Never throws; returns what it refreshed.
147
+ * @returns {{refreshed: {dir: string, have: string|null}[], skipped: string|null, errors: string[]}}
148
+ */
149
+ function refreshIfStale({ home, version, deliveryDir, repoRoot, env = process.env, targets = null, codexHome = null, log = () => {} }) {
150
+ const res = { refreshed: [], skipped: null, errors: [] };
151
+ try {
152
+ if (fromCheckout(repoRoot) && env.MEMHOUSE_REFRESH_SKILLS !== '1') { res.skipped = 'checkout'; return res; }
153
+ const stamp = path.join(home, STAMP);
154
+ let seen = null; try { seen = fs.readFileSync(stamp, 'utf-8').trim(); } catch { /* first run */ }
155
+ if (seen === version) { res.skipped = 'stamped'; return res; }
156
+ for (const s of staleCopies({ home, version, targets: targets || claudeTargets() })) {
157
+ try { installInto(s.dir, { deliveryDir, version }); res.refreshed.push(s); log(`/mem skills in ${s.dir} refreshed for ${version}${s.have ? ` (were ${s.have})` : ''} — reload them in Claude Code: /reload-plugins`); }
158
+ catch (e) { res.errors.push(`${s.dir}: ${String(e.message || e).split('\n')[0]}`); }
159
+ }
160
+ const cxp = require('./codex-plugin');
161
+ const cx = cxp.staleCopy({ codexHome: codexHome || cxp.codexHome(env), home, version, compareVersions });
162
+ if (cx) {
163
+ try {
164
+ // The block is rewritten only where it still stands: one taken out by hand stays out.
165
+ cxp.install({ codexHome: cx.codexHome, home: cx.home, bin: cx.bin, version, deliveryDir, onlyWhereInstalled: true });
166
+ res.refreshed.push({ dir: cx.codexHome, have: cx.have, codex: true });
167
+ log(`the memhouse skill and block in ${cx.codexHome} refreshed for ${version}${cx.have ? ` (were ${cx.have})` : ''} — they load in the next Codex session`);
168
+ } catch (e) { res.errors.push(`${cx.codexHome}: ${String(e.message || e).split('\n')[0]}`); }
169
+ }
170
+ // Stamp only when every copy is current, so a failure is tried again on the next run.
171
+ if (!res.errors.length && fs.existsSync(home)) { try { fs.writeFileSync(stamp, `${version}\n`); } catch { /* home not writable: check again next run */ } }
172
+ } catch (e) { res.errors.push(String(e.message || e).split('\n')[0]); }
173
+ return res;
174
+ }
175
+
176
+ module.exports = { PLUGIN_MARK, STAMP, isPluginInstalled, compareVersions, claudeTargets, installInto, copyVersion, boundHome, staleCopies, fromCheckout, refreshIfStale };
@@ -27,12 +27,17 @@ const { MEMBER_PIN, assertUsableName, roomPattern } = require('./house/house');
27
27
  * SELECT, INSERT the shipper writes, everything else reads
28
28
  * ALTER, OPTIMIZE ensureSchema adds columns in place; doctor compacts
29
29
  * CREATE TABLE, DROP TABLE their own rooms on first ship; a rebuild on migrate
30
- * ROW POLICY * `share --only` scopes a grantee to a project or a session
30
+ * CREATE/ALTER/DROP ROW POLICY `share --only` scopes a grantee to a project or a session
31
+ *
32
+ * Not SHOW ROW POLICIES: held on any table, it makes all of system.row_policies readable (a
33
+ * SHOW privilege opens its system table, and that one is not filtered per table), so a member
34
+ * would list every other member's scoped shares. A share does not need it: memhouse keeps what it
35
+ * shared in the member's own meta, and the policies are created, replaced and dropped by name.
31
36
  */
32
37
  const ROOM_PRIVILEGES = [
33
38
  'SELECT', 'INSERT', 'ALTER', 'OPTIMIZE',
34
39
  'CREATE TABLE', 'DROP TABLE',
35
- 'CREATE ROW POLICY', 'ALTER ROW POLICY', 'DROP ROW POLICY', 'SHOW ROW POLICIES',
40
+ 'CREATE ROW POLICY', 'ALTER ROW POLICY', 'DROP ROW POLICY',
36
41
  ];
37
42
 
38
43
  function roomGrant(db, member) {
@@ -0,0 +1,24 @@
1
+ // What doctor says about a gap that a newer release might close (an unpriced model): whether a
2
+ // newer release exists on the channel this install follows, from the registry's dist-tags.
3
+ // Pure: the tags are given (bin/memhouse.js reads them once per doctor).
4
+
5
+ const chan = require('./channel');
6
+
7
+ /**
8
+ * @param {{version:string, pinned?:string|null, tags:Record<string,string>|null}} p
9
+ * @returns {{known:boolean, up:{target:string,channel:string}|null, channel:string|null}}
10
+ * `known` only when the registry answered and this install follows a tag on it.
11
+ */
12
+ function releaseState({ version, pinned = null, tags = null }) {
13
+ if (!tags) return { known: false, up: null, channel: null };
14
+ const channel = chan.pickChannel({ version, pinned, tags }).channel;
15
+ return { known: !!channel, up: chan.behind({ version, pinned, tags }), channel };
16
+ }
17
+
18
+ function pricingHint(state, version) {
19
+ if (state.up) return `memhouse update installs ${state.up.target} from '${state.up.channel}', which may price it`;
20
+ if (state.known) return `this is the latest release on '${state.channel}' (${version}) and it has no price for it: its cost stays unknown until a release adds one`;
21
+ return 'prices ship with each release: a newer one may add them (memhouse update), and on the latest one their cost stays unknown until a release does';
22
+ }
23
+
24
+ module.exports = { releaseState, pricingHint };
@@ -1,4 +1,4 @@
1
- // Resume: hand the pilot back into a session memhouse only ever read.
1
+ // Resume: hand the user back into a session memhouse only ever read.
2
2
  //
3
3
  // Nothing here needs a schema change. A stored session already carries everything a
4
4
  // resume needs: `session_id` is canonical `<source>:<native-id>` (ship.js:575) and
@@ -8,8 +8,8 @@
8
8
  // Two rules, both learned the hard way in this repo:
9
9
  //
10
10
  // 1. PRINT the command, never run it. A wrong cwd or a stale id does not fail — it
11
- // starts a NEW session, and the transcript the pilot wanted is still lost while the
12
- // tool reports success. The pilot pastes it, sees what it does, and owns the result.
11
+ // starts a NEW session, and the transcript the user wanted is still lost while the
12
+ // tool reports success. The user pastes it, sees what it does, and owns the result.
13
13
  //
14
14
  // 2. Never write a flag from memory. Round 10 exists because a field name was asserted
15
15
  // from a failed search rather than read from the source. Every entry below was read
@@ -101,7 +101,7 @@ function shq(s) {
101
101
  }
102
102
 
103
103
  /**
104
- * Resolve a stored session row into something the pilot can paste.
104
+ * Resolve a stored session row into something the user can paste.
105
105
  *
106
106
  * @param {{session_id: string, source?: string, folder?: string, origin?: string}} row
107
107
  * @returns {{ok: boolean, source: string, nativeId: string|null, folder: string,