clavarium-parley 0.0.1 → 0.9.0

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 The Clavarium authors
3
+ Copyright (c) 2026 The Clavarium Parley authors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,7 +1,178 @@
1
- # clavarium-parley
1
+ # Clavarium Parley
2
2
 
3
- Clavarium Parley: a chat between a person and an AI agent (MCP + CLI).
3
+ A bridge between a command-line agent and a screen of its own.
4
4
 
5
- This package only keeps the name: the tool is not published yet. Running it prints this and exits. It runs nothing else and reaches no network.
5
+ A command-line agent lives in a terminal. Whoever does not read a terminal
6
+ cannot talk with it. Parley gives the agent seven verbs, as a command and as
7
+ an MCP server, and gives an application a small library that shows the
8
+ exchange as a conversation, on any screen.
6
9
 
7
- License: MIT.
10
+ It is part of the Clavarium family and needs nothing of Clavarium: any
11
+ application, any agent that can run a command.
12
+
13
+ ## The seven verbs
14
+
15
+ | The agent runs | To |
16
+ |---|---|
17
+ | `parley tell [--short "title"] "text"` | say where it is, what it did, in Markdown; a long text with its short version |
18
+ | `parley ask "question" [choice ...]` | ask, with or without choices; it does not wait |
19
+ | `parley show path ["note"]` | point to a file it made; several at once, each with its title: `parley show a.png="Home, before" b.png="Home, after"`, one gallery |
20
+ | `parley doing "text"` | say in one line what it is doing now, at each new step |
21
+ | `parley read` | read what was written to it, answers and attached files included, and its questions still open |
22
+ | `parley close ID "answer"` | close a question the person answered another way |
23
+ | `parley read --all` | the whole exchange, both sides: to pick up the thread after a restart |
24
+ | `parley status working\|waiting\|done ["note"]` | say where it is |
25
+
26
+ `parley --help` says all of it to the agent. The same seven verbs are an MCP
27
+ server (`parley-mcp`, or `node server/parley-mcp.mjs --dir FOLDER`) for an
28
+ agent that is given it that way; both write the same files.
29
+
30
+ Nothing waits: the agent posts, and is told in one line when a message
31
+ arrived. See [docs/protocol.md](docs/protocol.md).
32
+
33
+ ## Parley announces, the application decides
34
+
35
+ | Event, to `on(event)` | Carries | An application may |
36
+ |---|---|---|
37
+ | `person-wrote` | the message, how many the agent has not read | type `WAKE` in the agent's terminal, now or later |
38
+ | `agent-said` | the message | show it |
39
+ | `agent-asked` | the question and its choices | show buttons, ring a phone |
40
+ | `agent-showed` | the path, and how to open it (`open`, or `null`) | show a link, or say it is not in a shared folder |
41
+ | `agent-status` | `working`, `waiting` or `done`, and a note | show it; ring a phone on `waiting` |
42
+ | `agent-doing` | one line: what it is doing now | show it while it works; nothing more |
43
+ | `person-reacted` | the reaction, and whether the set says it wakes (`false`, `true`, `'now'`) | tell the agent now, later, or with the next message |
44
+
45
+ | Action | Does |
46
+ |---|---|
47
+ | `say(text, answers, files, {via})` | writes what the person said (`answers`: the message it answers or replies to; `via: 'voice'`: dictated) |
48
+ | `react(to, reaction)` | puts a reaction of the set on a message of the agent, or takes it back (`null`) |
49
+ | `read()` | the exchange: messages, open questions, status, steps (states and what it did, in order), files, unread |
50
+ | `invite(lang, { install, shared })` | the text that asks an agent to use Parley, in English, saying the person's language |
51
+
52
+ Parley types nothing anywhere. `WAKE` (`You have a new message: run "parley
53
+ read".`) and the invitation are texts an application uses as they
54
+ are, changes, or does not use.
55
+
56
+ ## The pieces
57
+
58
+ | Piece | File | What it is |
59
+ |---|---|---|
60
+ | The agent's side | `server/parley-mcp.mjs`, `bin/parley.mjs` | the seven verbs, as an MCP server and as a command; no dependency |
61
+ | The application's side | `host/parley-host.mjs` | reads and writes the files, cleans what the agent wrote, announces events |
62
+ | The screen | `ui/parley-ui.js`, `ui/parley-ui.css` | the exchange in two views, simple and detailed; no framework |
63
+ | The words | `ui/texts/en.json`, `ui/texts/fr.json` | one dictionary per language |
64
+ | The icons | `ui/parley-icons.js` | the Lucide icons used, as data, with their version and licence (ISC; MIT for those of Feather); `icon(name)`, `fileIcon(name)` for an application's own buttons too |
65
+
66
+ ## Use it
67
+
68
+ import { install, Parley, WAKE, texts } from 'clavarium-parley/host';
69
+
70
+ install('/path/to/work/.parley'); // the folder, once
71
+ const parley = new Parley('/path/to/work/.parley', {
72
+ on: (event) => { /* person-wrote: get WAKE to the agent, your way; agent-asked: alert, or not */ },
73
+ reach: (path) => (path.startsWith('/work/') ? path : null), // what the person can open
74
+ });
75
+ parley.listen();
76
+ typeInTheAgentsTerminal(parley.invite('fr', { install: 'npm install -g /path/to/clavarium-parley-0.9.0.tgz', shared: '/work' }));
77
+
78
+ and in a page:
79
+
80
+ import { mount } from 'clavarium-parley/ui';
81
+ mount(element, { load, send, words: texts('fr', operatorsOwn), open, terminal, view: 'simple',
82
+ voice: yourDictation, replies: yourReadyAnswers });
83
+
84
+ The command is installed from the archive `npm pack` makes, by the
85
+ application or by the agent: `npm install -g clavarium-parley-0.9.0.tgz`,
86
+ then `parley --help`. Installing it runs no script of its own. It finds
87
+ its folder as git finds a repository: the nearest `.parley` from where it
88
+ stands, or `--dir`, or `$PARLEY_DIR`.
89
+
90
+ ## The screen
91
+
92
+ Two views of one exchange, and a person picks one:
93
+
94
+ | | Simple | Detailed |
95
+ |---|---|---|
96
+ | Messages | the agent's across a reading column, rendered from Markdown; the person's in bubbles on the right, each with a sign of where it is (sent, read, worked on; handled: no sign); the time on hover, or a long press | the same, each with its time, every sign |
97
+ | Long messages | the short version first, or folded after about twelve lines; its IMPORTANT box in sight | unfolded |
98
+ | While it works | one line, moving: what it does now; gone when its next message comes; what came before it one click away | the same line, and every step of this turn above it |
99
+ | Steps it went through | folded into the message they led to: a click on it opens them ("5 steps" is said with its time) | unfolded between the messages, each with its time and state |
100
+ | Where it is | when it waits for the person: once, at the end of the thread, until they write; done: once, quietly, while it is the last thing that happened | the same; at the top, what it does now, or the state it said when the end does not say it; on the side, what it does now and its machine (`about`, given by the application) |
101
+ | Its newest message | never folded nor cut: nothing seems left below it | unfolded |
102
+ | Reactions | on its messages: a button on hover, a right click or a long press; eight, each with its word | the same |
103
+ | Pictures | in their place, their title under them; several shown at once, one gallery; large: grown out of the thumbnail, zoomed, a strip of the others, closed with a tap; "Media and files" in the menu | the same, with each file's name |
104
+ | Files | the ones the person can open, by name | every one, with its path; one that cannot be opened is said so |
105
+ | Its terminal | | a way to it |
106
+
107
+ In both, the day is said when it changes, and the time when long passed
108
+ between two messages, as a chat does.
109
+
110
+ ## The box, and the slots an application fills
111
+
112
+ Parley draws the box; what it holds that Parley cannot know, the application
113
+ gives it. Without a slot, nothing of it is shown.
114
+
115
+ | Slot | The application gives | The box shows |
116
+ |---|---|---|
117
+ | `react` | `(to, reaction)`: puts a reaction on a message of the agent (`null`: taken back); `reactions` replaces the set (`[{ id, icon }]`) | the reactions on its messages |
118
+ | `agentName`, `agentTool` | the name said above its messages (default: the dictionary's neutral word), the tool, small, in the detailed view | |
119
+ | `voice` | `{ listen(on) }`: opens a microphone, calls `on.phrase(text)`, `on.state(word)`, `on.speech()`, `on.error(message)`; resolves with a function that closes it | a button beside Send; what is said goes into the box; while it listens, the box itself says what it does (its placeholder, and a small label on its frame), so that nothing on the screen moves; Send pressed while a phrase is still being written sends it once written |
120
+ | `autoSend`, `onAutoSend(on)` | the device's choice, kept by the application | "send by itself when I stop talking", in the small menu of settings beside the view (only with `voice`); while it is on, the microphone button wears a dot |
121
+ | `replies` | `[{ label, text, send }]`, or later `handle.replies = [...]` | ready answers above the box: `send` sends at once, else the text is put in the box; one line, the rest behind "N more" |
122
+ | `enter` | `'auto'`, `'send'` or `'newline'` | Enter sends with a keyboard (`auto`), is a new line on a touch screen; Shift+Enter a new line and Ctrl+Enter sends, always |
123
+ | `touch` | true on a touch screen (default: any coarse pointer) | the box is never focused by the page, only by a finger: the keyboard never comes up by itself, and a tap on a button of the box leaves it open |
124
+ | `attach` | `{ upload(file, name) }`, resolving with `{ path, name, size }`: where the agent finds the file | a button beside the box picks files; they are shown above it until sent; on Send each is uploaded, then the message goes with them (files alone are a message); in the thread, by name in the person's bubble |
125
+
126
+ Lines of the application -- what happened around the agent, such as its
127
+ machine short of memory -- are given with the messages (`from: "system"`) and
128
+ shown as grey lines across the thread with their time and buttons, which call
129
+ the application's `act(id, line)`. See the protocol.
130
+
131
+ When the agent asks with choices, they are answered in its message; once
132
+ that message has scrolled away (or a keyboard took the room), the same
133
+ choices come first above the box, never twice on the screen. Every size is
134
+ in `em` and `rem`: the page's text size is the box's.
135
+
136
+ ## Words
137
+
138
+ What an agent reads (the help, the MCP descriptions, the invitation, the
139
+ wake line) is English only; the invitation tells it the person's language.
140
+ What a person reads comes from a dictionary per language, chosen by `lang`;
141
+ a missing word is the English one. An application may pass its own
142
+ dictionary, whole or partial, which replaces any word whatever it says:
143
+ `texts(lang, own)`. Every word is put on the page as text, never as markup.
144
+ A language is added with one file.
145
+
146
+ ## What was tried, and what was not
147
+
148
+ Tried for real, once, with the MCP server (version 0.1): an agent (OpenCode,
149
+ on hosted open models) in its own machine; an application (Clavarium)
150
+ showing the exchange. Invited, the agent said it was ready through `tell` in
151
+ 19 seconds; told a message arrived, it read it, did the job, and asked a
152
+ question with two choices through `ask`; answered with one touch on the
153
+ page, it read the answer and went on.
154
+
155
+ Tried for real with version 0.2 (OpenCode, on hosted open models): the
156
+ `parley` command installed by the agent from its archive, `tell`, `ask`,
157
+ `show` and `status`, an answer given ten minutes later, and the two views in
158
+ a real page.
159
+
160
+ Not tried yet with a real agent: version 0.8 -- several files shown at once with their titles, reactions, dictated messages, done said once, the media of a thread, the viewer, the side of the detailed view (tested here and in an application's own end-to-end runs, the screen checked on a real thread replayed); Markdown, short versions, `close` and the signs of each message (version 0.7, tested here and in an application's own end-to-end runs, the screen checked on a real thread replayed); `doing` and the line of what it does now
161
+ (version 0.3, tested here with stand-ins); the box's slots (version 0.4:
162
+ dictation, ready answers, the keyboard of a phone), tested in an
163
+ application's own end-to-end runs with a recorded voice and an emulated
164
+ phone, not yet by a person on a real phone; files attached by the person,
165
+ lines of the application and `read --all` (version 0.5), tested here and in an
166
+ application's own end-to-end runs, not yet with a real agent; any agent but OpenCode; an answer
167
+ coming hours later; a mirror between two agents.
168
+
169
+ ## Licence
170
+
171
+ MIT. See `LICENSE`.
172
+
173
+ ## Working on Parley
174
+
175
+ After cloning, wire the repository's hooks once: `npm run hooks` (it sets
176
+ `core.hooksPath` to `hooks` in this clone). It is a script of its own, never
177
+ run by an install: installing the package changes no git configuration.
178
+ `npm run check` fails while the hooks are not wired.
package/bin/parley.mjs ADDED
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ /* THE COMMAND: the seven verbs for any agent that can run a command. It writes
3
+ * the same files as the MCP server, through the same code (server/).
4
+ *
5
+ * parley read [--all] | tell | ask | show | close | doing | status | --help
6
+ *
7
+ * Exit codes: 0 done; 1 refused (said on standard error); 2 no folder found;
8
+ * 64 not understood. */
9
+ import { readFileSync, realpathSync, existsSync } from 'node:fs';
10
+ import { resolve } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { bridge, findFolder, STATES, VERSION, FOLDER, WRITING, SHORTER, GALLERY } from '../server/parley-mcp.mjs';
13
+
14
+ export const HELP = `parley: talk with the person you work for. They read and answer on their own screen, not in this terminal.
15
+
16
+ parley read read what they wrote since you last read (answers and the
17
+ files they attached included), and your questions still open
18
+ parley read --all the whole exchange, both sides, oldest first: to pick up the
19
+ thread after a restart
20
+ parley tell [--short "title"] "text" tell them something: where you are, what you did, what you found
21
+ parley ask [--short "title"] "question" [choice ...]
22
+ ask them something; give the answers they can pick when you can
23
+ parley close ID "answer" close a question of yours they answered another way (a message
24
+ typed later), saying the answer you understood; ID as read shows it
25
+ parley show path ["note"] point them to a file you made, so that they can open it
26
+ parley show a.png="title" b.png="title" ["note"]
27
+ several files at once, one gallery, each with its title
28
+ parley doing "text" say in one short line what you are doing now, each time you
29
+ move to something new: "reading the 12 recordings"
30
+ parley status ${STATES.join('|')} ["note"]
31
+ say where you are: working when you start, waiting when you
32
+ need them, done when the task is finished
33
+
34
+ ${wrap(WRITING)}
35
+
36
+ ${wrap(SHORTER)}
37
+
38
+ ${wrap(GALLERY)}
39
+ parley show before.png="Home page, before" after.png="Home page, after" "The new header"
40
+
41
+ Their reactions to your messages come with "parley read": only "go ahead" authorizes what a message
42
+ proposes (a thumbs up does not); "stop" means stop now. A message marked dictated may hold
43
+ recognition mistakes: ask when a key word seems wrong rather than guess.
44
+ parley tell --short "Disk full: 3 things to do on your side" - <<'EOF'
45
+ ...the long text, in Markdown...
46
+ EOF
47
+
48
+ Say what you are doing with "parley doing" at each new step: their screen shows it while you
49
+ work, and keeps it one click away once your next message came.
50
+
51
+ Nothing waits. After "ask", go on with something else, or stop: when they write, you are told in
52
+ the terminal ("You have a new message"). Then run "parley read". Close the questions they
53
+ answered another way.
54
+
55
+ Write to them in their language. Put a file you show, or a picture you place in a message, in a
56
+ folder you share with them; one outside it cannot be opened from their screen.
57
+
58
+ Text can come from standard input, with "-": parley tell - <<'EOF'
59
+ ...
60
+ EOF
61
+ The folder used is --dir FOLDER, else $PARLEY_DIR, else the nearest ${FOLDER} folder from here up.
62
+ parley ${VERSION}`;
63
+
64
+ /* A paragraph of the help, cut at 96 characters. */
65
+ function wrap(text) {
66
+ const out = [];
67
+ let line = '';
68
+ for (const word of text.split(' ')) { if (line && line.length + word.length + 1 > 96) { out.push(line); line = word; } else line = line ? `${line} ${word}` : word; }
69
+ if (line) out.push(line);
70
+ return out.join('\n');
71
+ }
72
+
73
+ /* The words of "show": first the files, each a path or path="title" (a path
74
+ that is there, as written or before its first "="), then the note. */
75
+ export function showing(words, cwd = process.cwd()) {
76
+ const files = [];
77
+ let i = 0;
78
+ for (; i < words.length; i += 1) {
79
+ const w = String(words[i]);
80
+ const cut = w.indexOf('=');
81
+ if (w && existsSync(resolve(cwd, w))) files.push({ path: w, title: '' });
82
+ else if (cut > 0 && existsSync(resolve(cwd, w.slice(0, cut)))) files.push({ path: w.slice(0, cut), title: w.slice(cut + 1) });
83
+ else break;
84
+ }
85
+ /* None there: the first word is the path, and is refused as such. */
86
+ if (files.length === 0 && words.length) { files.push({ path: String(words[0]), title: '' }); i = 1; }
87
+ return { files, note: words.slice(i).join(' ') };
88
+ }
89
+
90
+ /* Runs one command line. Resolves with { code, out, err }: nothing is printed
91
+ here, so that it can be tested without a terminal. */
92
+ export function run(argv, { env = process.env, cwd = process.cwd(), stdin = () => readFileSync(0, 'utf8') } = {}) {
93
+ const args = [...argv];
94
+ let dir;
95
+ const at = args.indexOf('--dir');
96
+ if (at >= 0) { dir = args[at + 1]; if (!dir) return { code: 64, out: '', err: '--dir needs a folder.\n' }; args.splice(at, 2); }
97
+ const [verb, ...rest] = args;
98
+ if (verb === undefined || verb === '--help' || verb === '-h' || verb === 'help') return { code: 0, out: `${HELP}\n`, err: '' };
99
+ if (verb === '--version') return { code: 0, out: `${VERSION}\n`, err: '' };
100
+ if (!['read', 'tell', 'ask', 'show', 'close', 'doing', 'status'].includes(verb)) return { code: 64, out: '', err: `Unknown: ${verb}. Run "parley --help".\n` };
101
+ const folder = findFolder({ dir, env, from: cwd });
102
+ if (!folder) return { code: 2, out: '', err: `No ${FOLDER} folder here or above. Run it from your work folder, or give --dir FOLDER.\n` };
103
+ const text = (value) => (value === '-' ? stdin().replace(/\n$/, '') : value);
104
+ const verbs = bridge(folder);
105
+ try {
106
+ let said;
107
+ if (verb === 'read') { if (rest.some((a) => a !== '--all')) return { code: 64, out: '', err: 'read takes nothing, or --all.\n' }; said = verbs.read({ all: rest.includes('--all') }); }
108
+ else if (verb === 'tell' || verb === 'ask') {
109
+ /* --short "title", anywhere among the words. */
110
+ const at2 = rest.indexOf('--short');
111
+ let short;
112
+ if (at2 >= 0) { short = rest[at2 + 1]; if (short === undefined) return { code: 64, out: '', err: '--short needs a title.\n' }; rest.splice(at2, 2); }
113
+ said = verb === 'tell' ? verbs.tell(text(rest.join(' ')), { short }) : verbs.ask(text(rest[0]), rest.slice(1), { short });
114
+ }
115
+ else if (verb === 'close') said = verbs.close(rest[0], text(rest.slice(1).join(' ')));
116
+ else if (verb === 'doing') said = verbs.doing(text(rest.join(' ')));
117
+ else if (verb === 'show') { const { files, note } = showing(rest, cwd); said = verbs.show(files.length === 1 && !files[0].title ? files[0].path : files, note, { from: cwd }); }
118
+ else said = verbs.status(rest[0], rest.slice(1).join(' '));
119
+ return { code: 0, out: `${said}\n`, err: '' };
120
+ } catch (error) {
121
+ return { code: 1, out: '', err: `Not done: ${error.message}. Run "parley --help".\n` };
122
+ }
123
+ }
124
+
125
+ const main = (() => { try { return realpathSync(process.argv[1] ?? '') === realpathSync(fileURLToPath(import.meta.url)); } catch { return false; } })();
126
+ if (main) {
127
+ const { code, out, err } = run(process.argv.slice(2));
128
+ if (out) process.stdout.write(out);
129
+ if (err) process.stderr.write(err);
130
+ process.exitCode = code;
131
+ }
@@ -0,0 +1,218 @@
1
+ # The protocol
2
+
3
+ Parley is three small things that agree on one folder.
4
+
5
+ ## The folder
6
+
7
+ | File | Written by | What it holds |
8
+ |---|---|---|
9
+ | `from-agent.jsonl` | the agent's side | what the agent posted, one JSON object per line |
10
+ | `to-agent.jsonl` | the application's side | what was written to the agent, one JSON object per line |
11
+ | `read.json` | the agent's side | how many lines of `to-agent.jsonl` the agent has read (`lines`), and when it read (`reads`: `[{lines, at}]`, the last 200) |
12
+ | `server.mjs` | the application, once | the MCP server, for an agent given it that way (`install()`) |
13
+
14
+ Both files only grow. Nothing is edited, nothing is deleted. There is no
15
+ address, no port and no key: whoever can read and write the folder is a side.
16
+
17
+ The command finds the folder as git finds a repository: `--dir`, else
18
+ `$PARLEY_DIR`, else the nearest `.parley` from where the agent stands.
19
+
20
+ ## What an agent can do: seven verbs
21
+
22
+ | Command | MCP tool | It posts | It returns |
23
+ |---|---|---|---|
24
+ | `parley tell [--short TITLE] TEXT` | `tell(text, short?)` | `{kind:"say", text, short?}` | at once |
25
+ | `parley ask [--short TITLE] QUESTION [CHOICE ...]` | `ask(question, choices?, short?)` | `{kind:"ask", text, choices, short?}` | at once: it never waits for the answer |
26
+ | `parley close ID ANSWER` | `close(id, answer)` | `{kind:"close", answers, text}` | at once; ID is the question's, or its first 8 characters or more |
27
+ | `parley show PATH [NOTE]` | `show(path, note?)` | `{kind:"show", path, text}`, the path made whole | at once; refused if there is no such file |
28
+ | `parley show A="title" B="title" [NOTE]` | `show({files: [{path, title}], note?})` (or `paths`) | `{kind:"show", path, text, files: [{path, title?}]}`, twelve at most, `path` the first | at once: one gallery, each with its title |
29
+ | `parley status STATE [NOTE]` | `status(state, note?)` | `{kind:"status", state, text}` | at once |
30
+ | `parley doing TEXT` | `doing(text)` | `{kind:"doing", text}`, one line of 200 characters at most | at once |
31
+ | `parley read` | `read()` | nothing | what was written since the last read, answers and attached files included, then the questions still open with their ids |
32
+ | `parley read --all` | `read({all: true})` | nothing | the whole exchange, both sides, oldest first (the newest 200 entries): to pick up the thread after a restart |
33
+
34
+ `TEXT` is Markdown, 16,000 characters at most; `short` is its short version, a title of one or two lines (200 characters), which a screen shows first: whatever asks the person for an action or a decision, or says the agent is blocked, belongs in it. A question is closed (`close`) when the person answered it another way (a message typed later): the screen says the answer the agent understood, and the person may still answer with the question's buttons. `STATE` is `working`, `waiting` or `done`. `doing` says what the agent is
35
+ doing now, at each new step; it means it works: a `doing` after `done` or
36
+ `waiting` makes its status `working` again. Every line posted also carries
37
+ `id` (unique) and `at` (the time, ISO 8601). A text may come from standard
38
+ input with `-`. Exit codes of the command: 0 done, 1 refused, 2 no folder,
39
+ 64 not understood.
40
+
41
+ ## What is written to an agent
42
+
43
+ `{id, at, text}`, and when it answers a question: `answers` (the id of that
44
+ question) and `question` (its words, so that the agent is reminded of them).
45
+ When it answers a question by text typed in reply, the same. When it replies
46
+ to another message of the agent: `re` (its id) and `quote` (its words). When
47
+ the person dictated part of it: `via: "voice"`, and `parley read` says it may
48
+ hold recognition mistakes.
49
+
50
+ A REACTION of the person to a message of the agent is a line of its own:
51
+ `{id, at, text: "", reaction, to, quote, toAt}` (`to`: the message, `quote`
52
+ and `toAt`: its words and its time, so that the agent knows which one;
53
+ `removed: true` when taken back). `parley read` gives its meaning: the
54
+ default set is `agree` (noted; it authorizes NOTHING), `go` (do what that
55
+ message proposes; several proposals: ask which), `disagree` (ask what is
56
+ wrong), `unclear` (explain more simply, without acting), `stop` (stop now,
57
+ do nothing more), `redo` (another way), `later` (nothing for now), `love`
58
+ (noted). An application may offer another set: an unknown one is given by
59
+ its name. When the person attached files: `files`, `[{path, name, size}]`, ten at most,
60
+ `path` as the agent sees it; the text may then be empty. Where the files are
61
+ put is the application's business: it puts them where the agent can open
62
+ them, and gives Parley their paths. `parley read` gives each path.
63
+
64
+ ## Nothing waits
65
+
66
+ A verb never blocks until a person answers. The agent posts and goes on, or
67
+ stops. When something was written to it, the application is told
68
+ (`person-wrote`, with how many messages the agent has not read) and gets ONE
69
+ LINE to the agent, by a way of its own, when it judges right:
70
+
71
+ You have a new message: run "parley read".
72
+
73
+ What was written is never on that line. After the agent lost its memory (its
74
+ machine restarted, a new session), the application may type, once:
75
+
76
+ Your chat with the person is kept: run "parley read --all" before going on.
77
+ The agent runs `parley read` and
78
+ finds it. An answer may therefore come two minutes or two days later.
79
+
80
+ ## Parley announces, the application decides
81
+
82
+ | Event | When | Carries |
83
+ |---|---|---|
84
+ | `person-wrote` | `say()` wrote a message | `message`, `unread` |
85
+ | `agent-said` | the agent ran `tell` | `message` |
86
+ | `agent-asked` | the agent ran `ask` | `message` (with `choices`) |
87
+ | `agent-showed` | the agent ran `show` | `message` (with `path`, and `open`: the application's way to open it, or `null`) |
88
+ | `agent-status` | the agent ran `status` | `message` (with `state`) |
89
+ | `agent-doing` | the agent ran `doing` | `message` (its `text`: one line) |
90
+ | `agent-closed` | the agent ran `close` | `message` (`answers`: the question; `text`: the answer it understood) |
91
+ | `person-reacted` | `react()` wrote a reaction | `message` (the line), `reaction` (null: taken back), `wakes` (`false`, `true` or `'now'`: the set's hint), `unread` |
92
+
93
+ Events of the agent are announced by `listen()`, each line once, once it is
94
+ whole, from the moment it was started: what was posted before is in
95
+ `read()`.
96
+
97
+ | Action | Gives |
98
+ |---|---|
99
+ | `say(text, answers?, files?, {via?})` | what `read()` gives, after writing; `answers` may be any message of the agent (a question is answered, another replied to) |
100
+ | `react(to, reaction)` | a reaction of the set (`REACTIONS`) on a message of the agent, or `null` to take it back; what `read()` gives |
101
+ | `read()` | `messages` (each of the person's says `seen`, and `seenAt` when known, `via` when dictated; each of the agent's may carry `short`, `reaction` (`{id, at, seen}`, the one standing), `files` for a gallery (`[{path, title, open}]`), and `refs`: the paths it cites that `reach` opens; a question it closed, `closed: {text, at}`), `waiting` (questions neither answered nor closed), `status`, `steps` (every `status` and `doing`, in order: `{id, kind, state, note, at}`), `files` (every file shown, and every one cited that can be opened, `cited: true`), `unread` |
102
+ | `invite(lang, {install?, shared?})` | the invitation, a text |
103
+ | `unread()` | how many of the person's messages the agent has not read |
104
+
105
+ `reach(path)`, given by the application, says how the person opens a file
106
+ the agent showed, or one the person attached: a reference of its own, or
107
+ `null` when it is not in a folder the person can open. The screen then says
108
+ so.
109
+
110
+ ## Lines of the application
111
+
112
+ What happens around the agent -- its machine short of memory, restarted --
113
+ belongs in the thread, where it happened. The application keeps such lines
114
+ itself (never in the folder: the agent writes there) and adds them to the
115
+ messages it gives the screen:
116
+
117
+ {from: "system", id, at, text, level: "needs-you"|"news"|"trace", tone?: "warn"|"bad",
118
+ topic?, brief?, actions: [{id, label}]}
119
+
120
+ The application gives each line a neutral level, and nothing else is decided
121
+ here: `needs-you` is shown in both views; `news` in both, as a compact line in
122
+ the simple one, where lines of one `topic` that follow each other are one line
123
+ (`{topic} ({count}): {briefs}`); `trace` in the detailed view only. (`warn` and
124
+ `bad` are read as needs-you, `info` as news.) The screen shows each as a grey
125
+ line across, with its time and its buttons (four at most); a button calls the
126
+ application's `act(id, line)`. A line of
127
+ the application takes no step and does not hide that the person's last
128
+ message is not read yet.
129
+
130
+ ## What the screen makes of the states
131
+
132
+ `waiting` and `done` are no steps. Each is said once, at the end of the
133
+ thread: waiting until the person writes; done ("Done: ..."), quietly, while it
134
+ is the last thing that happened -- a message of either side or a step after
135
+ it, and it is gone. The detailed view says the state at the top only when the
136
+ end does not.
137
+
138
+ ## What the screen makes of the steps
139
+
140
+ The steps that came between two messages belong to the second one: folded
141
+ into it in the simple view, unfolded above it in the detailed one. Those
142
+ after the last message are the agent's turn: while the newest of them is a `doing`
143
+ (or a `working` status), it is shown as the one line of what it does now, and
144
+ it goes when the next message comes. Nothing came for 15 minutes: the line
145
+ stays, still, and says since when. See the table in the README for what each
146
+ view shows.
147
+
148
+ ## The box
149
+
150
+ Under the exchange, the box the person writes in. The application may fill
151
+ four slots, and nothing of them is shown without it: `voice` (a dictation
152
+ of its own, its words go into the box), `replies` (ready answers above the
153
+ box), `enter` (what Enter does) and `attach` (files the person attaches: a
154
+ button beside the box picks them, they are shown above it, and on Send each
155
+ is given to `upload(file)`, which puts it where the agent can open it and
156
+ resolves with `{path, name, size}`; then the message goes, with them). The choices of the question the agent
157
+ asks now come first above the box when its message has scrolled away. The
158
+ page never puts the cursor in the box on a touch screen: only a finger does.
159
+ See the README for each slot.
160
+
161
+ ## Words
162
+
163
+ | Read by | Language | Where |
164
+ |---|---|---|
165
+ | the agent: help, MCP descriptions, invitation, wake line | English only; the invitation names the person's language | `bin/`, `server/`, `host/` |
166
+ | the person: the screen, alerts | a dictionary per language, `en` when a word is missing; an application's own dictionary replaces any word | `ui/texts/<lang>.json`, `texts(lang, own)` |
167
+
168
+ ## What the agent writes, on the screen
169
+
170
+ With `markdown: true`, the library reads the agent's Markdown into plain data
171
+ (headings, lists, tasks, emphasis, `==highlight==`, tables, quotes, the alerts
172
+ of GitHub `> [!NOTE|TIP|IMPORTANT|WARNING|CAUTION]`, badges `[ok] [todo]
173
+ [blocked] [warn]`, code blocks) and builds the page from that data element by
174
+ element. A link is followed only when it is http or https (a new tab,
175
+ `rel="noopener noreferrer"`), or when it is a path the application opens. A
176
+ picture is shown only from a path the application opens (`picture(file)`):
177
+ never from anywhere else. A ```mermaid block is drawn by the application's
178
+ `diagram(code)` and shown as a picture, never inserted as markup; a failure
179
+ shows its code. A code block is coloured from the token tree of the
180
+ application's `highlight(code, lang)`, spans made here. A long message is
181
+ folded in the simple view (after its short version, or after about twelve
182
+ lines), its `> [!IMPORTANT]` boxes kept in sight; a question never, nor
183
+ the newest message of the agent (nothing seems left below it). The text in
184
+ brackets of a picture is its title, under it. Files shown at once are one
185
+ gallery, each its title under it (no title: the name of the file without its
186
+ extension and dashes); the name of the file on hover and in the detailed view.
187
+
188
+ ## Reactions, the media, the viewer
189
+
190
+ A message of the agent takes a reaction: a button on hover, a right click, or
191
+ a long press opens the set, each icon with its word under it and its meaning
192
+ on hover, and Reply. The icons are Lucide's (`ui/parley-icons.js`, copied with
193
+ its version and licence, nothing loaded). "Media and files", in the menu of
194
+ settings, shows every picture of the thread as a grid (the newest first) and
195
+ the other files as a list; a picture goes large among all of them; "See in
196
+ the thread" finds where it came. A picture large grows out of its thumbnail,
197
+ zooms up to eight times, goes to the previous or next (arrows, a swipe, a
198
+ strip of small pictures), and closes with a tap on it (not zoomed) or beside
199
+ it, a swipe down, or Escape. What arrives while the thread is open comes in
200
+ softly and is marked a moment; movement only for whoever did not ask for
201
+ less.
202
+
203
+ ## What each side must not trust
204
+
205
+ - What the agent posts is text chosen by a program. The application's side
206
+ bounds it and removes what a screen obeys rather than shows; the library
207
+ puts it on the page as text, never as markup. The words of a dictionary,
208
+ too: they are shown as text, whatever they say.
209
+ - The agent can write in its own folder: it can forge a message to itself.
210
+ That harms nobody but itself. Nothing here gives it a right it had not.
211
+ - It all depends on the agent's good will: it uses these verbs because it
212
+ was asked to. What it asks in its terminal is not seen here.
213
+
214
+ ## A mirror
215
+
216
+ The other side need not be a person. Two folders, each one's `from-agent`
217
+ copied into the other's `to-agent`, and two agents talk, whatever each one
218
+ is. Not built yet.