memhouse 0.20.0 → 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/CHANGELOG.md +60 -0
- package/bin/memhouse.js +206 -26
- package/docs/guides/install.md +3 -0
- package/docs/guides/invite.md +7 -0
- package/docs/guides/search-and-resume.md +30 -0
- package/docs/guides/share.md +4 -0
- package/docs/releases/README.md +1 -0
- package/docs/releases/v0.20.1.md +114 -0
- package/memhouse/codex-plugin.js +194 -0
- package/memhouse/delivery/INVITATION.md.tpl +1 -1
- package/memhouse/delivery/codex/AGENTS-BLOCK.md +11 -0
- package/memhouse/delivery/codex/SKILL.md +50 -0
- package/memhouse/delivery/plugin/reference/HOUSE.md +7 -9
- package/memhouse/delivery/plugin/skills/access/SKILL.md +1 -1
- package/memhouse/flags.js +1 -1
- package/memhouse/help.js +37 -0
- package/memhouse/house/schema-choice.js +15 -6
- package/memhouse/invitation.js +8 -2
- package/memhouse/plugin-copies.js +13 -1
- package/memhouse/release-hint.js +24 -0
- package/memhouse/share.js +31 -1
- package/memhouse/version-notice.js +66 -0
- package/package.json +2 -2
- package/public/assets/{index-CQSJ3nhV.js → index-CcNnhfM3.js} +1 -1
- package/public/index.html +1 -1
- package/ui/src/lib/dashboard-token.js +7 -2
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# memhouse 0.20.1 — 2026-11-02
|
|
2
|
+
|
|
3
|
+
A patch release on 0.20.0, with no schema change. Install it with `memhouse update`, or
|
|
4
|
+
`npm install -g memhouse`.
|
|
5
|
+
|
|
6
|
+
- **An invite's password expires.** It stops working 7 days after the invite unless it is
|
|
7
|
+
installed first. A file that sits unused, or that was read where it should not have been,
|
|
8
|
+
stops working on its own.
|
|
9
|
+
- **Codex gets memhouse on request.** `memhouse plugins install codex` adds a short rules block
|
|
10
|
+
to Codex's `AGENTS.md` and a `memhouse` skill with the read recipes. `plugins remove codex`
|
|
11
|
+
takes out exactly those.
|
|
12
|
+
- **Clearer answers from the CLI:**
|
|
13
|
+
- `memhouse <command> --help` prints that command's help;
|
|
14
|
+
- a refused credential is named before anything is written;
|
|
15
|
+
- `share --list` also shows the shares you received;
|
|
16
|
+
- `members` tells a share from a member's own tables;
|
|
17
|
+
- `discover` and `update` stop reporting problems that belong to nobody.
|
|
18
|
+
- **A new minor version's first run says so,** and names the release notes to read.
|
|
19
|
+
|
|
20
|
+
## Read this before upgrading
|
|
21
|
+
|
|
22
|
+
Nothing in the house changes, and there is no migration. Three things behave differently:
|
|
23
|
+
|
|
24
|
+
- **Inviting: the password now expires.** `memhouse invite` sets the expiry on the new account
|
|
25
|
+
using the server's clock. The default is 7 days; `--expires <days>` takes 1 to 365. The
|
|
26
|
+
output, the invite file and `MEMHOUSE-INVITATION.md` all give the date. An invitee who
|
|
27
|
+
installs after that date is refused with "this invite expired at … ask whoever invited you
|
|
28
|
+
for a new one". To invite them again under the same name, remove the unused account first
|
|
29
|
+
with `DROP USER <name>`, as the administrator; their tables are kept and reused. Invites
|
|
30
|
+
made by 0.20.0 or earlier carry no expiry and keep working as before.
|
|
31
|
+
- **The first command after moving to a new minor version prints one line** to stderr. It
|
|
32
|
+
names the version it replaces and the release notes. Patch releases, downgrades and fresh
|
|
33
|
+
installs print nothing.
|
|
34
|
+
- **Codex is opt-in.** Nothing is written under `~/.codex` (or `CODEX_HOME`) unless you run
|
|
35
|
+
`memhouse plugins install codex`.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
memhouse update # then: memhouse status — every line a check mark
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Invites
|
|
42
|
+
|
|
43
|
+
- **The expiry is set by the server**, as `VALID UNTIL` on the new account, so it holds no
|
|
44
|
+
matter what the file says. `install --env` also checks the date in the file and refuses
|
|
45
|
+
before contacting the server.
|
|
46
|
+
- **Installing lifts it.** A password change removes the expiry, and so does
|
|
47
|
+
`--keep-password`, which keeps the invited password.
|
|
48
|
+
- **`invite --print-sql`** prints the same `ALTER USER … VALID UNTIL` statement for an
|
|
49
|
+
administrator who runs the plan by hand. The date is left as a placeholder, so the
|
|
50
|
+
statement does not parse until a real date is put in.
|
|
51
|
+
- **Re-inviting an unused name** used to fail with the install message, "give their
|
|
52
|
+
password". It now says the invite was never installed, gives its expiry, and names the
|
|
53
|
+
remedy.
|
|
54
|
+
|
|
55
|
+
## Codex
|
|
56
|
+
|
|
57
|
+
Codex puts `AGENTS.md` into every prompt and lists each skill by its name and description,
|
|
58
|
+
reading the body only when the skill is used. memhouse uses both, and only when asked:
|
|
59
|
+
|
|
60
|
+
- **The block** in `AGENTS.md` holds what must always apply:
|
|
61
|
+
- never read or print the env file or a `MEMHOUSE_*` value;
|
|
62
|
+
- `MEMHOUSE_URL` is the house and `MEMHOUSE_PORT` the dashboard;
|
|
63
|
+
- questions about past sessions go to the skill.
|
|
64
|
+
|
|
65
|
+
It sits between `<!-- memhouse:begin … -->` and `<!-- memhouse:end -->` markers. A reinstall
|
|
66
|
+
replaces it where it stands. Remove takes it out and leaves the rest of the file
|
|
67
|
+
byte-identical. A file that held only the block is deleted, unless it is a symlink: then its
|
|
68
|
+
target is emptied and the link kept.
|
|
69
|
+
- **The skill**, `skills/memhouse/`, carries the read recipes:
|
|
70
|
+
- `memhouse q` for search, sessions, activity by hour, spend, models, tools and machines;
|
|
71
|
+
- `--member` for a housemate's shared sessions;
|
|
72
|
+
- `memhouse room-sql` for anything else.
|
|
73
|
+
|
|
74
|
+
It is memhouse's only if it holds `.memhouse.json`. A `skills/memhouse` without that file is
|
|
75
|
+
never replaced or removed.
|
|
76
|
+
- **An instance with its own home or binary** is named in both, as `MEMHOUSE_HOME=<home>
|
|
77
|
+
<binary>`.
|
|
78
|
+
- **After an upgrade,** the first command refreshes an installed copy, as it does the `/mem`
|
|
79
|
+
copies. A block you took out by hand stays out.
|
|
80
|
+
- **The `/mem` skills are still for Claude Code.**
|
|
81
|
+
|
|
82
|
+
## Smaller changes
|
|
83
|
+
|
|
84
|
+
- **`memhouse <command> --help`** prints that command's entry. A command with no entry of its
|
|
85
|
+
own still gets the whole page. `memhouse q --help` lists the named queries.
|
|
86
|
+
- **A refused credential is named up front.** When ClickHouse refuses the credential (code
|
|
87
|
+
516, or 194 for a missing password), `install`, `invite` and `deploy --local` stop before
|
|
88
|
+
writing anything, with "could not authenticate as '<user>' at <url>".
|
|
89
|
+
- **`share --list` also lists the shares you received,** with the command that reads each
|
|
90
|
+
one. `--json` adds `received`.
|
|
91
|
+
- **`members` tells a share from a member's own tables:**
|
|
92
|
+
- own tables;
|
|
93
|
+
- each member whose sessions this account reads, scoped or full;
|
|
94
|
+
- any other grant.
|
|
95
|
+
|
|
96
|
+
`--json` adds `own`, `shares` and `other`.
|
|
97
|
+
- **`discover`** shows an unreachable `http://localhost:8123` as a plain line when nothing is
|
|
98
|
+
configured to use it, instead of a ✗.
|
|
99
|
+
- **`update` on a machine with no member install** (an administrator's machine) skips the
|
|
100
|
+
schema heal and migration check, instead of printing the default member's authentication
|
|
101
|
+
failures.
|
|
102
|
+
- **`doctor`** names the release channel when it says a release is the latest. Offline, it no
|
|
103
|
+
longer says the install is current.
|
|
104
|
+
- **Dashboard:** a browser that refuses sessionStorage keeps the access token in memory for
|
|
105
|
+
that page load, instead of showing the "open it with `memhouse dashboard`" page.
|
|
106
|
+
- **/mem skills:** the schema-2 raw-SQL recipe runs `memhouse room-sql <<'SQL'` directly. It
|
|
107
|
+
used to wrap the call in a shell function, so every call asked for approval.
|
|
108
|
+
|
|
109
|
+
## Deprecated
|
|
110
|
+
|
|
111
|
+
Unchanged from 0.20.0: passwords on the command line (`--password`, `--member-password`,
|
|
112
|
+
`--to-password`), and `MEMHOUSE_USER` / `MEMHOUSE_PASSWORD` as the admin for `migrate --v3-*`,
|
|
113
|
+
still work with a warning and are refused from the next minor release. See
|
|
114
|
+
[0.20.0's Deprecated](v0.20.0.md#deprecated).
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// `memhouse plugins install codex`: what Codex reads of memhouse, and only on request.
|
|
2
|
+
//
|
|
3
|
+
// Codex (0.165, checked against the request it sends) puts <codex home>/AGENTS.md into every
|
|
4
|
+
// prompt in full, and lists each skill under <codex home>/skills by its name and description,
|
|
5
|
+
// reading the body only when the skill is used. So the rules that must hold in every session go
|
|
6
|
+
// in a short block in AGENTS.md, and the read recipes in one skill, skills/memhouse.
|
|
7
|
+
//
|
|
8
|
+
// The block is marker-delimited. Install puts it at the top of the file, followed by one blank
|
|
9
|
+
// line, or creates the file with the block alone; a reinstall replaces it where it stands. Remove
|
|
10
|
+
// takes out the block and that one line, so the rest of the file is what it was, byte for byte;
|
|
11
|
+
// a file left empty is deleted, unless it is a symlink: then the target is emptied through it and
|
|
12
|
+
// the link stays. The skill directory carries an ownership file (OWNER_FILE): a
|
|
13
|
+
// skills/memhouse without it is somebody else's and is never replaced or removed.
|
|
14
|
+
//
|
|
15
|
+
// Nothing here writes under the Codex home except install, remove, and the refresh of a copy
|
|
16
|
+
// that install already put there (plugin-copies.js).
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
const SKILL = 'memhouse';
|
|
23
|
+
const OWNER_FILE = '.memhouse.json';
|
|
24
|
+
const BEGIN = '<!-- memhouse:begin';
|
|
25
|
+
const END = '<!-- memhouse:end -->';
|
|
26
|
+
const MARKERS = /^<!-- memhouse:(begin|end)\b[^\n]*$/gm;
|
|
27
|
+
|
|
28
|
+
/** The Codex home: CODEX_HOME when set, else ~/.codex. */
|
|
29
|
+
function codexHome(env = process.env) {
|
|
30
|
+
return env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
31
|
+
}
|
|
32
|
+
const agentsFile = (cx) => path.join(cx, 'AGENTS.md');
|
|
33
|
+
const skillDir = (cx) => path.join(cx, 'skills', SKILL);
|
|
34
|
+
const real = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Where the block sits in `text`: { start, end, version }, or null when there is none.
|
|
38
|
+
* Throws when the markers do not make exactly one block, so nothing is written over a file
|
|
39
|
+
* whose block was damaged by hand.
|
|
40
|
+
*/
|
|
41
|
+
function findBlock(text) {
|
|
42
|
+
const begins = []; const ends = [];
|
|
43
|
+
let m;
|
|
44
|
+
MARKERS.lastIndex = 0;
|
|
45
|
+
while ((m = MARKERS.exec(text))) (m[1] === 'begin' ? begins : ends).push(m.index);
|
|
46
|
+
if (!begins.length && !ends.length) return null;
|
|
47
|
+
if (begins.length !== 1 || ends.length !== 1 || ends[0] < begins[0]) {
|
|
48
|
+
throw new Error(`AGENTS.md holds ${begins.length} memhouse:begin and ${ends.length} memhouse:end markers, not one block — fix it by hand; nothing was changed`);
|
|
49
|
+
}
|
|
50
|
+
const nl = text.indexOf('\n', ends[0]);
|
|
51
|
+
const version = (/^<!-- memhouse:begin (\d+\.\d+\.\d+\S*)/.exec(text.slice(begins[0])) || [])[1] || null;
|
|
52
|
+
return { start: begins[0], end: nl === -1 ? text.length : nl + 1, version };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** `text` with `block` in it: replaced where it stands, or put first with one blank line after. */
|
|
56
|
+
function withBlock(text, block) {
|
|
57
|
+
const b = findBlock(text);
|
|
58
|
+
if (b) return text.slice(0, b.start) + block + text.slice(b.end);
|
|
59
|
+
return text ? `${block}\n${text}` : block;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** `text` without the block, and without the blank line install put after it. */
|
|
63
|
+
function withoutBlock(text) {
|
|
64
|
+
const b = findBlock(text);
|
|
65
|
+
if (!b) return text;
|
|
66
|
+
let rest = text.slice(b.end);
|
|
67
|
+
if (b.start === 0 && rest.startsWith('\n')) rest = rest.slice(1);
|
|
68
|
+
return text.slice(0, b.start) + rest;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A path for the text, with the home directory as ~. */
|
|
72
|
+
function shown(p) {
|
|
73
|
+
const h = os.homedir();
|
|
74
|
+
return p === h || p.startsWith(h + path.sep) ? `~${p.slice(h.length)}` : p;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A shell word: as is when it needs no quoting, else single-quoted. */
|
|
78
|
+
function shArg(s) {
|
|
79
|
+
return /^[A-Za-z0-9_@%+=:,./~-]+$/.test(s) ? s : `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The `memhouse` on PATH, resolved, or null. */
|
|
83
|
+
function memhouseOnPath(env = process.env) {
|
|
84
|
+
for (const d of String(env.PATH || '').split(path.delimiter).filter(Boolean)) {
|
|
85
|
+
const f = path.join(d, 'memhouse');
|
|
86
|
+
try { fs.accessSync(f, fs.constants.X_OK); return real(f); } catch { /* not here */ }
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* How the block and the skill name the command: `memhouse` for the default instance, else with
|
|
93
|
+
* MEMHOUSE_HOME and the binary spelled out, each only where it differs from the default.
|
|
94
|
+
*/
|
|
95
|
+
function invocation({ home, bin, defaultHome = path.join(os.homedir(), '.memhouse'), pathBin = memhouseOnPath() }) {
|
|
96
|
+
const parts = [];
|
|
97
|
+
if (home && real(home) !== real(defaultHome)) parts.push(`MEMHOUSE_HOME=${shArg(shown(home))}`);
|
|
98
|
+
parts.push(!bin || (pathBin && real(bin) === pathBin) ? 'memhouse' : shArg(shown(bin)));
|
|
99
|
+
return parts.join(' ');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function render(tpl, { mh, envFile }) {
|
|
103
|
+
return tpl.replace(/\{\{MH\}\}/g, mh).replace(/\{\{ENV_FILE\}\}/g, envFile);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function renderBlock({ deliveryDir, version, mh, envFile }) {
|
|
107
|
+
const body = render(fs.readFileSync(path.join(deliveryDir, 'codex', 'AGENTS-BLOCK.md'), 'utf-8'), { mh, envFile }).trim();
|
|
108
|
+
return `${BEGIN} ${version} — managed by \`memhouse plugins install codex\`; \`memhouse plugins remove codex\` takes it out -->\n${body}\n${END}\n`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The skill directory's ownership record, or null when it is not memhouse's (or absent). */
|
|
112
|
+
function readOwner(dir) {
|
|
113
|
+
try {
|
|
114
|
+
const o = JSON.parse(fs.readFileSync(path.join(dir, OWNER_FILE), 'utf-8'));
|
|
115
|
+
return o && o.owner === 'memhouse' ? o : null;
|
|
116
|
+
} catch { return null; }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Write the block and the skill. Everything is checked before anything is written: a foreign
|
|
121
|
+
* skills/memhouse or a damaged block throws with the Codex home untouched.
|
|
122
|
+
* `onlyWhereInstalled`: the refresh — the block is rewritten only where it already is.
|
|
123
|
+
* @returns {{ file: string, created: boolean, block: boolean, dir: string, mh: string }}
|
|
124
|
+
*/
|
|
125
|
+
function install({ codexHome: cx, home, bin, version, deliveryDir, pathBin, onlyWhereInstalled = false }) {
|
|
126
|
+
const dir = skillDir(cx);
|
|
127
|
+
if (fs.existsSync(dir) && !readOwner(dir)) {
|
|
128
|
+
throw new Error(`${shown(dir)} is not memhouse's (it has no ${OWNER_FILE}) — not replacing it; move it aside and install again`);
|
|
129
|
+
}
|
|
130
|
+
const file = agentsFile(cx);
|
|
131
|
+
let text = null;
|
|
132
|
+
try { text = fs.readFileSync(file, 'utf-8'); } catch (e) { if (e.code !== 'ENOENT') throw e; }
|
|
133
|
+
const mh = invocation({ home, bin, ...(pathBin !== undefined ? { pathBin } : {}) });
|
|
134
|
+
const envFile = shown(path.join(home, 'env'));
|
|
135
|
+
const writeBlock = !onlyWhereInstalled || (text !== null && findBlock(text) !== null);
|
|
136
|
+
const next = writeBlock ? withBlock(text || '', renderBlock({ deliveryDir, version, mh, envFile })) : text;
|
|
137
|
+
const skill = render(fs.readFileSync(path.join(deliveryDir, 'codex', 'SKILL.md'), 'utf-8'), { mh, envFile });
|
|
138
|
+
|
|
139
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
140
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
141
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), skill);
|
|
142
|
+
fs.writeFileSync(path.join(dir, OWNER_FILE), `${JSON.stringify({ owner: 'memhouse', version, home, bin: bin || null }, null, 2)}\n`);
|
|
143
|
+
// In place, not a rename: an AGENTS.md that is a symlink (a dotfiles checkout) stays one.
|
|
144
|
+
if (writeBlock && next !== text) fs.writeFileSync(file, next);
|
|
145
|
+
return { file, created: text === null && writeBlock, block: writeBlock, dir, mh };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Take out the block and the skill — the skill only when it is memhouse's.
|
|
150
|
+
* @returns {{ block: boolean, fileDeleted: boolean, emptiedLink: boolean, skill: boolean, foreignSkill: string|null }}
|
|
151
|
+
*/
|
|
152
|
+
function remove({ codexHome: cx }) {
|
|
153
|
+
const res = { block: false, fileDeleted: false, emptiedLink: false, skill: false, foreignSkill: null };
|
|
154
|
+
const file = agentsFile(cx);
|
|
155
|
+
let text = null;
|
|
156
|
+
try { text = fs.readFileSync(file, 'utf-8'); } catch (e) { if (e.code !== 'ENOENT') throw e; }
|
|
157
|
+
const next = text !== null && findBlock(text) ? withoutBlock(text) : null;
|
|
158
|
+
const dir = skillDir(cx);
|
|
159
|
+
const owned = !!readOwner(dir);
|
|
160
|
+
if (!owned && fs.existsSync(dir)) res.foreignSkill = dir;
|
|
161
|
+
if (next !== null) {
|
|
162
|
+
// Only a regular file is unlinked. Unlinking a symlink (a dotfiles checkout) would leave the
|
|
163
|
+
// block in its target, so the target is written through the link instead.
|
|
164
|
+
if (next === '' && !fs.lstatSync(file).isSymbolicLink()) { fs.rmSync(file); res.fileDeleted = true; }
|
|
165
|
+
else { fs.writeFileSync(file, next); res.emptiedLink = next === ''; }
|
|
166
|
+
res.block = true;
|
|
167
|
+
}
|
|
168
|
+
if (owned) { fs.rmSync(dir, { recursive: true }); res.skill = true; }
|
|
169
|
+
return res;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** What is installed: the skill's owner record (or why not), and whether the block is there. */
|
|
173
|
+
function status({ codexHome: cx }) {
|
|
174
|
+
const dir = skillDir(cx);
|
|
175
|
+
const owner = readOwner(dir);
|
|
176
|
+
let block = null; let damaged = null;
|
|
177
|
+
try { block = findBlock(fs.readFileSync(agentsFile(cx), 'utf-8')); } catch (e) { if (e.code !== 'ENOENT') damaged = e.message; }
|
|
178
|
+
return { codexHome: cx, installed: !!owner, owner, foreignSkill: !owner && fs.existsSync(dir) ? dir : null, block: !!block, blockVersion: block ? block.version : null, damaged };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The copy to refresh for `version`, or null: installed, memhouse's, bound to `home`, and older.
|
|
183
|
+
*/
|
|
184
|
+
function staleCopy({ codexHome: cx, home, version, compareVersions }) {
|
|
185
|
+
const owner = readOwner(skillDir(cx));
|
|
186
|
+
if (!owner || !owner.home || real(owner.home) !== real(home)) return null;
|
|
187
|
+
const c = compareVersions(owner.version, version);
|
|
188
|
+
return c === -1 || c === null ? { codexHome: cx, have: owner.version || null, home: owner.home, bin: owner.bin || null } : null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
SKILL, OWNER_FILE, BEGIN, END, codexHome, agentsFile, skillDir, findBlock, withBlock, withoutBlock,
|
|
193
|
+
invocation, memhouseOnPath, renderBlock, readOwner, install, remove, status, staleCopy, shown,
|
|
194
|
+
};
|
|
@@ -46,7 +46,7 @@ them exist because an agent broke them:
|
|
|
46
46
|
|
|
47
47
|
## 0. Find your credential
|
|
48
48
|
|
|
49
|
-
The install needs `{{FILE}}`, the file delivered with this page. Confirm it is here first —
|
|
49
|
+
{{EXPIRY}}The install needs `{{FILE}}`, the file delivered with this page. Confirm it is here first —
|
|
50
50
|
run this from the directory this page is in now, and if it prints the filename you are ready:
|
|
51
51
|
|
|
52
52
|
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## memhouse: past agent sessions
|
|
2
|
+
|
|
3
|
+
This machine's agent sessions (Codex, Claude Code and other editors) are kept in a memhouse
|
|
4
|
+
house. For a question about earlier work (what was done, when, in which hours, what it cost,
|
|
5
|
+
how something was fixed), use the `memhouse` skill before saying you do not know.
|
|
6
|
+
|
|
7
|
+
- Never read, print or echo `{{ENV_FILE}}`, and never print a `MEMHOUSE_*` value: they hold the
|
|
8
|
+
house's password, and this conversation may be shipped into the house. `{{MH}}` reads its
|
|
9
|
+
own configuration.
|
|
10
|
+
- `MEMHOUSE_URL` is the house (ClickHouse). `MEMHOUSE_PORT` is the local dashboard's port, not
|
|
11
|
+
the house's.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memhouse
|
|
3
|
+
description: "Answer from past agent sessions kept in memhouse (Codex, Claude Code and other editors): what was worked on and when, which hours or days, spend and models, tools used, how something was fixed, a past session's transcript, a housemate's shared sessions. Read-only memhouse commands. Use before saying you do not know about earlier work."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# memhouse: read past sessions
|
|
7
|
+
|
|
8
|
+
Every command here only reads. `{{MH}}` reads its own configuration: never read or print
|
|
9
|
+
`{{ENV_FILE}}` or any `MEMHOUSE_*` value.
|
|
10
|
+
|
|
11
|
+
Start with `{{MH}} q schema`. The recipes below are for schema 3. On 2 (a house still on the
|
|
12
|
+
0.18 rooms), `{{MH}} prompt` prints the recipes for it.
|
|
13
|
+
|
|
14
|
+
## Named queries
|
|
15
|
+
|
|
16
|
+
`{{MH}} q <name> --json` prints one JSON object. A session is `<harness>:<session_id>`, the
|
|
17
|
+
`ref` every result carries.
|
|
18
|
+
|
|
19
|
+
- Sessions about a topic (every term in one message; a "quoted phrase" as written):
|
|
20
|
+
`{{MH}} q search clickhouse "authentication failed" --json`
|
|
21
|
+
- Around a hit: `{{MH}} q window <ref> --at <message_id> --json`. A whole session:
|
|
22
|
+
`{{MH}} q session <ref> --json`, paged with `--cursor`.
|
|
23
|
+
- Recent sessions: `{{MH}} q recent --limit 20 --since 7d --json`
|
|
24
|
+
- Which hours or days: `{{MH}} q activity --by hour --grain usage --tz <IANA zone> --json`
|
|
25
|
+
counts messages and tokens by the hour they happened; `--by day`, `--by dow`, `--by day,host`.
|
|
26
|
+
- Spend and models: `{{MH}} q spend --by model --since 30d --json`, `{{MH}} q models --json`
|
|
27
|
+
- Tools: `{{MH}} q tools --limit 25 --json`; one tool's calls: `{{MH}} q tool <name> --json`
|
|
28
|
+
- Machines: `{{MH}} q hosts --json`. What the house holds: `{{MH}} q coverage --json`
|
|
29
|
+
- Filters on most names: `--harness`, `--project`, `--folder`, `--host`, `--since`, `--until`.
|
|
30
|
+
`{{MH}} q` lists every name and flag.
|
|
31
|
+
|
|
32
|
+
## A housemate's sessions
|
|
33
|
+
|
|
34
|
+
Add `--member <name>` to a named query to read sessions a housemate shared with you.
|
|
35
|
+
`{{MH}} share --list` names who shared with you. An empty result means nothing is visible,
|
|
36
|
+
not that nothing happened.
|
|
37
|
+
|
|
38
|
+
## Raw SQL, when no named query fits
|
|
39
|
+
|
|
40
|
+
`{{MH}} room-sql "<SELECT …>"` runs one read-only statement. The bare names `sessions`,
|
|
41
|
+
`session_parts`, `session_usage` and `calls` resolve to your tables (`--member <name>` for a
|
|
42
|
+
housemate's). Read them `FINAL` with `is_deleted = 0 AND removed = 0`; a session is
|
|
43
|
+
`(host, harness, harness_path, session_id, incarnation)`; take totals from `sessions`, never by
|
|
44
|
+
summing parts or usage.
|
|
45
|
+
|
|
46
|
+
## Answering
|
|
47
|
+
|
|
48
|
+
Answer from what the commands return, cite the `ref` of each session used, and say so when a
|
|
49
|
+
search finds nothing. `{{MH}} resume <ref>` prints the command that reopens a session. Cost is
|
|
50
|
+
derived from token counts: an unpriced model is unknown, not free.
|
|
@@ -20,8 +20,8 @@ memhouse q schema # prints 3 or 2
|
|
|
20
20
|
- **3** — the house holds harness-sessions-sql's profile tables (`sessions`, `session_parts`,
|
|
21
21
|
`session_usage`, `calls`; memhouse 0.20). Read
|
|
22
22
|
[Schema 3](#schema-3--harness-sessions-sqls-profile) below and use **`memhouse q`**; nothing about
|
|
23
|
-
epochs, `seq` or the
|
|
24
|
-
need raw SQL, and then use `memhouse room-sql
|
|
23
|
+
epochs, `seq` or the 0.18 rooms' name rewrite applies. Skip everything from "Connect" on
|
|
24
|
+
unless you need raw SQL, and then use `memhouse room-sql`.
|
|
25
25
|
- **2** — the 0.18 rooms (`messages`, `tool_calls`, `sessions`). Everything from
|
|
26
26
|
"What this is" on is for you, unchanged.
|
|
27
27
|
|
|
@@ -161,8 +161,8 @@ Every member's rooms are named for them — `mem.alice_messages`, `mem.alice_ses
|
|
|
161
161
|
`mem.alice_tool_calls` — and one grant covers exactly those: `ON mem.alice_*`. Housemates
|
|
162
162
|
keep their own rooms beside yours and cannot read them, list them, or grant them onward;
|
|
163
163
|
you cannot read theirs. `SHOW GRANTS` shows you the one line you hold. **A bare
|
|
164
|
-
`FROM messages` names nothing you have** —
|
|
165
|
-
member name for you, and `memhouse rooms` prints what yours are called.
|
|
164
|
+
`FROM messages` names nothing you have** — `memhouse room-sql` (below) prefixes room names
|
|
165
|
+
with your member name for you, and `memhouse rooms` prints what yours are called.
|
|
166
166
|
|
|
167
167
|
An **operator** owns the ClickHouse itself; an **admin credential** is what proves it.
|
|
168
168
|
`memhouse whoami` says which you are holding.
|
|
@@ -189,13 +189,11 @@ instances on two channels; the top of this file says when to use it instead of p
|
|
|
189
189
|
Credentials resolve **flags > exported `MEMHOUSE_*` > `$MEMHOUSE_HOME/env`** (default
|
|
190
190
|
`~/.memhouse/env`), and the CLI does that itself: you never source the file, and the
|
|
191
191
|
password never appears in a command. Every read goes through `memhouse room-sql`, with the
|
|
192
|
-
SQL on stdin:
|
|
192
|
+
SQL on stdin. Run it as written, not through a shell function: the skills pre-approve
|
|
193
|
+
`memhouse room-sql`, and a wrapper would ask for approval every time.
|
|
193
194
|
|
|
194
195
|
```bash
|
|
195
|
-
|
|
196
|
-
memhouse room-sql "$@"
|
|
197
|
-
}
|
|
198
|
-
q <<'SQL'
|
|
196
|
+
memhouse room-sql <<'SQL'
|
|
199
197
|
SELECT 1 FORMAT PrettyCompact
|
|
200
198
|
SQL
|
|
201
199
|
```
|
|
@@ -116,7 +116,7 @@ each one:
|
|
|
116
116
|
memhouse share <user> everything you own
|
|
117
117
|
memhouse share <user> --only project=memhouse just one project
|
|
118
118
|
memhouse share <user> --only session=<id> just one conversation
|
|
119
|
-
memhouse share --list who can read it, and
|
|
119
|
+
memhouse share --list who can read it, how much, and whose you can read
|
|
120
120
|
memhouse share <user> --revoke withdraw
|
|
121
121
|
```
|
|
122
122
|
|
package/memhouse/flags.js
CHANGED
|
@@ -43,7 +43,7 @@ const COMMAND_FLAGS = {
|
|
|
43
43
|
update: ['check', 'migrate', 'no-install', 'channel'],
|
|
44
44
|
reset: ['all-origins'],
|
|
45
45
|
invite: ['url', 'db', 'out', 'admin-user', 'admin-password-file', 'adopt', 'print-sql',
|
|
46
|
-
'allow-local', 'member-password', 'member-password-file', 'schema'],
|
|
46
|
+
'allow-local', 'member-password', 'member-password-file', 'schema', 'expires'],
|
|
47
47
|
passwd: ['password', 'password-file', 'admin-user', 'admin-password-file', 'print-sql', 'member'],
|
|
48
48
|
nightly: ['out'],
|
|
49
49
|
migrate: ['dry-run', 'v3-prepare', 'v3-convert', 'v3-reconcile', 'v3-shares', 'v3-switch', 'member', 'batch-rows', 'force-after-dedupe',
|
package/memhouse/help.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// `memhouse <command> --help`: that command's entries from the full help (bin/memhouse.js HELP),
|
|
2
|
+
// not the whole page. The full help stays the one text: an entry starts in column 13 (after the
|
|
3
|
+
// section label or 13 spaces), and its continuation lines are indented further. Pure.
|
|
4
|
+
|
|
5
|
+
const COL = 13;
|
|
6
|
+
|
|
7
|
+
/** The entries of `help`: [{ words, lines }], `words` being the command names the entry is for. */
|
|
8
|
+
function entries(help) {
|
|
9
|
+
const out = [];
|
|
10
|
+
let cur = null;
|
|
11
|
+
for (const line of String(help).split('\n')) {
|
|
12
|
+
const m = /^([A-Za-z]*)( +)(\S.*)$/.exec(line);
|
|
13
|
+
if (m && m[1].length + m[2].length === COL) {
|
|
14
|
+
// `start | stop | status …` is one entry for three commands.
|
|
15
|
+
const name = m[3].split(/\s{2,}/)[0];
|
|
16
|
+
const words = /^[a-z][a-z-]*( \| [a-z][a-z-]*)+/.test(name)
|
|
17
|
+
? name.split(/\s*\|\s*/).map((w) => w.split(/\s+/)[0]).filter(Boolean)
|
|
18
|
+
: [name.split(/\s+/)[0]];
|
|
19
|
+
cur = { words, lines: [`${' '.repeat(COL)}${m[3]}`] };
|
|
20
|
+
out.push(cur);
|
|
21
|
+
} else if (cur && /^ {20,}\S/.test(line)) {
|
|
22
|
+
cur.lines.push(line);
|
|
23
|
+
} else {
|
|
24
|
+
cur = null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The help for `cmd`, or null when no entry names it (the caller prints the full help). */
|
|
31
|
+
function helpFor(help, cmd, version) {
|
|
32
|
+
const mine = entries(help).filter((e) => e.words.includes(cmd));
|
|
33
|
+
if (!mine.length) return null;
|
|
34
|
+
return [`memhouse ${version} — ${cmd}`, '', ...mine.flatMap((e) => e.lines), '', 'Every command: memhouse --help'].join('\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { entries, helpFor };
|
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
const sqlStr = (s) => `'${String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
8
8
|
|
|
9
|
-
// ClickHouse's AUTHENTICATION_FAILED
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// ClickHouse's AUTHENTICATION_FAILED, and REQUIRED_PASSWORD (a user that needs a password was
|
|
10
|
+
// given none). Told by the code, which the HTTP wrappers carry as `chCode` from the
|
|
11
|
+
// X-ClickHouse-Exception-Code header (bin/memhouse.js ch(), q-cli.js httpTransport), never by its
|
|
12
|
+
// prose: a credential the server refused is not a house whose tables are unreadable.
|
|
12
13
|
const AUTHENTICATION_FAILED = 516;
|
|
13
|
-
|
|
14
|
+
const REQUIRED_PASSWORD = 194;
|
|
15
|
+
const AUTH_CODES = new Set([AUTHENTICATION_FAILED, REQUIRED_PASSWORD]);
|
|
16
|
+
function isAuthFailure(e) { return AUTH_CODES.has(Number(e && e.chCode)); }
|
|
14
17
|
/** What every caller says when it is one; the caller adds what to set. */
|
|
15
18
|
function authFailureText(user, url) {
|
|
16
19
|
return `could not authenticate${user ? ` as '${user}'` : ''}${url ? ` at ${url}` : ''}`;
|
|
@@ -88,7 +91,13 @@ function devLayoutRefusal(det, member) {
|
|
|
88
91
|
return `the house holds ${member}_turns, the layout of memhouse 0.20's development builds, which harness-sessions-sql's profile replaces — its admin recaptures or converts '${member}' first (docs/guides/upgrade-to-0.20.md)${notice ? `. ${notice}` : ''}`;
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
|
|
94
|
+
/**
|
|
95
|
+
* @param {{user?:string, url?:string}} [who] the credential the detection connected as, to name a refusal
|
|
96
|
+
*/
|
|
97
|
+
function chooseSchema(stated, det, member, who = {}) {
|
|
98
|
+
// A credential the server refused saw nothing: say that, rather than reading the house as new
|
|
99
|
+
// and letting the next step fail on the same credential.
|
|
100
|
+
if (det.authFailed) return { refuse: `${authFailureText(who.user, who.url)} — check the user and password, then run this again`, auth: true };
|
|
92
101
|
// Checked before the member's own tables: an --ensure-schema that laid the profile beside
|
|
93
102
|
// <m>_turns must not make the development layout look converted.
|
|
94
103
|
const dev = devLayoutRefusal(det, member);
|
|
@@ -109,4 +118,4 @@ function chooseSchema(stated, det, member) {
|
|
|
109
118
|
return { schema: '3' }; // a new house
|
|
110
119
|
}
|
|
111
120
|
|
|
112
|
-
module.exports = { detectHouseSchema, chooseSchema, importsNotice, importWritesProfile, devLayoutRefusal, isAuthFailure, authFailureText, AUTHENTICATION_FAILED };
|
|
121
|
+
module.exports = { detectHouseSchema, chooseSchema, importsNotice, importWritesProfile, devLayoutRefusal, isAuthFailure, authFailureText, AUTHENTICATION_FAILED, REQUIRED_PASSWORD };
|
package/memhouse/invitation.js
CHANGED
|
@@ -84,13 +84,19 @@ function render(tpl, values) {
|
|
|
84
84
|
* says. `schema` is the layout the invite provisioned ('3', anything else for the 0.18 rooms);
|
|
85
85
|
* `install` is step 1's block and `version` what `memhouse --version` should print.
|
|
86
86
|
*/
|
|
87
|
-
|
|
87
|
+
/** When the invite's password stops working, or '' for an invite with no expiry. */
|
|
88
|
+
function expiryLine(expires, inviter) {
|
|
89
|
+
return expires ? `**This invitation expires at ${expires} (UTC).** Its password stops working then; install before that, or ask ${inviter} for a new one.\n\n` : '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function invitationPage({ out, name, url, db, schema, version, install, inviter, template, expires = null }) {
|
|
88
93
|
const file = path.basename(out);
|
|
89
94
|
const text = render(template === undefined ? readTemplate() : template, {
|
|
90
95
|
NAME: name, FILE: file, FILE_ARG: shellWord(file), URL: url, DB: db, VERSION: version,
|
|
91
96
|
INSTALL: install, INVITER: inviter, JOIN: joinStep(file), OWNED: ownedTables({ db, name, schema }),
|
|
97
|
+
EXPIRY: expiryLine(expires, inviter),
|
|
92
98
|
});
|
|
93
99
|
return { path: path.join(path.dirname(out), PAGE), file, text };
|
|
94
100
|
}
|
|
95
101
|
|
|
96
|
-
module.exports = { PAGE, TEMPLATE, readTemplate, shellWord, ownedTables, joinStep, render, invitationPage };
|
|
102
|
+
module.exports = { PAGE, TEMPLATE, readTemplate, shellWord, ownedTables, joinStep, expiryLine, render, invitationPage };
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
//
|
|
11
11
|
// Only copies BOUND TO THIS HOME are touched: a machine can run two instances on two versions,
|
|
12
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.
|
|
13
15
|
// From a git checkout nothing happens unless MEMHOUSE_REFRESH_SKILLS=1: a checkout run with a
|
|
14
16
|
// real MEMHOUSE_HOME would otherwise copy work-in-progress skills over the installed ones.
|
|
15
17
|
|
|
@@ -144,7 +146,7 @@ function fromCheckout(repoRoot) {
|
|
|
144
146
|
* stamp already names this version. Never throws; returns what it refreshed.
|
|
145
147
|
* @returns {{refreshed: {dir: string, have: string|null}[], skipped: string|null, errors: string[]}}
|
|
146
148
|
*/
|
|
147
|
-
function refreshIfStale({ home, version, deliveryDir, repoRoot, env = process.env, targets = null, log = () => {} }) {
|
|
149
|
+
function refreshIfStale({ home, version, deliveryDir, repoRoot, env = process.env, targets = null, codexHome = null, log = () => {} }) {
|
|
148
150
|
const res = { refreshed: [], skipped: null, errors: [] };
|
|
149
151
|
try {
|
|
150
152
|
if (fromCheckout(repoRoot) && env.MEMHOUSE_REFRESH_SKILLS !== '1') { res.skipped = 'checkout'; return res; }
|
|
@@ -155,6 +157,16 @@ function refreshIfStale({ home, version, deliveryDir, repoRoot, env = process.en
|
|
|
155
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`); }
|
|
156
158
|
catch (e) { res.errors.push(`${s.dir}: ${String(e.message || e).split('\n')[0]}`); }
|
|
157
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
|
+
}
|
|
158
170
|
// Stamp only when every copy is current, so a failure is tried again on the next run.
|
|
159
171
|
if (!res.errors.length && fs.existsSync(home)) { try { fs.writeFileSync(stamp, `${version}\n`); } catch { /* home not writable: check again next run */ } }
|
|
160
172
|
} catch (e) { res.errors.push(String(e.message || e).split('\n')[0]); }
|
|
@@ -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 };
|
package/memhouse/share.js
CHANGED
|
@@ -716,5 +716,35 @@ function parseShareRecord(value) {
|
|
|
716
716
|
return m ? { granted: true, scopeRaw: m[1] ? m[1].trim() : null } : { granted: false, scopeRaw: null };
|
|
717
717
|
}
|
|
718
718
|
|
|
719
|
+
/**
|
|
720
|
+
* One account's grants from membersAccess, told apart for a reader: its own tables, the tables
|
|
721
|
+
* of each member it reads (a share), and anything else (a hand-made grant, a wildcard reaching
|
|
722
|
+
* past its own name). A table belongs to the member whose name is left after the longest
|
|
723
|
+
* memhouse table name (MEMBER_TABLES) is taken off its end. `policies` are the row policies the
|
|
724
|
+
* reading credential can list ([{ short_name, table }]); null when it cannot, and then a share's
|
|
725
|
+
* scope is unknown.
|
|
726
|
+
* @returns {{ own: string[], shares: {from:string, tables:string[], scoped:boolean|null}[], other: string[] }}
|
|
727
|
+
*/
|
|
728
|
+
function describeAccess({ u, list }, { policies = null } = {}) {
|
|
729
|
+
const own = []; const other = []; const shared = new Map();
|
|
730
|
+
for (const label of list || []) {
|
|
731
|
+
const bare = String(label).replace(/ \(through role [^)]*\)$/, '');
|
|
732
|
+
const name = bare.includes('.') ? bare.slice(bare.lastIndexOf('.') + 1) : bare;
|
|
733
|
+
if (name === `${u}_*` || name === `${u}_`) { own.push(label); continue; }
|
|
734
|
+
const type = [...MEMBER_TABLES, 'session_units', 'session_sources'].filter((t) => name.endsWith(`_${t}`) && name.length > t.length + 1).sort((a, b) => b.length - a.length)[0];
|
|
735
|
+
const owner = type ? name.slice(0, -(type.length + 1)) : null;
|
|
736
|
+
if (owner === u) own.push(label);
|
|
737
|
+
else if (owner && !bare.includes('.') && V3_TABLES.concat(LEGACY_ROOMS).includes(type)) {
|
|
738
|
+
if (!shared.has(owner)) shared.set(owner, []);
|
|
739
|
+
shared.get(owner).push(label);
|
|
740
|
+
} else other.push(label);
|
|
741
|
+
}
|
|
742
|
+
const shares = [...shared.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([from, tables]) => ({
|
|
743
|
+
from, tables,
|
|
744
|
+
scoped: policies ? policies.some((p) => String(p.short_name).startsWith(`mh_share_${u}_`) && String(p.table).startsWith(`${from}_`)) : null,
|
|
745
|
+
}));
|
|
746
|
+
return { own, shares, other };
|
|
747
|
+
}
|
|
748
|
+
|
|
719
749
|
module.exports = { SCOPE_COLUMNS, parseScope, scopePredicate, policyName, V3_TABLES, scopePredicateV3, shareV3Plan, revokeV3Plan, parseShareRecord,
|
|
720
|
-
assertName, ident, qual, memberTargets, moveSharePlan, wildcardRevoke, memberCollisions, wildcardOnRevoke, holdsMemberGrant, collidingUsers, ownPolicies, showPoliciesRevokes, wildcardReachText, wildcardAdminLine, shareGate, selectTargets, rolesGranted, reachThroughRoles, revokeRoleReport, revokeCheck, classifyShare, legacyPolicyDrops, readLiveShares, membersGrantsSQL, MEMBERS_ROLE_GRANTS_SQL, membersAccess, roleDifferences, LEGACY_ROOMS };
|
|
750
|
+
assertName, ident, qual, memberTargets, moveSharePlan, wildcardRevoke, memberCollisions, wildcardOnRevoke, holdsMemberGrant, collidingUsers, ownPolicies, showPoliciesRevokes, wildcardReachText, wildcardAdminLine, shareGate, selectTargets, rolesGranted, reachThroughRoles, revokeRoleReport, revokeCheck, classifyShare, legacyPolicyDrops, readLiveShares, membersGrantsSQL, MEMBERS_ROLE_GRANTS_SQL, membersAccess, describeAccess, roleDifferences, LEGACY_ROOMS };
|