clavarium-binder 0.1.1 → 0.1.2-next.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/README.md CHANGED
@@ -37,6 +37,8 @@ way, universal (nothing in it is specific to Clavarium).
37
37
  it, or any state after it, only once a person ticked it. An agent proposes,
38
38
  and works on what was approved.
39
39
  - **See what it was not given.** Access is given per list: read, or change.
40
+ It may make a list of its own from a template (`binder new batches`): given
41
+ to it alone, and seen by a person as any other; a few at most.
40
42
  - **Delete.** It archives; a person deletes, and deleted is deleted.
41
43
 
42
44
  ## Without an application
package/bin/binder.mjs CHANGED
@@ -23,6 +23,8 @@ export const HELP = `binder: the lists you were given, and their records.
23
23
  binder attach ID KIND REF ["title"] KIND: message, file, asset, branch, link, record
24
24
  binder detach ID WHAT
25
25
  binder archive ID | binder restore ID
26
+ binder new TEMPLATE ["title"] a list of your own, when none you were given fits
27
+ TEMPLATE: batches, development, customers, people, roadmap, simple
26
28
  binder serve [--store DIR] [--port N] [--side FOLDER=NAME]
27
29
  keep a binder yourself, with its page (no application)
28
30
 
@@ -33,7 +35,7 @@ You never approve: a person ticks what may go on. Talking with them stays in the
33
35
  The folder used is --dir FOLDER, else $BINDER_DIR, else the nearest ${FOLDER} folder from here up.
34
36
  binder ${VERSION}`;
35
37
 
36
- const VERBS = ['lists', 'show', 'get', 'search', 'add', 'edit', 'move', 'note', 'attach', 'detach', 'archive', 'restore'];
38
+ const VERBS = ['lists', 'show', 'get', 'search', 'add', 'edit', 'move', 'note', 'attach', 'detach', 'archive', 'restore', 'new'];
37
39
 
38
40
  /* A command line, read: [op, args] or a refusal. */
39
41
  export function read(argv) {
@@ -62,6 +64,7 @@ export function read(argv) {
62
64
  case 'note': return rest.length >= 2 ? ['note', { id: rest[0], text: rest.slice(1).join(' ') }] : { refused: 'binder note ID "text"' };
63
65
  case 'attach': return rest.length >= 3 ? ['attach', { id: rest[0], kind: rest[1], ref: rest[2], title: rest.slice(3).join(' ') }] : { refused: 'binder attach ID KIND REF ["title"]' };
64
66
  case 'detach': return rest.length === 2 ? ['detach', { id: rest[0], what: rest[1] }] : { refused: 'binder detach ID WHAT' };
67
+ case 'new': return rest[0] ? ['create-list', { template: rest[0], title: rest.slice(1).join(' ') || undefined }] : { refused: 'binder new TEMPLATE ["title"]' };
65
68
  case 'archive': case 'restore': return rest[0] ? ['archive', { id: rest[0], restore: verb === 'restore' }] : { refused: `binder ${verb} ID` };
66
69
  default: return { refused: 'not understood' };
67
70
  }
package/docs/protocol.md CHANGED
@@ -49,9 +49,12 @@ person, what the record is and why; Markdown-ish (paragraphs, `- ` lists,
49
49
  `**bold**`, `[text](https://...)` and bare http(s) links), 4000 characters at
50
50
  most, empty to remove it. It is in the history like any other change.
51
51
 
52
- A person's alone (from the screen): `approve`, `delete`, `create-list`,
53
- `edit-list`, `grant`, `archive-list`, `delete-list`; a note is changed by
54
- whoever wrote it (`edit-note`).
52
+ A person's alone (from the screen): `approve`, `delete`, `edit-list`,
53
+ `grant`, `archive-list`, `delete-list`; a note is changed by whoever wrote it
54
+ (`edit-note`). `create-list` is a person's, and an agent's from a template
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; five such lists not
57
+ archived per agent at most.
55
58
 
56
59
  ## The application's side
57
60
 
@@ -64,6 +67,10 @@ whoever wrote it (`edit-note`).
64
67
 
65
68
  And the screen: `mount(element, { call, words, lang, people, picture, openAttached })`
66
69
  from `clavarium-binder/ui`, with `ui/binder-ui.css` and `ui/texts/*.json`.
70
+ It gives back `refresh()`, `destroy()`, and `open(id)`: one record shown
71
+ whole, in its list, from elsewhere on the page (a list beside a chat);
72
+ it resolves `false`, saying nothing on the screen, for a record not in a
73
+ list shown.
67
74
 
68
75
  What a card shows: what is set, nothing about what is empty. The fields set
69
76
  make one line under its title; its state is said unless the card is in the
@@ -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 'create-list': return `Made the list ${data.title} [${data.id}], yours to change: binder add ${data.id} "title".`;
74
75
  default: return 'Done.';
75
76
  }
76
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clavarium-binder",
3
- "version": "0.1.1",
3
+ "version": "0.1.2-next.1",
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.1';
26
+ export const VERSION = '0.1.2-next.1';
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;
@@ -73,6 +73,7 @@ export const TOOLS = [
73
73
  { name: 'note', description: 'Add a short note to a record: a decision, a fact, a result. Talking with the person stays in the chat.', inputSchema: { type: 'object', properties: { id: { type: 'string' }, text: { type: 'string' } }, required: ['id', 'text'] } },
74
74
  { name: 'attach', description: 'Attach something to a record: a message of the chat, a file, an asset, a branch, a link, another record.', inputSchema: { type: 'object', properties: { id: { type: 'string' }, kind: { type: 'string', enum: ['message', 'file', 'asset', 'branch', 'link', 'record'] }, ref: S('what it is: a path, a URL, repository:branch, a message id, a record number'), title: { type: 'string' } }, required: ['id', 'kind', 'ref'] } },
75
75
  { name: 'detach', description: 'Take back something attached to a record.', inputSchema: { type: 'object', properties: { id: { type: 'string' }, what: S('what get shows of it: its ref or its id') }, required: ['id', 'what'] } },
76
+ { name: 'new-list', description: 'Make a list from a template (batches, development, customers, people, roadmap, simple) when none you were given fits: it is given to you alone, and the person sees it. Batches: the work you propose; the person ticks what you may start.', inputSchema: { type: 'object', properties: { template: S('batches, development, customers, people, roadmap or simple'), title: S('its name; the template\'s by default') }, required: ['template'] } },
76
77
  { name: 'archive', description: 'Archive a record (it leaves the list; found again with archived), or restore it.', inputSchema: { type: 'object', properties: { id: { type: 'string' }, restore: { type: 'boolean' } }, required: ['id'] } },
77
78
  ];
78
79
  /* A tool's arguments as the keeper reads them. */
@@ -90,6 +91,7 @@ export function asked(name, a = {}) {
90
91
  case 'attach': return ['attach', { id: a.id, kind: a.kind, ref: a.ref, title: a.title }];
91
92
  case 'detach': return ['detach', { id: a.id, what: a.what }];
92
93
  case 'archive': return ['archive', { id: a.id, restore: !!a.restore }];
94
+ case 'new-list': return ['create-list', { template: a.template, title: a.title }];
93
95
  default: return null;
94
96
  }
95
97
  }
package/server/store.mjs CHANGED
@@ -15,7 +15,8 @@
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.
18
+ * person approved it. An agent may make a list from a template (a few at
19
+ * most): it is given that list alone, to change, and a person sees it as any.
19
20
  */
20
21
  import { readFileSync, writeFileSync, appendFileSync, renameSync, mkdirSync, existsSync, lstatSync } from 'node:fs';
21
22
  import { join } from 'node:path';
@@ -23,6 +24,8 @@ import { randomUUID } from 'node:crypto';
23
24
  import { seed, TEMPLATES, VIEWS, FIELD_TYPES } from './templates.mjs';
24
25
 
25
26
  export const TITLE_MAX = 200;
27
+ /* Lists an agent may have made and not archived. */
28
+ export const AGENT_LISTS_MAX = 5;
26
29
  export const NOTE_MAX = 2000;
27
30
  /* A record's description: a few lines for a person, what it is and why (Markdown-ish: paragraphs, "- " lists, **bold**, links). */
28
31
  export const DESCRIPTION_MAX = 4000;
@@ -163,16 +166,19 @@ export function openStore(dir, { now = () => Date.now() } = {}) {
163
166
  return out;
164
167
  },
165
168
 
166
- /* ---- lists: a person's ---- */
169
+ /* ---- lists: a person's; an agent makes one too, from a template, given to itself alone ---- */
167
170
  createList({ template = 'simple', title, lang = 'en' } = {}, who) {
168
171
  const w = whoOf(who);
169
- if (!isPerson(w)) no('only a person makes a list');
170
172
  if (Object.keys(s.lists).length >= LISTS_MAX) no(`${LISTS_MAX} lists at most`);
173
+ if (!isPerson(w)) {
174
+ if (!Object.hasOwn(TEMPLATES, String(template))) no(`a list is made from a template: ${Object.keys(TEMPLATES).join(', ')}`);
175
+ if (Object.values(s.lists).filter((l) => l.by === w.id && !l.archived).length >= AGENT_LISTS_MAX) no(`${AGENT_LISTS_MAX} lists made by you at most: use one of yours`);
176
+ }
171
177
  const base = seed(template, lang);
172
178
  const name = oneLine(title, TITLE_MAX) || base.title;
173
179
  let id = key(name) || 'list';
174
180
  for (let i = 2; s.lists[id]; i++) id = `${key(name) || 'list'}-${i}`;
175
- s.lists[id] = { id, ...base, title: name, access: {}, created: stamp(), by: w.id };
181
+ s.lists[id] = { id, ...base, title: name, access: isPerson(w) ? {} : { [w.id]: 'write' }, created: stamp(), by: w.id };
176
182
  save(); log(w, 'create-list', { list: id, change: { title: name, template } });
177
183
  return s.lists[id];
178
184
  },
package/ui/binder-ui.js CHANGED
@@ -11,6 +11,7 @@
11
11
  * openAttached: (a) => void, a thing attached opened by the page (a message, a file...)
12
12
  * });
13
13
  * b.refresh(); something changed: read again
14
+ * b.open(id); a record shown whole, in its list (resolves false when it is not in a list shown)
14
15
  * b.destroy();
15
16
  *
16
17
  * Built element by element: nothing a record holds is ever read as HTML.
@@ -491,5 +492,20 @@ export function mount(root, { call, words = {}, lang = 'en', people = () => [],
491
492
  document.addEventListener('pointerdown', away);
492
493
  document.addEventListener('keydown', escape);
493
494
  void refresh();
494
- return { refresh, destroy() { alive = false; document.removeEventListener('pointerdown', away); document.removeEventListener('keydown', escape); box.remove(); }, get state() { return { at: ui.at, mode: ui.mode } } };
495
+ /* One record opened, from elsewhere on the page (a list beside a chat): its list, then it. */
496
+ async function openRecord(id) {
497
+ if (!alive) return false;
498
+ try {
499
+ /* Asked quietly: a record not found says nothing on the screen. */
500
+ const got = await call('get', { id: String(id ?? '') });
501
+ if (!got?.ok) return false;
502
+ const d = got.data;
503
+ if (!ui.lists.some((l) => l.id === d.list.id)) await load();
504
+ if (!ui.lists.some((l) => l.id === d.list.id)) return false;
505
+ if (ui.at !== d.list.id) { ui.at = d.list.id; ui.view = null; ui.data = await ask('show', { list: ui.at, archived: ui.archived, mine: ui.mine }); }
506
+ leave(); ui.record = d; ui.mode = 'record'; paint();
507
+ return true;
508
+ } catch { return false; }
509
+ }
510
+ return { refresh, open: openRecord, destroy() { alive = false; document.removeEventListener('pointerdown', away); document.removeEventListener('keydown', escape); box.remove(); }, get state() { return { at: ui.at, mode: ui.mode } } };
495
511
  }