clavarium-binder 0.1.2-next.1 → 0.1.2
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/docs/protocol.md +18 -2
- package/host/binder-host.mjs +3 -0
- package/package.json +1 -1
- package/server/binder.mjs +1 -1
- package/server/store.mjs +34 -5
- package/ui/binder-ui.css +1 -0
- package/ui/binder-ui.js +14 -3
- package/ui/texts/en.json +1 -0
- package/ui/texts/fr.json +1 -0
package/docs/protocol.md
CHANGED
|
@@ -53,8 +53,24 @@ A person's alone (from the screen): `approve`, `delete`, `edit-list`,
|
|
|
53
53
|
`grant`, `archive-list`, `delete-list`; a note is changed by whoever wrote it
|
|
54
54
|
(`edit-note`). `create-list` is a person's, and an agent's from a template
|
|
55
55
|
(`binder new TEMPLATE ["title"]`, MCP `new-list`): the list is given to that
|
|
56
|
-
agent alone, to change, and a person sees it as any;
|
|
57
|
-
archived
|
|
56
|
+
agent alone, to change, and a person sees it as any; per agent, as many such
|
|
57
|
+
lists not archived as the store's setting `agentLists` says.
|
|
58
|
+
|
|
59
|
+
## The store's settings
|
|
60
|
+
|
|
61
|
+
A person's alone to change; anyone reads them. Each change is a line of the
|
|
62
|
+
log, op `edit-settings`, with what changed (`change`), and no list or record.
|
|
63
|
+
|
|
64
|
+
| Setting | What it is | Range | By default |
|
|
65
|
+
|---|---|---|---|
|
|
66
|
+
| `agentLists` | lists an agent may have made and not archived | 0 to 50 (0: an agent makes none) | 5 |
|
|
67
|
+
|
|
68
|
+
| Op (keeper) | Args | Gives |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `settings` | none | `{agentLists}` |
|
|
71
|
+
| `edit-settings` | `{agentLists}` | `{agentLists}`; an agent is refused |
|
|
72
|
+
|
|
73
|
+
An agent at the limit is refused, and told the number.
|
|
58
74
|
|
|
59
75
|
## The application's side
|
|
60
76
|
|
package/host/binder-host.mjs
CHANGED
|
@@ -71,6 +71,7 @@ export function say(op, data, list) {
|
|
|
71
71
|
case 'attach': return 'Attached.';
|
|
72
72
|
case 'detach': return 'Taken off.';
|
|
73
73
|
case 'archive': return data.archived ? `Archived #${data.id}.` : `Restored #${data.id}.`;
|
|
74
|
+
case 'settings': case 'edit-settings': return `Lists an agent may make: ${data.agentLists}.`;
|
|
74
75
|
case 'create-list': return `Made the list ${data.title} [${data.id}], yours to change: binder add ${data.id} "title".`;
|
|
75
76
|
default: return 'Done.';
|
|
76
77
|
}
|
|
@@ -105,6 +106,8 @@ export function keeper(dir, { sides = () => [], now = () => Date.now() } = {}) {
|
|
|
105
106
|
case 'grant': data = store.grant(a.list, a.to, a.access ?? null, who); break;
|
|
106
107
|
case 'archive-list': data = store.archiveList(a.list, a.archived !== false, who); break;
|
|
107
108
|
case 'delete-list': data = store.deleteList(a.list, who); break;
|
|
109
|
+
case 'settings': data = store.settings(who); break;
|
|
110
|
+
case 'edit-settings': data = store.editSettings({ agentLists: a.agentLists }, who); break;
|
|
108
111
|
case 'templates': data = Object.fromEntries(Object.entries(TEMPLATES).map(([k, t]) => [k, t.name])); break;
|
|
109
112
|
default: return { ok: false, text: `there is no ${oneLine(op, 40)}` };
|
|
110
113
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clavarium-binder",
|
|
3
|
-
"version": "0.1.2
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Clavarium Binder: lists of records kept together by people and AI agents, each with its id (a command and an MCP server for agents, a keeper and a screen for the application). No dependency.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/clavarium/clavarium-binder#readme",
|
package/server/binder.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
23
23
|
import { randomUUID } from 'node:crypto';
|
|
24
24
|
import { createInterface } from 'node:readline';
|
|
25
25
|
|
|
26
|
-
export const VERSION = '0.1.2
|
|
26
|
+
export const VERSION = '0.1.2';
|
|
27
27
|
export const FOLDER = '.binder';
|
|
28
28
|
/* How long an ask waits for its answer. The keeper answers within a second or two. */
|
|
29
29
|
export const WAIT_MS = 20_000;
|
package/server/store.mjs
CHANGED
|
@@ -15,8 +15,12 @@
|
|
|
15
15
|
* WHO is { id, kind, name }: kind "person" or "agent". A person may do
|
|
16
16
|
* anything. An agent reads or writes a list only where it was given access, and
|
|
17
17
|
* never approves: a record reaches a gate state (or one after it) only once a
|
|
18
|
-
* person approved it. An agent may make a list from a template (
|
|
19
|
-
*
|
|
18
|
+
* person approved it. An agent may make a list from a template (as many as a
|
|
19
|
+
* person set, 5 unless they said): it is given that list alone, to change, and
|
|
20
|
+
* a person sees it as any.
|
|
21
|
+
*
|
|
22
|
+
* SETTINGS: the store's own, a person's alone to change, in the history too.
|
|
23
|
+
* agentLists lists an agent may have made and not archived (0 to 50; 0: none)
|
|
20
24
|
*/
|
|
21
25
|
import { readFileSync, writeFileSync, appendFileSync, renameSync, mkdirSync, existsSync, lstatSync } from 'node:fs';
|
|
22
26
|
import { join } from 'node:path';
|
|
@@ -24,8 +28,11 @@ import { randomUUID } from 'node:crypto';
|
|
|
24
28
|
import { seed, TEMPLATES, VIEWS, FIELD_TYPES } from './templates.mjs';
|
|
25
29
|
|
|
26
30
|
export const TITLE_MAX = 200;
|
|
27
|
-
/* Lists an agent may have made and not archived. */
|
|
28
|
-
export const
|
|
31
|
+
/* Lists an agent may have made and not archived: 5 until a person sets it (settings.agentLists), 0 to 50. */
|
|
32
|
+
export const AGENT_LISTS_DEFAULT = 5;
|
|
33
|
+
export const AGENT_LISTS_TOP = 50;
|
|
34
|
+
/* The default, by its former name. */
|
|
35
|
+
export const AGENT_LISTS_MAX = AGENT_LISTS_DEFAULT;
|
|
29
36
|
export const NOTE_MAX = 2000;
|
|
30
37
|
/* A record's description: a few lines for a person, what it is and why (Markdown-ish: paragraphs, "- " lists, **bold**, links). */
|
|
31
38
|
export const DESCRIPTION_MAX = 4000;
|
|
@@ -117,6 +124,9 @@ export function openStore(dir, { now = () => Date.now() } = {}) {
|
|
|
117
124
|
count: Object.values(s.records).filter((r) => r.list === l.id && !r.archived).length });
|
|
118
125
|
|
|
119
126
|
/* Whether a record's title, description or notes hold `q` (lower case). */
|
|
127
|
+
/* The store's settings, as they are now (a store from before them has the defaults). */
|
|
128
|
+
const settingsNow = () => ({ agentLists: Number.isInteger(s.settings?.agentLists) ? s.settings.agentLists : AGENT_LISTS_DEFAULT });
|
|
129
|
+
|
|
120
130
|
const said = (r, q) => r.title.toLowerCase().includes(q) || (r.description ?? '').toLowerCase().includes(q) || r.notes.some((n) => n.text.toLowerCase().includes(q));
|
|
121
131
|
|
|
122
132
|
const api = {
|
|
@@ -172,7 +182,9 @@ export function openStore(dir, { now = () => Date.now() } = {}) {
|
|
|
172
182
|
if (Object.keys(s.lists).length >= LISTS_MAX) no(`${LISTS_MAX} lists at most`);
|
|
173
183
|
if (!isPerson(w)) {
|
|
174
184
|
if (!Object.hasOwn(TEMPLATES, String(template))) no(`a list is made from a template: ${Object.keys(TEMPLATES).join(', ')}`);
|
|
175
|
-
|
|
185
|
+
const most = settingsNow().agentLists;
|
|
186
|
+
if (most === 0) no('a person set 0 lists an agent may make: use one you were given');
|
|
187
|
+
if (Object.values(s.lists).filter((l) => l.by === w.id && !l.archived).length >= most) no(`${most} lists made by you at most, as a person set: use one of yours`);
|
|
176
188
|
}
|
|
177
189
|
const base = seed(template, lang);
|
|
178
190
|
const name = oneLine(title, TITLE_MAX) || base.title;
|
|
@@ -247,6 +259,23 @@ export function openStore(dir, { now = () => Date.now() } = {}) {
|
|
|
247
259
|
return true;
|
|
248
260
|
},
|
|
249
261
|
|
|
262
|
+
/* ---- the store's settings: anyone reads them, a person changes them ---- */
|
|
263
|
+
settings(who) { whoOf(who); return settingsNow(); },
|
|
264
|
+
editSettings({ agentLists } = {}, who) {
|
|
265
|
+
const w = whoOf(who);
|
|
266
|
+
if (!isPerson(w)) no('only a person changes the settings');
|
|
267
|
+
const change = {};
|
|
268
|
+
if (agentLists !== undefined) {
|
|
269
|
+
const n = typeof agentLists === 'string' && agentLists.trim() !== '' ? Number(agentLists) : agentLists;
|
|
270
|
+
if (!Number.isInteger(n) || n < 0 || n > AGENT_LISTS_TOP) no(`lists an agent may make: a whole number from 0 to ${AGENT_LISTS_TOP}`);
|
|
271
|
+
change.agentLists = n;
|
|
272
|
+
}
|
|
273
|
+
if (!Object.keys(change).length) return settingsNow();
|
|
274
|
+
s.settings = { ...s.settings, ...change };
|
|
275
|
+
save(); log(w, 'edit-settings', { change });
|
|
276
|
+
return settingsNow();
|
|
277
|
+
},
|
|
278
|
+
|
|
250
279
|
/* ---- records ---- */
|
|
251
280
|
add(given, { title, description, state, fields, owners, due, note } = {}, who) {
|
|
252
281
|
const w = whoOf(who);
|
package/ui/binder-ui.css
CHANGED
|
@@ -149,6 +149,7 @@ button.binder-who:hover span:not(.binder-dot) { text-decoration: underline; text
|
|
|
149
149
|
.binder-history { display: flex; flex-direction: column; gap: .3rem; margin-bottom: 1rem; }
|
|
150
150
|
.binder-box { border: 1px solid var(--binder-line); border-radius: var(--binder-radius); background: var(--binder-panel); padding: .7rem .9rem; display: flex; flex-direction: column; gap: .55rem; min-width: 0; }
|
|
151
151
|
.binder-box h3 { font-size: .75rem; letter-spacing: .06em; text-transform: uppercase; color: var(--binder-dim); display: flex; justify-content: space-between; }
|
|
152
|
+
.binder-box input[data-setting] { width: 6em; align-self: flex-start; }
|
|
152
153
|
.binder-log { color: var(--binder-dim); font-size: .85em; }
|
|
153
154
|
.binder-log time { font-variant-numeric: tabular-nums; margin-right: .6em; }
|
|
154
155
|
.binder .binder-warn { color: var(--binder-danger); }
|
package/ui/binder-ui.js
CHANGED
|
@@ -22,7 +22,7 @@ export const KEYS = Object.freeze(['lists', 'new-list', 'new-item', 'no-lists',
|
|
|
22
22
|
'states', 'fields', 'access', 'read', 'write', 'none', 'save', 'empty', 'archived', 'show-archived', 'gate', 'done', 'add-state', 'add-field',
|
|
23
23
|
'kind-message', 'kind-file', 'kind-asset', 'kind-branch', 'kind-link', 'kind-record', 'what', 'name', 'type', 'saved', 'edited', 'by', 'mine', 'all',
|
|
24
24
|
'delete-list', 'delete-list-sure', 'archive-list', 'no-records', 'search', 'description', 'edit', 'more', 'approve', 'add', 'ok',
|
|
25
|
-
'add-what', 'owner', 'attachment', 'tools', 'cover',
|
|
25
|
+
'add-what', 'owner', 'attachment', 'tools', 'cover', 'agent-lists',
|
|
26
26
|
'op-add', 'op-edit', 'op-move', 'op-note', 'op-edit-note', 'op-remove-note', 'op-attach', 'op-detach', 'op-archive', 'op-restore', 'op-delete']);
|
|
27
27
|
const KINDS = ['message', 'file', 'asset', 'branch', 'link', 'record'];
|
|
28
28
|
const ICON = { message: '💬', file: '📄', asset: '🗄', branch: '⑂', link: '🔗', record: '▣' };
|
|
@@ -162,7 +162,7 @@ export function rich(text) {
|
|
|
162
162
|
export function mount(root, { call, words = {}, lang = 'en', people = () => [], picture = () => null, openAttached = null } = {}) {
|
|
163
163
|
const w = (k) => words[k] ?? k;
|
|
164
164
|
/* open: what of a record is being changed ('states', 'who', 'description', 'attach'); draft: what was typed, kept across a repaint. */
|
|
165
|
-
const ui = { lists: [], at: null, data: null, view: null, record: null, mode: 'list', archived: false, mine: false, error: '', open: null, more: false, draft: {}, focus: false };
|
|
165
|
+
const ui = { lists: [], at: null, data: null, view: null, record: null, mode: 'list', archived: false, mine: false, error: '', open: null, more: false, draft: {}, focus: false, settings: null };
|
|
166
166
|
const box = h('div', { class: 'binder' });
|
|
167
167
|
root.replaceChildren(box);
|
|
168
168
|
let alive = true;
|
|
@@ -194,7 +194,7 @@ export function mount(root, { call, words = {}, lang = 'en', people = () => [],
|
|
|
194
194
|
const menu = ui.open === 'tools' ? h('div', { class: 'binder-menu binder-menu-end', role: 'menu' },
|
|
195
195
|
item(ui.mine, w('mine'), 'mine', () => flip('mine')),
|
|
196
196
|
item(ui.archived, w('archived'), 'archived', () => flip('archived')),
|
|
197
|
-
l.access === 'write' && l.grants ? item(null, w('settings'), 'settings', () => { ui.open = null; ui.mode = 'settings'; paint(); }) : null) : null;
|
|
197
|
+
l.access === 'write' && l.grants ? item(null, w('settings'), 'settings', () => { ui.open = null; ui.mode = 'settings'; paint(); void readSettings(); }) : null) : null;
|
|
198
198
|
const chip = (k) => (ui[k] ? h('button', { type: 'button', class: 'binder-chip', 'aria-pressed': 'true', 'data-act': `off-${k}`, onclick: () => flip(k) }, w(k === 'mine' ? 'mine' : 'archived'), h('span', { 'aria-hidden': 'true' }, ' ×')) : null);
|
|
199
199
|
return h('div', { class: 'binder-bar' },
|
|
200
200
|
h('h2', {}, l.title), chip('mine'), chip('archived'),
|
|
@@ -423,6 +423,11 @@ export function mount(root, { call, words = {}, lang = 'en', people = () => [],
|
|
|
423
423
|
attached, addRow, notes, more);
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
+
/* The store's settings, read when its page opens; asked quietly (a keeper without them shows none). */
|
|
427
|
+
async function readSettings() {
|
|
428
|
+
try { const r = await call('settings', {}); ui.settings = r?.ok ? r.data : null; } catch { ui.settings = null; }
|
|
429
|
+
if (ui.mode === 'settings') paint();
|
|
430
|
+
}
|
|
426
431
|
function settings(l) {
|
|
427
432
|
const title = h('input', { type: 'text', value: l.title, 'aria-label': w('title') });
|
|
428
433
|
const states = l.states.map((s) => ({ ...s }));
|
|
@@ -449,12 +454,18 @@ export function mount(root, { call, words = {}, lang = 'en', people = () => [],
|
|
|
449
454
|
const sure = h('span', { class: 'binder-sure', hidden: true }, w('delete-list-sure'),
|
|
450
455
|
h('button', { type: 'button', class: 'binder-danger', onclick: () => void soft(ask('delete-list', { list: l.id })).then(() => { ui.mode = 'list'; ui.at = null; return refresh(); }) }, w('delete')),
|
|
451
456
|
h('button', { type: 'button', class: 'binder-ghost', onclick: () => { sure.hidden = true; } }, w('cancel')));
|
|
457
|
+
function agentLists() {
|
|
458
|
+
const n = h('input', { type: 'number', min: '0', max: '50', step: '1', inputmode: 'numeric', value: String(ui.settings.agentLists), 'aria-label': w('agent-lists'), 'data-setting': 'agentLists' });
|
|
459
|
+
n.addEventListener('change', () => void soft(ask('edit-settings', { agentLists: n.value })).then((got) => { if (got) ui.settings = got; paint(); }));
|
|
460
|
+
return n;
|
|
461
|
+
}
|
|
452
462
|
return h('div', { class: 'binder-page binder-settings' },
|
|
453
463
|
h('div', { class: 'binder-bar' }, h('button', { type: 'button', class: 'binder-ghost', onclick: () => { ui.mode = 'list'; paint(); } }, `‹ ${l.title}`)),
|
|
454
464
|
h('section', { class: 'binder-box' }, h('h3', {}, w('title')), title),
|
|
455
465
|
h('section', { class: 'binder-box' }, h('h3', {}, w('states')), stateRows),
|
|
456
466
|
h('section', { class: 'binder-box' }, h('h3', {}, w('fields')), fieldRows),
|
|
457
467
|
grants.length ? h('section', { class: 'binder-box' }, h('h3', {}, w('access')), grants) : null,
|
|
468
|
+
ui.settings ? h('section', { class: 'binder-box' }, h('h3', {}, w('agent-lists')), agentLists()) : null,
|
|
458
469
|
h('div', { class: 'binder-actions' },
|
|
459
470
|
h('button', { type: 'button', class: 'binder-ghost', onclick: () => void soft(ask('archive-list', { list: l.id })).then(() => { ui.mode = 'list'; ui.at = null; return refresh(); }) }, w('archive-list')),
|
|
460
471
|
h('button', { type: 'button', class: 'binder-ghost', onclick: () => { sure.hidden = false; } }, w('delete-list')), sure,
|
package/ui/texts/en.json
CHANGED