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 +1 -1
- package/README.md +175 -4
- package/bin/parley.mjs +131 -0
- package/docs/protocol.md +218 -0
- package/host/parley-host.mjs +357 -0
- package/package.json +45 -6
- package/server/parley-mcp.mjs +264 -0
- package/ui/parley-icons.js +115 -0
- package/ui/parley-ui.css +521 -0
- package/ui/parley-ui.js +2058 -0
- package/ui/texts/en.json +131 -0
- package/ui/texts/fr.json +131 -0
- package/keep.mjs +0 -4
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,7 +1,178 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Clavarium Parley
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A bridge between a command-line agent and a screen of its own.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/docs/protocol.md
ADDED
|
@@ -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.
|