connection-centric 0.3.0 → 0.5.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/GUIDE.md +3 -0
- package/centric.mjs +1 -1
- package/commands.mjs +95 -27
- package/package.json +1 -1
package/GUIDE.md
CHANGED
|
@@ -49,6 +49,9 @@ A tool lapses after 90 days unused. The person disconnects it from Me, or with `
|
|
|
49
49
|
- **me**: the person themselves: their display name, photo, and their own switch for the assistant.
|
|
50
50
|
- **assistant**: Centric's, which answers a person's questions from what they can see and corrects a message they are writing. It is not a connected tool: it never acts, and asking it needs the person's own switch on.
|
|
51
51
|
- **conversation**: a person's questions to the assistant and its answers, theirs alone; deleted outright.
|
|
52
|
+
- **memory**: a line the assistant keeps about a person because they told it to, read before each of their questions; theirs alone to see, add and delete.
|
|
53
|
+
- **linked-system**: a firm's own system, such as its stores or accounts, that its Company's admins link so its people's assistant can read from it, through its tools that only read.
|
|
54
|
+
- **report-template**: the shape an Assistant report is written to: its sections, each a heading and what goes in it, the period it covers and the places it draws from. A person's own, or a Project's or Company's, written by its staff (a Company's by its admins). An assistant report is written to one by asking the assistant for it, with `assistant question`.
|
|
52
55
|
- **application**: a firm asking Centric for its Company. An Operator approves or rejects it, once.
|
|
53
56
|
- **connected-tool**: this command, or any program a person approved; it acts as them.
|
|
54
57
|
|
package/centric.mjs
CHANGED
|
@@ -118,7 +118,7 @@ const where = (x) => x.container_name ?? (x.channel ? `${x.channel.name} · ${x.
|
|
|
118
118
|
// A tombstone in channel show is named by the message it stands for; a message with a file, by the document to download.
|
|
119
119
|
const row = (x) => (x && typeof x === "object" ? [x.kind && `[${x.kind}]`, x.id ?? x.zulip_message_id, label(x), where(x), x.document && `document ${x.document.id}`, x.due_on && `due ${x.due_on}`, x.this && "(this one)"].filter((v) => v != null && v !== "").join(" ") : String(x));
|
|
120
120
|
// Words others wrote, on a terminal: control characters (escape sequences among them) go, but a line and a tab stay.
|
|
121
|
-
const plain = (s) => s.replace(/[\
|
|
121
|
+
const plain = (s) => s.replace(/[^\P{Cc}\t\n]/gu, "");
|
|
122
122
|
// JSON escapes the first 32 itself; the C1 ones a terminal may also obey are escaped too, so nothing is lost.
|
|
123
123
|
const asJson = (v) => JSON.stringify(v).replace(/[\u007f-\u009f]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
124
124
|
function text(v) {
|
package/commands.mjs
CHANGED
|
@@ -18,7 +18,7 @@ export const CLI_VERSION = JSON.parse(readFileSync(new URL("./package.json", imp
|
|
|
18
18
|
export const VERBS = ["list", "show", "make", "edit", "delete", "archive", "unarchive", "close", "reopen", "not-yet", "grant", "revoke", "appoint", "remove",
|
|
19
19
|
"issue", "acknowledge", "add", "revise", "restore", "download", "messages", "mark-read", "send", "forward", "react", "unreact", "history", "readers",
|
|
20
20
|
"pin", "unpin", "mute", "unmute", "private", "open-to-company", "set-picture", "remove-picture", "set-photo", "remove-photo",
|
|
21
|
-
"approve", "reject", "withdraw", "rotate-key", "dormant", "reactivate", "report", "record-pack", "audit", "question", "correct", "disconnect"];
|
|
21
|
+
"approve", "reject", "withdraw", "rotate-key", "dormant", "reactivate", "report", "record-pack", "audit", "question", "correct", "disconnect", "transcript"];
|
|
22
22
|
|
|
23
23
|
// What a value names, read by kind. Things are found by id, or by an exact name only one thing the person can see has.
|
|
24
24
|
const THINGS = ["project", "company", "container", "channel", "task", "document", "folder", "zone", "identity", "membership", "admin", "connected-tool"];
|
|
@@ -34,11 +34,13 @@ export class Broken extends Error {
|
|
|
34
34
|
const refuse = (message, extra) => { throw new Refused(message, extra); };
|
|
35
35
|
|
|
36
36
|
export const commandName = (cmd) => [cmd.noun, cmd.verb].filter(Boolean).join(" ");
|
|
37
|
+
// A document's revision by its number, or its current one: for the command line, MCP and the assistant's look alike.
|
|
38
|
+
export const revisionOf = (d, n) => (n ? d.revisions.find((x) => x.n === Number(n)) ?? refuse(`there is no revision ${n} of it`) : d.revisions.find((x) => x.id === d.document.current_revision_id));
|
|
37
39
|
const enc = encodeURIComponent;
|
|
38
40
|
const qs = (o) => { const p = new URLSearchParams(Object.entries(o).filter(([, v]) => v != null && v !== false).map(([k, v]) => [k, v === true ? "1" : String(v)])); return p.size ? `?${p}` : ""; };
|
|
39
41
|
// Rule 9: what a list or a place prints, the same everywhere.
|
|
40
42
|
const listed = (items, more = null) => ({ items, more });
|
|
41
|
-
const meShown = async (c) => { const m = await c.get("/api/me"); return { identity: m.identity, assistant: m.assistant, application: m.application, operator: m.operator, containers: m.containers.length }; };
|
|
43
|
+
const meShown = async (c) => { const m = await c.get("/api/me"); return { identity: m.identity, assistant: m.assistant, speech: m.speech, opened_directs: m.opened_directs, application: m.application, operator: m.operator, containers: m.containers.length }; };
|
|
42
44
|
const newest = (x, y) => (y.last?.id ?? 0) - (x.last?.id ?? 0); // a place with nothing in it yet goes last
|
|
43
45
|
|
|
44
46
|
// Rule 11: a moment is shown in India's time, ISO 8601 with +05:30; a day stays YYYY-MM-DD. Field names stay the API's.
|
|
@@ -50,14 +52,15 @@ export const inIndia = (v) => Array.isArray(v) ? v.map(inIndia)
|
|
|
50
52
|
// Rule 5: what sits inside a container may be named with --in, so a name is looked for there alone. Every command
|
|
51
53
|
// whose target does takes it, whether or not its table lists it; the command line and MCP both read flags from here.
|
|
52
54
|
const INSIDE = ["channel", "task", "document", "folder", "zone", "membership", "admin"];
|
|
53
|
-
|
|
55
|
+
// Undescribed over MCP: the guide says once what --in does, and the list sits in an MCP client's context every turn.
|
|
56
|
+
export const flagsOf = (cmd) => (INSIDE.includes(cmd.target?.kind) && !cmd.flags.in ? { ...cmd.flags, in: { kind: "container", about: "where to look for its name", said: "in the guide" } } : cmd.flags);
|
|
54
57
|
|
|
55
58
|
// Values as given (from the command line's words or MCP's arguments, both by their flag names) read into what the
|
|
56
59
|
// command uses: each thing to its id, `in` first, since a name is looked for inside it.
|
|
57
60
|
export async function prepare(cmd, given, c) {
|
|
58
61
|
const a = {}, spec = { ...(cmd.target ? { [cmd.target.name]: { ...cmd.target, need: !cmd.target.optional } } : {}), ...flagsOf(cmd) };
|
|
59
62
|
for (const k of Object.keys(given)) if (!spec[k]) refuse(`${commandName(cmd)} takes no ${cmd.target?.name === k ? k : `--${k}`}`);
|
|
60
|
-
const order = Object.keys(spec).sort((x, y) => (y === "in") - (x === "in") || (y === "company") - (x === "company"));
|
|
63
|
+
const order = Object.keys(spec).sort((x, y) => Number(y === "in") - Number(x === "in") || Number(y === "company") - Number(x === "company"));
|
|
61
64
|
for (const k of order) {
|
|
62
65
|
const f = spec[k], v = given[k];
|
|
63
66
|
if (v === undefined || v === null || (Array.isArray(v) && !v.length)) { if (f.need) refuse(`${commandName(cmd)} needs ${f === spec[cmd.target?.name] ? `the ${k}` : `--${k}`}`); continue; }
|
|
@@ -167,9 +170,16 @@ export const COMMANDS = [
|
|
|
167
170
|
} },
|
|
168
171
|
|
|
169
172
|
// ── work ──
|
|
170
|
-
{ noun: "task", verb: "list", flags: { in: IN,
|
|
171
|
-
|
|
173
|
+
{ noun: "task", verb: "list", flags: { in: { ...IN, need: false }, mine: { kind: "bool", about: "your own open tasks in every project instead, as Home groups them: needs you, overdue, this week, waiting; 200 at most, with the counts of each" },
|
|
174
|
+
state: { kind: "choice", of: ["open", "closed"] }, zone: { kind: "zone" }, before: { kind: "words", about: "with --state closed: the next page, from the more: of the one before" } },
|
|
175
|
+
about: "A container's tasks: open ones first, soonest due, then the most important; done ones newest first, a page at a time. Or, with --mine, your own across every project.",
|
|
172
176
|
run: async (c, a) => {
|
|
177
|
+
if (a.mine) {
|
|
178
|
+
if (a.in || a.state || a.zone || a.before) refuse("--mine is every project's open tasks of yours: leave --in, --state, --zone and --before off");
|
|
179
|
+
const r = await c.get("/api/me/tasks");
|
|
180
|
+
return { ...listed(r.tasks), counts: r.counts };
|
|
181
|
+
}
|
|
182
|
+
if (!a.in) refuse("task list needs --in, or --mine");
|
|
173
183
|
if (a.before && a.state !== "closed") refuse("--before pages done tasks: with --state closed");
|
|
174
184
|
const l = await c.get(`/api/containers/${a.in}/tasks${qs({ state: a.state, zone_id: a.zone, before: a.before })}`);
|
|
175
185
|
return listed(l, a.state === "closed" && l.length >= 100 ? l.at(-1).done_key : null); // the server's page of done ones (DONE_PAGE)
|
|
@@ -215,8 +225,9 @@ export const COMMANDS = [
|
|
|
215
225
|
run: async (c, a) => listed((await c.get(`/api/containers/${a.in}?documents=0`)).zones) },
|
|
216
226
|
{ noun: "document", verb: "list", flags: { in: IN, folder: { kind: "folder", about: "only what is in this folder" } }, about: "A container's documents, newest revision first.",
|
|
217
227
|
run: async (c, a) => listed((await c.get(`/api/containers/${a.in}/documents`)).filter((d) => !a.folder || d.folder_id === a.folder)) },
|
|
218
|
-
{ noun: "document", verb: "show", target: { name: "document", kind: "document" }, flags: {
|
|
219
|
-
|
|
228
|
+
{ noun: "document", verb: "show", target: { name: "document", kind: "document" }, flags: { contents: { kind: "bool", about: "with what is inside its current revision" } },
|
|
229
|
+
about: "One document: its revisions, issuances and acknowledgements.",
|
|
230
|
+
run: (c, a) => c.get(`/api/documents/${a.document}${a.contents ? "?contents=1" : ""}`) },
|
|
220
231
|
{ noun: "folder", verb: "list", flags: { in: IN }, about: "A container's folders that you can see.",
|
|
221
232
|
run: async (c, a) => listed((await c.get(`/api/containers/${a.in}?documents=0`)).folders) },
|
|
222
233
|
|
|
@@ -266,7 +277,7 @@ export const COMMANDS = [
|
|
|
266
277
|
{ noun: "task", verb, target: { name: "task", kind: "task" }, flags: {}, about, run: (c, a) => c.post(`/api/tasks/${a.task}/${verb}`) })),
|
|
267
278
|
{ noun: "task", verb: "delete", target: { name: "task", kind: "task" }, flags: {}, about: "Deletes a task, leaving a tombstone: staff, or whoever made it.", run: (c, a) => c.del(`/api/tasks/${a.task}`) },
|
|
268
279
|
{ noun: "ask", verb: "make", flags: { task: { kind: "task", about: "the task to ask about" }, to: { kind: "identity", need: true }, channel: { kind: "channel", about: "for an ask with a task of its own: the channel the task is posted in" }, title: { kind: "words", about: "for an ask with a task of its own" }, due: { kind: "day" } },
|
|
269
|
-
about: "Asks someone to do a task; or,
|
|
280
|
+
about: "Asks someone to do a task; or, with --channel and --title instead of --task, makes it too, posted there, as the app does.",
|
|
270
281
|
async run(c, a) {
|
|
271
282
|
if (a.task ? a.title || a.channel || a.due : !(a.channel && a.title)) refuse("either --task, or --channel and --title for an ask with a task of its own");
|
|
272
283
|
const task = a.task ?? (await c.post(`/api/containers/${(await c.get(`/api/channels/${a.channel}`)).container_id}/tasks`, { title: a.title, channel_id: a.channel, due_on: a.due })).id;
|
|
@@ -281,14 +292,14 @@ export const COMMANDS = [
|
|
|
281
292
|
about: "Issues a document's revision to someone, who is then asked to acknowledge it.",
|
|
282
293
|
async run(c, a) {
|
|
283
294
|
const d = await c.get(`/api/documents/${a.document}`);
|
|
284
|
-
const r =
|
|
295
|
+
const r = revisionOf(d, a.revision);
|
|
285
296
|
return c.post("/api/issuances", { container_id: d.document.container_id, document_revision_id: r.id, issued_to: a.to });
|
|
286
297
|
} },
|
|
287
298
|
{ noun: "message", verb: "issue", target: { name: "message", kind: "message" }, flags: { to: { kind: "identity", need: true } }, about: "Issues a channel message as an instruction to someone.",
|
|
288
299
|
run: (c, a) => c.post("/api/issuances", { zulip_message_id: a.message, issued_to: a.to }) },
|
|
289
300
|
{ noun: "message", verb: "send", flags: { channel: { kind: "channel" }, task: { kind: "task", about: "a reply to the task's message, in its channel" }, text: { ...TEXT, need: false }, "reply-to": { kind: "message", about: "a message of the same place to answer" },
|
|
290
301
|
mention: { kind: "identity", many: true, about: "someone who reads the channel" }, "mention-all": { kind: "bool", about: "everyone who reads the channel" }, path: { kind: "path", many: true, about: "a photo or file on this computer; each is kept as a document" } },
|
|
291
|
-
about: "Sends a message to a channel, or a reply to a task's message where it is talked about.
|
|
302
|
+
about: "Sends a message to a channel, or a reply to a task's message where it is talked about. @Name in the text reaches nobody: a mention is a flag. Files sent with --path are kept as documents, the words the last one's caption.",
|
|
292
303
|
async run(c, a) {
|
|
293
304
|
if (!a.channel === !a.task) refuse("say where: --channel or --task");
|
|
294
305
|
if (!a.text && !a.path) refuse("say something: --text, or --path for a file");
|
|
@@ -324,6 +335,10 @@ export const COMMANDS = [
|
|
|
324
335
|
run: (c, a) => c.post("/api/messages/read", { channel: a.channel, through: a.through }) },
|
|
325
336
|
{ noun: "direct", verb: "mark-read", target: { name: "identity", kind: "identity" }, flags: {}, about: "Read, on purpose: the whole Direct.",
|
|
326
337
|
run: (c, a) => c.post("/api/messages/read", { direct: a.identity }) },
|
|
338
|
+
// CONTEXT.md Opened: your own assistant reads it until you close it; the Direct tells them each time.
|
|
339
|
+
{ noun: "direct", verb: "edit", target: { name: "identity", kind: "identity" }, flags: { assistant: { kind: "choice", of: ["open", "closed"], need: true } },
|
|
340
|
+
about: "Opens or closes your Direct with them to your own assistant; it tells them.",
|
|
341
|
+
run: (c, a) => c.post(`/api/directs/${a.identity}/assistant`, { open: a.assistant === "open" }) },
|
|
327
342
|
{ noun: "notification", verb: "mark-read", target: { name: "notification", kind: "key", many: true, optional: true }, flags: { all: { kind: "bool", about: "every one in the list now" } },
|
|
328
343
|
about: "Marks notifications read, by their keys from notification list, or all of them.",
|
|
329
344
|
run: (c, a) => (!a.notification === !a.all ? refuse("name the notifications by key, or say --all") : c.post("/api/me/notifications/read", a.all ? { all: true } : { keys: a.notification })) },
|
|
@@ -347,11 +362,22 @@ export const COMMANDS = [
|
|
|
347
362
|
run: (c, a) => (a.name === undefined && a.folder === undefined ? refuse("say what changes: --name or --folder") : c.patch(`/api/documents/${a.document}`, { name: a.name, folder_id: a.folder })) },
|
|
348
363
|
{ noun: "document", verb: "delete", target: { name: "document", kind: "document" }, flags: {}, about: "Deletes a document, leaving a tombstone: staff only.",
|
|
349
364
|
run: (c, a) => c.del(`/api/documents/${a.document}`) },
|
|
365
|
+
// A voice note's Transcript (CONTEXT.md, issue 307): asked for by someone with Speech on, made once and kept for everyone
|
|
366
|
+
// who can hear the note; taken away by the note's sender alone, which the app asks about first (--dry-run).
|
|
367
|
+
{ noun: "document", verb: "transcript", target: { name: "document", kind: "document" }, flags: { remove: { kind: "bool", about: "take it away: the note's sender alone" }, "dry-run": { ...DRY, about: "with --remove: what would go" } },
|
|
368
|
+
about: "A voice note's Transcript: asked for, and made once, with Speech on; with --remove, taken away by the note's sender.",
|
|
369
|
+
run: async (c, a) => {
|
|
370
|
+
if (a["dry-run"] && !a.remove) refuse("--dry-run is for --remove");
|
|
371
|
+
if (!a.remove) return c.post(`/api/documents/${a.document}/transcript`);
|
|
372
|
+
if (!a["dry-run"]) return c.del(`/api/documents/${a.document}/transcript`);
|
|
373
|
+
const tr = revisionOf(await c.get(`/api/documents/${a.document}`))?.transcript;
|
|
374
|
+
return tr ? { dry_run: true, would_remove: { document: a.document, transcript: tr.text } } : refuse("it has no transcript to remove");
|
|
375
|
+
} },
|
|
350
376
|
{ noun: "document", verb: "download", target: { name: "document", kind: "document" }, flags: { revision: { kind: "int", about: "which revision; the current one if left out" }, out: { kind: "path", about: "where to write it; the file's own name here if left out" } },
|
|
351
377
|
about: "Writes the exact bytes kept for a revision to this computer, after checking them against the hash Centric kept.",
|
|
352
378
|
async run(c, a) {
|
|
353
379
|
const d = await c.get(`/api/documents/${a.document}`);
|
|
354
|
-
const r =
|
|
380
|
+
const r = revisionOf(d, a.revision);
|
|
355
381
|
const bytes = await c.bytes(`/api/files?path=${enc(r.zulip_path)}`), sha256_hex = createHash("sha256").update(bytes).digest("hex");
|
|
356
382
|
if (sha256_hex !== r.sha256_hex) throw new Broken(`what arrived does not match the hash kept for revision ${r.n}; nothing was written`);
|
|
357
383
|
const path = a.out ?? (basename(decodeURIComponent(r.zulip_path.split("/").pop())).replace(/^\.+/, "") || "file");
|
|
@@ -370,11 +396,14 @@ export const COMMANDS = [
|
|
|
370
396
|
{ noun: "project", verb: "make", flags: { company: { kind: "company", need: true }, name: { kind: "words", need: true } }, about: "Makes a Project in a Company, with you in it: its admins only.",
|
|
371
397
|
async run(c, a) { const co = (await c.get("/api/me")).containers.find((x) => x.id === a.company); return c.post("/api/containers", { company_id: co.company_id, name: a.name }); } },
|
|
372
398
|
...["project", "company"].flatMap((noun) => [
|
|
373
|
-
{ noun, verb: "edit", target: { name: noun, kind: noun }, flags: { name: { kind: "words" }, brief: { kind: "text", none: true, about: "what the assistant should know first; none clears it" }
|
|
374
|
-
|
|
399
|
+
{ noun, verb: "edit", target: { name: noun, kind: noun }, flags: { name: { kind: "words" }, brief: { kind: "text", none: true, about: "what the assistant should know first; none clears it" },
|
|
400
|
+
...(noun === "company" && { "documents-to-model": { kind: "choice", of: ["on", "off"], about: "off: none of its files go to a model" } }) },
|
|
401
|
+
about: noun === "project" ? "Renames a Project (its admins), or writes its brief (its staff)." : "Renames a Company, or writes its brief: its admins only.",
|
|
375
402
|
async run(c, a) {
|
|
376
|
-
|
|
377
|
-
|
|
403
|
+
const to = a["documents-to-model"];
|
|
404
|
+
if (a.name === undefined && a.brief === undefined && to === undefined) refuse(`say what changes: --name or --brief${noun === "company" ? " or --documents-to-model" : ""}`);
|
|
405
|
+
const patch = (b) => c.patch(`/api/containers/${a[noun]}`, b);
|
|
406
|
+
return { ...(a.name !== undefined && (await patch({ name: a.name }))), ...(a.brief !== undefined && (await patch({ brief: a.brief ?? "" }))), ...(to !== undefined && (await patch({ files_never_sent: to === "off" }))) };
|
|
378
407
|
} },
|
|
379
408
|
{ noun, verb: "record-pack", target: { name: noun, kind: noun }, flags: { out: { ...PAGE, need: true } }, about: "Writes the record pack: every document, issuance, task and deletion, with the chain that proves it unchanged.",
|
|
380
409
|
run: (c, a) => page(c, `/api/containers/${a[noun]}/record-pack.html`, a.out) },
|
|
@@ -385,10 +414,10 @@ export const COMMANDS = [
|
|
|
385
414
|
about: verb === "archive" ? "Takes a Project out of the lists, everything in it kept: admins only." : "Brings an archived Project back.",
|
|
386
415
|
run: (c, a) => c.patch(`/api/containers/${a.project}`, { archived: verb === "archive" }) })),
|
|
387
416
|
{ noun: "project", verb: "report", target: { name: "project", kind: "project" }, flags: { day: { kind: "day", about: "as it stood at the end of that day; now if left out" }, out: PAGE },
|
|
388
|
-
about: "The Project's report: open tasks by age, response time, overdue by zone. Staff only.",
|
|
417
|
+
about: "The Project's Daily report: open tasks by age, response time, overdue by zone. Staff only.",
|
|
389
418
|
run: (c, a) => (a.out ? page(c, `/api/containers/${a.project}/report.html${qs({ day: a.day })}`, a.out) : c.get(`/api/containers/${a.project}/report${qs({ day: a.day })}`)) },
|
|
390
419
|
...[["project", "containers"], ["company", "containers"], ["channel", "channels"]].flatMap(([noun, route]) => [
|
|
391
|
-
{ noun, verb: "set-picture", target: { name: noun, kind: noun }, flags: { path: { ...PATH, about: "a square JPEG, PNG or WebP
|
|
420
|
+
{ noun, verb: "set-picture", target: { name: noun, kind: noun }, flags: { path: { ...PATH, about: "a square JPEG, PNG or WebP, 300 KB at most" } },
|
|
392
421
|
about: `Sets the picture a ${noun} shows beside its name.`, run: (c, a) => c.put(`/api/${route}/${a[noun]}/picture`, { image: c.image(a.path) }) },
|
|
393
422
|
{ noun, verb: "remove-picture", target: { name: noun, kind: noun }, flags: {}, about: `Takes a ${noun}'s picture away.`, run: (c, a) => c.del(`/api/${route}/${a[noun]}/picture`) },
|
|
394
423
|
]),
|
|
@@ -428,10 +457,10 @@ export const COMMANDS = [
|
|
|
428
457
|
{ noun: "admin", verb: "appoint", flags: { company: { kind: "company", need: true }, identity: { kind: "identity", need: true } }, about: "Makes someone of the Company one of its admins.",
|
|
429
458
|
run: (c, a) => c.post(`/api/containers/${a.company}/admins`, { identity_id: a.identity }) },
|
|
430
459
|
{ noun: "admin", verb: "remove", target: { name: "admin", kind: "admin" }, flags: {}, about: "Takes an admin's role away; the last admin cannot give it up.", run: (c, a) => c.del(`/api/admins/${a.admin}`) },
|
|
431
|
-
{ noun: "identity", verb: "rotate-key", target: { name: "identity", kind: "identity" }, flags: { "dry-run": { ...DRY, about: "
|
|
460
|
+
{ noun: "identity", verb: "rotate-key", target: { name: "identity", kind: "identity" }, flags: { "dry-run": { ...DRY, about: "the firms they are in, and each tool of theirs that would disconnect" } },
|
|
432
461
|
about: "Signs a person out everywhere: their phone signs in again, and every tool of theirs is disconnected.", run: (c, a) => c.post(`/api/identities/${a.identity}/rotate-key`, { dry_run: a["dry-run"] }) },
|
|
433
|
-
{ noun: "identity", verb: "dormant", target: { name: "identity", kind: "identity" }, flags: { in: { kind: "container", about: "where to look for their name" }, "dry-run": { ...DRY, about: "
|
|
434
|
-
about: "Puts a person to sleep, as after their number changed hands: every membership ends, and every tool of theirs is disconnected. Centric's Operators only, by
|
|
462
|
+
{ noun: "identity", verb: "dormant", target: { name: "identity", kind: "identity" }, flags: { in: { kind: "container", about: "where to look for their name" }, "dry-run": { ...DRY, about: "the firms they are in, and each tool of theirs that would disconnect" } },
|
|
463
|
+
about: "Puts a person to sleep, as after their number changed hands: every membership ends, and every tool of theirs is disconnected. Centric's Operators only, by an id from identity show.", run: (c, a) => c.post(`/api/identities/${a.identity}/dormant`, { dry_run: a["dry-run"] }) },
|
|
435
464
|
{ noun: "identity", verb: "reactivate", target: { name: "identity", kind: "identity" }, flags: {}, about: "Brings a person put to sleep back; an admin names them on containers again. Centric's Operators only.",
|
|
436
465
|
run: (c, a) => c.post(`/api/identities/${a.identity}/reactivate`) },
|
|
437
466
|
{ noun: "access-link", verb: "make", flags: { in: { ...IN, kind: "project" }, phone: { kind: "phone", need: true }, designation: { kind: "words", need: true }, task: { kind: "task", about: "an ask's task, shown when the link opens" } },
|
|
@@ -452,25 +481,64 @@ export const COMMANDS = [
|
|
|
452
481
|
|
|
453
482
|
// ── me and the assistant ──
|
|
454
483
|
{ noun: "me", verb: "edit", flags: { "display-name": { kind: "words", about: "what you call yourself, everywhere you work" }, assistant: { kind: "choice", of: ["on", "off"], about: "your own switch for Centric's assistant (ADR 0002)" },
|
|
455
|
-
language: { kind: "choice", of: ["en", "gu", "hi"], about: "what your assistant and your digest answer in" }
|
|
456
|
-
|
|
484
|
+
language: { kind: "choice", of: ["en", "gu", "hi"], about: "what your assistant and your digest answer in" },
|
|
485
|
+
speech: { kind: "choice", of: ["on", "off"], about: "your own switch for read aloud, transcripts and dictation (ADR 0019)" } },
|
|
486
|
+
about: "Changes your display name, switches your assistant or speech on or off, or sets the assistant's language.",
|
|
457
487
|
async run(c, a) {
|
|
458
|
-
if (a["display-name"] === undefined && a.assistant === undefined && a.language === undefined) refuse("say what changes: --display-name, --assistant or --
|
|
488
|
+
if (a["display-name"] === undefined && a.assistant === undefined && a.language === undefined && a.speech === undefined) refuse("say what changes: --display-name, --assistant, --language or --speech");
|
|
459
489
|
if (a["display-name"] !== undefined) await c.patch("/api/me", { display_name: a["display-name"] });
|
|
460
490
|
if (a.assistant !== undefined || a.language !== undefined) await c.patch("/api/me/assistant", { on: a.assistant && a.assistant === "on", lang: a.language });
|
|
491
|
+
if (a.speech !== undefined) await c.patch("/api/me/speech", { on: a.speech === "on" });
|
|
461
492
|
return meShown(c);
|
|
462
493
|
} },
|
|
463
|
-
{ noun: "me", verb: "set-photo", flags: { path: { ...PATH, about: "a square JPEG, PNG or WebP
|
|
494
|
+
{ noun: "me", verb: "set-photo", flags: { path: { ...PATH, about: "a square JPEG, PNG or WebP, 300 KB at most" } }, about: "Sets the photo shown beside your name.",
|
|
464
495
|
run: (c, a) => c.put("/api/me/photo", { image: c.image(a.path) }) },
|
|
465
496
|
{ noun: "me", verb: "remove-photo", flags: {}, about: "Takes your photo away; it is gone, kept nowhere.", run: (c) => c.del("/api/me/photo") },
|
|
466
|
-
{ noun: "assistant", verb: "question", target: { name: "text", kind: "words" }, flags: { conversation: { kind: "id", about: "to go on from one" }, focus: { kind: "focus", about: "a Project, Company or channel for a new conversation to keep to" }
|
|
497
|
+
{ noun: "assistant", verb: "question", target: { name: "text", kind: "words" }, flags: { conversation: { kind: "id", about: "to go on from one" }, focus: { kind: "focus", about: "a Project, Company or channel for a new conversation to keep to" },
|
|
498
|
+
path: { kind: "path", about: "a file for it to read, kept with the conversation" } },
|
|
467
499
|
about: "Asks Centric's assistant, which answers from what you can see. Only while your own switch is on; it counts against the day's share.",
|
|
468
|
-
run
|
|
500
|
+
async run(c, a) {
|
|
501
|
+
const up = a.path && (await c.upload(a.path)); // uploaded as the person, linked to no message: theirs alone
|
|
502
|
+
return c.post("/api/assist", { q: a.text, conversation: a.conversation, focus: a.focus, ...(up && { files: [{ path: up.path, name: up.name }] }) });
|
|
503
|
+
} },
|
|
469
504
|
{ noun: "assistant", verb: "correct", target: { name: "text", kind: "words" }, flags: {},
|
|
470
505
|
about: "The assistant puts a message's spelling, grammar and punctuation right; it is still yours to send.",
|
|
471
506
|
run: (c, a) => c.post("/api/assist/correct", { text: a.text }) },
|
|
472
507
|
{ noun: "conversation", verb: "delete", target: { name: "conversation", kind: "id" }, flags: {}, about: "Deletes a conversation with the assistant outright; it leaves no tombstone.",
|
|
473
508
|
run: (c, a) => c.del(`/api/assistant/conversations/${a.conversation}`) },
|
|
509
|
+
// A firm's own system its assistant reads from (CONTEXT.md Linked system): its admins link and remove one; its people list them.
|
|
510
|
+
{ noun: "linked-system", verb: "list", flags: { company: { kind: "company", need: true } }, about: "The firm's systems its assistant reads from.",
|
|
511
|
+
run: async (c, a) => listed(await c.get(`/api/containers/${a.company}/linked-systems`)) },
|
|
512
|
+
{ noun: "linked-system", verb: "add", flags: { company: { kind: "company", need: true }, name: { kind: "words", need: true, about: "short, as stores" },
|
|
513
|
+
url: { kind: "url", need: true, about: "its https MCP address" }, credential: { kind: "text", need: true, about: "or - for standard input" } },
|
|
514
|
+
about: "Links the firm's MCP server; its reading tools reach its assistant. Admins only.",
|
|
515
|
+
run: (c, a) => c.post(`/api/containers/${a.company}/linked-systems`, { name: a.name, url: a.url, credential: a.credential }) },
|
|
516
|
+
{ noun: "linked-system", verb: "remove", target: { name: "linked-system", kind: "id" }, flags: {}, about: "Unlinks a firm's system.",
|
|
517
|
+
run: (c, a) => c.del(`/api/linked-systems/${a["linked-system"]}`) },
|
|
518
|
+
// What the assistant keeps about its person (CONTEXT.md Memory): written by the assistant when they tell it to keep
|
|
519
|
+
// something, or by them, keeping a line it offered after reading others' words; seen and deleted by them.
|
|
520
|
+
{ noun: "memory", verb: "list", flags: {}, about: "What the assistant keeps about you, as you told it to.",
|
|
521
|
+
run: async (c) => listed(await c.get("/api/assistant/memories")) },
|
|
522
|
+
{ noun: "memory", verb: "add", flags: { text: { kind: "text", need: true, about: "one line, 300 characters at most" } }, about: "Keeps a line for the assistant to read before each of your questions.",
|
|
523
|
+
run: (c, a) => c.post("/api/assistant/memories", { text: a.text }) },
|
|
524
|
+
{ noun: "memory", verb: "delete", target: { name: "memory", kind: "id" }, flags: {}, about: "Stops the assistant keeping one line about you.",
|
|
525
|
+
run: (c, a) => c.del(`/api/assistant/memories/${a.memory}`) },
|
|
526
|
+
// A Report template (CONTEXT.md): yours, or a place's, written by its staff (a Company's by its admins). A report is
|
|
527
|
+
// written to one by asking the assistant for it.
|
|
528
|
+
...(() => {
|
|
529
|
+
const SECTIONS = { kind: "text", about: "a line each: its heading, a colon, what goes in it" }, PERIOD = { kind: "choice", of: ["day", "week", "since-last"] }, T = { name: "report-template", kind: "id" };
|
|
530
|
+
const lines = (s) => String(s).split("\n").map((l) => l.trim()).filter(Boolean).map((l) => { const i = l.indexOf(":"); return i > 0 ? { heading: l.slice(0, i).trim(), what: l.slice(i + 1).trim() } : { heading: l, what: "" }; });
|
|
531
|
+
const body = (a) => ({ name: a.name, ...(a.sections !== undefined && { sections: lines(a.sections) }), ...(a.period && { period: a.period.replace("-", "_") }) });
|
|
532
|
+
return [
|
|
533
|
+
{ noun: "report-template", verb: "list", flags: {}, about: "Report templates: yours and your places'.", run: async (c) => listed(await c.get("/api/report-templates")) },
|
|
534
|
+
{ noun: "report-template", verb: "show", target: T, flags: {}, about: "One, with who wrote to it and when.", run: (c, a) => c.get(`/api/report-templates/${a["report-template"]}`) },
|
|
535
|
+
{ noun: "report-template", verb: "make", flags: { name: { kind: "words" }, sections: SECTIONS, period: PERIOD, in: { kind: "container", about: "a place's; yours if left out" }, "from-report": { kind: "id", about: "an assistant report of yours" } },
|
|
536
|
+
about: "Makes one, or saves an assistant report as one.", run: (c, a) => c.post("/api/report-templates", { ...body(a), container_id: a.in, from_report: a["from-report"] }) },
|
|
537
|
+
{ noun: "report-template", verb: "edit", target: T, flags: { name: { kind: "words" }, sections: SECTIONS, period: PERIOD }, about: "Changes its name, sections or period.",
|
|
538
|
+
run: (c, a) => c.patch(`/api/report-templates/${a["report-template"]}`, body(a)) },
|
|
539
|
+
{ noun: "report-template", verb: "delete", target: T, flags: {}, about: "Deletes one; assistant reports written to it stay.", run: (c, a) => c.del(`/api/report-templates/${a["report-template"]}`) },
|
|
540
|
+
];
|
|
541
|
+
})(),
|
|
474
542
|
|
|
475
543
|
// ── connected tools ──
|
|
476
544
|
{ noun: "connected-tool", verb: "list", flags: {}, about: "The tools connected to you, with when each was last used.",
|
package/package.json
CHANGED