connection-centric 0.2.0 → 0.4.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 +4 -0
- package/centric.mjs +9 -7
- package/commands.mjs +121 -40
- package/package.json +1 -1
package/GUIDE.md
CHANGED
|
@@ -32,6 +32,7 @@ A tool lapses after 90 days unused. The person disconnects it from Me, or with `
|
|
|
32
32
|
- **company**: the one container a firm holds for work that belongs to no site. Never archived; nobody makes one but Centric, when an Operator approves its Application.
|
|
33
33
|
- **channel**: a named conversation inside a container, and the only place a container's messages live. In a Project it is either visible to externals or staff-only. A Company channel may be private: read only by the people its admins name.
|
|
34
34
|
- **message**: one message in a channel or a Direct. A reply quotes the message it answers. Files sent in a channel are kept as documents.
|
|
35
|
+
- **pinned-message**: a message kept at the top of its channel, Direct or conversation, for everyone in it, at most 50 in one place. In a Project only staff pin; anyone who may pin may unpin. A channel's pins are on the record; a Direct's and a conversation's are not.
|
|
35
36
|
- **direct**: a conversation between two people who share a container, or a person's own with themselves, for notes and what they keep to file later. Name your own `me` (`direct send me`); `direct list` marks it `you`. Nothing in a Direct is part of the record: no task, no issuance, no deleting, and the assistant never reads or corrects one.
|
|
36
37
|
- **task**: an open piece of work in one container, which anyone who can see it may close or reopen. Every task is part of one channel's conversation: made on its own, it is posted there as your message; what is said about it is a reply to that message (`message send --task`). It may carry details, a due day, a priority and a zone. Open past its due day, or with an ask escalated, it is overdue.
|
|
37
38
|
- **ask**: one person's request to another, on a task, closed by closing the task. The person asked may answer not yet, saying why and when to be asked again.
|
|
@@ -48,6 +49,9 @@ A tool lapses after 90 days unused. The person disconnects it from Me, or with `
|
|
|
48
49
|
- **me**: the person themselves: their display name, photo, and their own switch for the assistant.
|
|
49
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.
|
|
50
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`.
|
|
51
55
|
- **application**: a firm asking Centric for its Company. An Operator approves or rejects it, once.
|
|
52
56
|
- **connected-tool**: this command, or any program a person approved; it acts as them.
|
|
53
57
|
|
package/centric.mjs
CHANGED
|
@@ -105,20 +105,20 @@ async function connect(c, a, server) {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
mkdirSync(dir(), { recursive: true, mode: 0o700 });
|
|
108
|
-
writeFileSync(file(), JSON.stringify({ server, credential: got.credential
|
|
108
|
+
writeFileSync(file(), JSON.stringify({ server, credential: got.credential }, null, 2), { mode: 0o600 });
|
|
109
109
|
chmodSync(file(), 0o600); // a file there already keeps its old mode otherwise
|
|
110
110
|
return { tool: got.tool, identity: got.identity, server };
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
// ── printing ──
|
|
114
|
-
const clip = (s
|
|
114
|
+
const clip = (s) => { s = String(s).replace(/\s+/g, " "); return s.length > 70 ? `${s.slice(0, 69)}…` : s; };
|
|
115
115
|
const label = (x) => x.name ?? x.title ?? x.topic_name ?? x.task_title ?? x.document_name ?? x.display_name ?? x.firm_name
|
|
116
|
-
?? (x.text != null ? `${x.sender?.display_name ?? "?"}${x.via ? ` (via ${x.via.name})` : ""}: ${clip(x.text
|
|
116
|
+
?? (x.text != null ? `${x.sender?.display_name ?? "?"}${x.via ? ` (via ${x.via.name})` : ""}: ${clip(x.text)}` : (x.tombstone ?? x).deleted_by_name ? `deleted by ${(x.tombstone ?? x).deleted_by_name}` : null);
|
|
117
117
|
const where = (x) => x.container_name ?? (x.channel ? `${x.channel.name} · ${x.channel.container_name}` : x.direct ? `Direct with ${x.direct.display_name}` : null);
|
|
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) {
|
|
@@ -137,12 +137,14 @@ async function main(argv) {
|
|
|
137
137
|
let json = argv.includes("--json");
|
|
138
138
|
try {
|
|
139
139
|
const { cmd, given, opts, lead } = parse(argv);
|
|
140
|
-
json
|
|
140
|
+
json ||= opts.json === true || opts.json === "true"; // --json=true too, which argv alone does not show
|
|
141
|
+
// Help is one JSON value too under --json (rule 9): a program reading stdout gets what it asked for, or an error.
|
|
142
|
+
const say = (h) => { process.stdout.write(`${json ? asJson({ help: h }) : h}\n`); return 0; };
|
|
141
143
|
if (!cmd) {
|
|
142
|
-
if (!lead.length || lead[0] === "help")
|
|
144
|
+
if (!lead.length || lead[0] === "help") return say(help(lead[0] === "help" && (commandOf(lead[1], null) ?? commandOf(lead[1], lead[2]))));
|
|
143
145
|
throw new Refused(`no command "${lead.slice(0, 2).join(" ")}"; centric help lists them`);
|
|
144
146
|
}
|
|
145
|
-
if (opts.help)
|
|
147
|
+
if (opts.help) return say(help(cmd));
|
|
146
148
|
const file = saved(), server = opts.server || process.env.CENTRIC_SERVER || file.server || DEFAULT_SERVER; // set but empty is not set
|
|
147
149
|
// The credential crosses a network only encrypted: an http:// server is this machine's, for development.
|
|
148
150
|
if (!/^https:\/\//i.test(server) && !/^http:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?(\/|$)/i.test(server)) throw new Refused(`${server} is not https; a credential is sent only over https, or to this machine`);
|
package/commands.mjs
CHANGED
|
@@ -16,7 +16,7 @@ export const CLI_VERSION = JSON.parse(readFileSync(new URL("./package.json", imp
|
|
|
16
16
|
|
|
17
17
|
// Rule 2: the verbs, each meaning one thing on every noun. A new verb is added here, and to the spec, before it is used.
|
|
18
18
|
export const VERBS = ["list", "show", "make", "edit", "delete", "archive", "unarchive", "close", "reopen", "not-yet", "grant", "revoke", "appoint", "remove",
|
|
19
|
-
"issue", "acknowledge", "add", "revise", "restore", "download", "messages", "mark-read", "send", "react", "unreact", "history", "readers",
|
|
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
21
|
"approve", "reject", "withdraw", "rotate-key", "dormant", "reactivate", "report", "record-pack", "audit", "question", "correct", "disconnect"];
|
|
22
22
|
|
|
@@ -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, 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; }
|
|
@@ -103,6 +106,7 @@ const TEXT = { kind: "text", need: true, about: "the words, or - to read them fr
|
|
|
103
106
|
const PATH = { kind: "path", need: true, about: "a file on this computer" };
|
|
104
107
|
const NOTE = { kind: "text", about: "what it is, or what changed" };
|
|
105
108
|
const DRY = { kind: "bool", about: "say what would change, and change nothing" };
|
|
109
|
+
const SEARCH = { kind: "words" }; // only the messages holding these words (Features 523)
|
|
106
110
|
const PAGE = { kind: "path", about: "write it as a page, a file ending .html" };
|
|
107
111
|
// A page the server draws (a report, a record pack), written to this computer: never over a file already there.
|
|
108
112
|
const page = async (c, path, out) => { if (!/\.html?$/i.test(out)) refuse("--out is a page: name it something.html"); const b = await c.bytes(path); await c.save(out, b); return { path: out, bytes: b.length }; };
|
|
@@ -130,11 +134,24 @@ export const COMMANDS = [
|
|
|
130
134
|
...named.items.map((x) => ({ kind: "mention", ...x })), ...directs.items.map((x) => ({ kind: "direct", ...x })),
|
|
131
135
|
]);
|
|
132
136
|
} },
|
|
133
|
-
|
|
137
|
+
// Search (CONTEXT.md Search, ADR 0009): the record by its words, every version of a message, the words inside a PDF, and
|
|
138
|
+
// your own Directs; narrowed by who, where, when, a document's kind and a state.
|
|
139
|
+
{ noun: "search", verb: null, target: { name: "words", kind: "words", optional: true },
|
|
140
|
+
flags: { from: { kind: "identity", about: "only what this person did" },
|
|
141
|
+
in: { kind: "container" }, channel: { kind: "channel" }, task: { kind: "task", about: "only in its conversation" },
|
|
142
|
+
after: { kind: "day", about: "from this day" }, before: { kind: "day", about: "to this day" },
|
|
143
|
+
kind: { kind: "choice", of: ["photo", "pdf", "video", "voice", "spreadsheet", "other"] },
|
|
144
|
+
state: { kind: "choice", of: ["open", "closed", "unacknowledged"], about: "tasks open or closed; issuances not acknowledged" },
|
|
145
|
+
oldest: { kind: "bool", about: "oldest first, not best first" } },
|
|
146
|
+
about: "The record by its words: messages (earlier versions too), documents and their text, tasks, issuances, deletions, channels, people, your own Directs.",
|
|
134
147
|
async run(c, a) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
148
|
+
if (a.channel && a.task) refuse("--channel or --task, not both");
|
|
149
|
+
const by = { from: a.from, in: a.channel ?? a.task ?? a.in, after: a.after, before: a.before, kind: a.kind, state: a.state };
|
|
150
|
+
if (!a.words && !Object.values(by).some(Boolean)) refuse("search needs the words, or one of --from, --in, --channel, --task, --after, --before, --kind and --state");
|
|
151
|
+
const r = await c.get(`/api/search${qs({ q: a.words, ...by, order: a.oldest && "oldest" })}`), revised = new Set(r.revisions.map((x) => x.document_id));
|
|
152
|
+
return listed([...r.messages.map((x) => ({ kind: "message", ...x })), ...r.directs.map((x) => ({ kind: "direct", ...x })), ...r.revisions.map((x) => ({ kind: "revision", ...x })),
|
|
153
|
+
...r.documents.filter((x) => !revised.has(x.id)).map((x) => ({ kind: "document", ...x })), ...r.tasks.map((x) => ({ kind: "task", ...x })), ...r.issuances.map((x) => ({ kind: "issuance", ...x })),
|
|
154
|
+
...r.tombstones.map((x) => ({ kind: "tombstone", ...x })), ...r.channels.map((x) => ({ kind: "channel", ...x })), ...r.zones.map((x) => ({ kind: "zone", ...x })), ...r.people.map((x) => ({ kind: "identity", ...x }))]);
|
|
138
155
|
} },
|
|
139
156
|
{ noun: "guide", verb: null, flags: { as: { kind: "choice", of: ["skill", "agents"], about: "as a Claude Code skill (a SKILL.md), or as a section an AGENTS.md can include" } },
|
|
140
157
|
about: "The instructions for a program using this command: what each noun means, what is refused and why, the mark, the cap, --json, the exit codes, and every command.",
|
|
@@ -153,9 +170,13 @@ export const COMMANDS = [
|
|
|
153
170
|
} },
|
|
154
171
|
|
|
155
172
|
// ── work ──
|
|
156
|
-
{ noun: "task", verb: "list", flags: { in: IN, state: { kind: "choice", of: ["open", "closed"] }, zone: { kind: "zone" } },
|
|
173
|
+
{ noun: "task", verb: "list", flags: { in: IN, 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" } },
|
|
157
174
|
about: "A container's tasks: open ones first, soonest due, then the most important; done ones newest first, a page at a time.",
|
|
158
|
-
run: async (c, a) =>
|
|
175
|
+
run: async (c, a) => {
|
|
176
|
+
if (a.before && a.state !== "closed") refuse("--before pages done tasks: with --state closed");
|
|
177
|
+
const l = await c.get(`/api/containers/${a.in}/tasks${qs({ state: a.state, zone_id: a.zone, before: a.before })}`);
|
|
178
|
+
return listed(l, a.state === "closed" && l.length >= 100 ? l.at(-1).done_key : null); // the server's page of done ones (DONE_PAGE)
|
|
179
|
+
} },
|
|
159
180
|
{ noun: "task", verb: "show", target: { name: "task", kind: "task" }, flags: {}, about: "One task in full: its state, details, due day, priority, zone, and every ask on it with what happened since.",
|
|
160
181
|
run: (c, a) => c.get(`/api/tasks/${a.task}`) },
|
|
161
182
|
{ noun: "ask", verb: "list", flags: { "to-me": { kind: "bool", about: "only asks of you" }, "from-me": { kind: "bool", about: "only your own asks" }, state: { kind: "choice", of: ["open", "closed"], about: "open (the default) or done" } },
|
|
@@ -163,9 +184,9 @@ export const COMMANDS = [
|
|
|
163
184
|
run: (c, a) => (a["to-me"] && a["from-me"] ? refuse("--to-me or --from-me, not both") : c.get(`/api/asks${qs({ to_me: a["to-me"], from_me: a["from-me"], state: a.state })}`).then((x) => listed(x))) },
|
|
164
185
|
{ noun: "direct", verb: "list", flags: {}, about: "Your Directs with something in them, newest first.",
|
|
165
186
|
run: async (c) => listed((await c.get("/api/places")).filter((p) => p.kind === "direct").sort(newest)) },
|
|
166
|
-
{ noun: "direct", verb: "messages", target: { name: "identity", kind: "identity" }, flags: { before: { kind: "message", about: "older than this message" }, limit: { kind: "int", about: "how many, up to 100 (50 if left out)" } },
|
|
187
|
+
{ noun: "direct", verb: "messages", target: { name: "identity", kind: "identity" }, flags: { before: { kind: "message", about: "older than this message" }, limit: { kind: "int", about: "how many, up to 100 (50 if left out)" }, search: SEARCH },
|
|
167
188
|
about: "A Direct's messages, newest last. Marks nothing read.",
|
|
168
|
-
run: (c, a) => c.get(`/api/messages${qs({ direct: a.identity, before: a.before, limit: a.limit })}`) },
|
|
189
|
+
run: (c, a) => c.get(`/api/messages${qs({ direct: a.identity, before: a.before, limit: a.limit, search: a.search })}`) },
|
|
169
190
|
{ noun: "issuance", verb: "list", flags: { "to-me": { kind: "bool", about: "only what was issued to you" }, "by-me": { kind: "bool", about: "only what you issued" }, in: { kind: "container" }, state: { kind: "choice", of: ["open", "acknowledged"] } },
|
|
170
191
|
about: "What was issued to you or by you, newest first.",
|
|
171
192
|
run: (c, a) => (a["to-me"] && a["by-me"] ? refuse("--to-me or --by-me, not both") : c.get(`/api/issuances${qs({ to_me: a["to-me"], by_me: a["by-me"], in: a.in, state: a.state })}`).then((x) => listed(x))) },
|
|
@@ -186,21 +207,22 @@ export const COMMANDS = [
|
|
|
186
207
|
|
|
187
208
|
// ── places in a container ──
|
|
188
209
|
{ noun: "channel", verb: "list", flags: { in: IN, archived: { kind: "bool", about: "the archived ones instead" } }, about: "A container's channels that you read.",
|
|
189
|
-
run: async (c, a) => listed((await c.get(`/api/containers/${a.in}`)).channels.filter((ch) => ch.reads && !ch.archived_at === !a.archived)) },
|
|
210
|
+
run: async (c, a) => listed((await c.get(`/api/containers/${a.in}?documents=0`)).channels.filter((ch) => ch.reads && !ch.archived_at === !a.archived)) },
|
|
190
211
|
{ noun: "channel", verb: "show", target: { name: "channel", kind: "channel" }, flags: {}, about: "One channel: its brief, flag, readers if private, and tombstones.",
|
|
191
212
|
run: (c, a) => c.get(`/api/channels/${a.channel}`) },
|
|
192
213
|
{ noun: "channel", verb: "messages", target: { name: "channel", kind: "channel" },
|
|
193
|
-
flags: { before: { kind: "message", about: "older than this message" }, after: { kind: "message", about: "newer than this message" }, limit: { kind: "int", about: "how many, up to 100 (50 if left out)" }, unread: { kind: "bool", about: "only what you have not read" } },
|
|
214
|
+
flags: { before: { kind: "message", about: "older than this message" }, after: { kind: "message", about: "newer than this message" }, limit: { kind: "int", about: "how many, up to 100 (50 if left out)" }, unread: { kind: "bool", about: "only what you have not read" }, search: SEARCH },
|
|
194
215
|
about: "A channel's messages, newest last, with tombstones where messages were deleted. Marks nothing read.",
|
|
195
|
-
run: (c, a) => (a.before && a.after ? refuse("--before or --after, not both") : c.get(`/api/messages${qs({ channel: a.channel, before: a.before, after: a.after, limit: a.limit, unread: a.unread })}`)) },
|
|
216
|
+
run: (c, a) => (a.before && a.after ? refuse("--before or --after, not both") : c.get(`/api/messages${qs({ channel: a.channel, before: a.before, after: a.after, limit: a.limit, unread: a.unread, search: a.search })}`)) },
|
|
196
217
|
{ noun: "zone", verb: "list", flags: { in: { ...IN, kind: "project" } }, about: "A Project's zones.",
|
|
197
|
-
run: async (c, a) => listed((await c.get(`/api/containers/${a.in}`)).zones) },
|
|
218
|
+
run: async (c, a) => listed((await c.get(`/api/containers/${a.in}?documents=0`)).zones) },
|
|
198
219
|
{ 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.",
|
|
199
|
-
run: async (c, a) => listed((await c.get(`/api/containers/${a.in}`)).
|
|
200
|
-
{ noun: "document", verb: "show", target: { name: "document", kind: "document" }, flags: {
|
|
201
|
-
|
|
220
|
+
run: async (c, a) => listed((await c.get(`/api/containers/${a.in}/documents`)).filter((d) => !a.folder || d.folder_id === a.folder)) },
|
|
221
|
+
{ noun: "document", verb: "show", target: { name: "document", kind: "document" }, flags: { contents: { kind: "bool", about: "with what is inside its current revision" } },
|
|
222
|
+
about: "One document: its revisions, issuances and acknowledgements.",
|
|
223
|
+
run: (c, a) => c.get(`/api/documents/${a.document}${a.contents ? "?contents=1" : ""}`) },
|
|
202
224
|
{ noun: "folder", verb: "list", flags: { in: IN }, about: "A container's folders that you can see.",
|
|
203
|
-
run: async (c, a) => listed((await c.get(`/api/containers/${a.in}`)).folders) },
|
|
225
|
+
run: async (c, a) => listed((await c.get(`/api/containers/${a.in}?documents=0`)).folders) },
|
|
204
226
|
|
|
205
227
|
// ── containers, people and Centric ──
|
|
206
228
|
{ noun: "project", verb: "list", flags: {}, about: "The Projects you belong to.", run: async (c) => listed((await c.get("/api/me")).containers.filter((x) => x.kind === "project")) },
|
|
@@ -210,9 +232,9 @@ export const COMMANDS = [
|
|
|
210
232
|
{ noun: "company", verb: "show", target: { name: "company", kind: "company" }, flags: {}, about: "One Company in full: its brief, people, channels and documents.",
|
|
211
233
|
run: (c, a) => c.get(`/api/containers/${a.company}`) },
|
|
212
234
|
{ noun: "membership", verb: "list", flags: { in: IN }, about: "Who belongs to a container, with each one's designation.",
|
|
213
|
-
run: async (c, a) => listed((await c.get(`/api/containers/${a.in}`)).members.map((m) => ({ id: m.membership_id, ...m }))) },
|
|
235
|
+
run: async (c, a) => listed((await c.get(`/api/containers/${a.in}?documents=0`)).members.map((m) => ({ id: m.membership_id, ...m }))) },
|
|
214
236
|
{ noun: "admin", verb: "list", flags: { company: { kind: "company", need: true } }, about: "A Company's admins.",
|
|
215
|
-
run: async (c, a) => listed((await c.get(`/api/containers/${a.company}`)).members.filter((m) => m.is_admin).map((m) => ({ id: m.admin_id, identity_id: m.identity_id, display_name: m.display_name, designation: m.designation }))) },
|
|
237
|
+
run: async (c, a) => listed((await c.get(`/api/containers/${a.company}?documents=0`)).members.filter((m) => m.is_admin).map((m) => ({ id: m.admin_id, identity_id: m.identity_id, display_name: m.display_name, designation: m.designation }))) },
|
|
216
238
|
{ noun: "identity", verb: "show", target: { name: "identity", kind: "identity" }, flags: {}, about: "One person: their name, and the containers you both belong to with their designation in each.",
|
|
217
239
|
run: (c, a) => c.get(`/api/identities/${a.identity}`) },
|
|
218
240
|
{ noun: "access-link", verb: "list", flags: { in: { ...IN, kind: "project" } }, about: "A Project's access links: who each is for, and whether it was used, withdrawn or ran out.",
|
|
@@ -248,7 +270,7 @@ export const COMMANDS = [
|
|
|
248
270
|
{ noun: "task", verb, target: { name: "task", kind: "task" }, flags: {}, about, run: (c, a) => c.post(`/api/tasks/${a.task}/${verb}`) })),
|
|
249
271
|
{ 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}`) },
|
|
250
272
|
{ 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" } },
|
|
251
|
-
about: "Asks someone to do a task; or,
|
|
273
|
+
about: "Asks someone to do a task; or, with --channel and --title instead of --task, makes it too, posted there, as the app does.",
|
|
252
274
|
async run(c, a) {
|
|
253
275
|
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");
|
|
254
276
|
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;
|
|
@@ -263,20 +285,23 @@ export const COMMANDS = [
|
|
|
263
285
|
about: "Issues a document's revision to someone, who is then asked to acknowledge it.",
|
|
264
286
|
async run(c, a) {
|
|
265
287
|
const d = await c.get(`/api/documents/${a.document}`);
|
|
266
|
-
const r =
|
|
288
|
+
const r = revisionOf(d, a.revision);
|
|
267
289
|
return c.post("/api/issuances", { container_id: d.document.container_id, document_revision_id: r.id, issued_to: a.to });
|
|
268
290
|
} },
|
|
269
291
|
{ 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.",
|
|
270
292
|
run: (c, a) => c.post("/api/issuances", { zulip_message_id: a.message, issued_to: a.to }) },
|
|
271
293
|
{ 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" },
|
|
272
294
|
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" } },
|
|
273
|
-
about: "Sends a message to a channel, or a reply to a task's message where it is talked about.
|
|
295
|
+
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.",
|
|
274
296
|
async run(c, a) {
|
|
275
297
|
if (!a.channel === !a.task) refuse("say where: --channel or --task");
|
|
276
298
|
if (!a.text && !a.path) refuse("say something: --text, or --path for a file");
|
|
277
299
|
const uploads = a.path && (await Promise.all(a.path.map((p) => c.upload(p)))).map((u) => ({ path: u.path, name: u.name }));
|
|
278
300
|
return c.post("/api/messages", { channel: a.channel, task: a.task, text: a.text, reply_to: a["reply-to"], mention: a.mention, mention_all: a["mention-all"], uploads });
|
|
279
301
|
} },
|
|
302
|
+
{ noun: "message", verb: "forward", target: { name: "message", kind: "message" }, flags: { channel: { kind: "channel" }, direct: { kind: "identity" }, "dry-run": { ...DRY, about: "who would newly read it" } },
|
|
303
|
+
about: "Sends a message on, marked with who wrote it and the day, never where; a file as itself.",
|
|
304
|
+
run: (c, a) => c.post(`/api/messages/${a.message}/forward`, { channel: a.channel, direct: a.direct, dry_run: a["dry-run"] }) },
|
|
280
305
|
{ noun: "message", verb: "edit", target: { name: "message", kind: "message" }, flags: { text: TEXT }, about: "Changes the words of a message you sent; Zulip keeps every earlier version.",
|
|
281
306
|
run: (c, a) => c.patch(`/api/messages/${a.message}`, { text: a.text }) },
|
|
282
307
|
{ noun: "message", verb: "delete", target: { name: "message", kind: "message" }, flags: {}, about: "Deletes a channel message, leaving a tombstone where it stood: staff only.",
|
|
@@ -285,12 +310,28 @@ export const COMMANDS = [
|
|
|
285
310
|
run: (c, a) => c.post(`/api/messages/${a.message}/reactions`, { emoji: a.emoji }) },
|
|
286
311
|
{ noun: "message", verb: "unreact", target: { name: "message", kind: "message" }, flags: { emoji: EMOJI }, about: "Takes your reaction back.",
|
|
287
312
|
run: (c, a) => c.del(`/api/messages/${a.message}/reactions`, { emoji: a.emoji }) },
|
|
313
|
+
// A pinned message (CONTEXT.md Pinned message, #146): kept at the top of its conversation, for everyone in it. Given
|
|
314
|
+
// --conversation, the id is one of the assistant's answers there, as conversation show lists them.
|
|
315
|
+
...["pin", "unpin"].map((verb) => ({ noun: "message", verb, target: { name: "message", kind: "message" }, flags: { conversation: { kind: "id", about: "an answer in this conversation of yours" } },
|
|
316
|
+
about: verb === "pin" ? "Pins a message for everyone in its conversation. In a Project, staff pin; 50 at most." : "Takes a message's pin off.",
|
|
317
|
+
run: (c, a) => c[verb === "pin" ? "post" : "del"]("/api/pins", a.conversation ? { conversation: a.conversation, turn: a.message } : { message: a.message }) })),
|
|
318
|
+
{ noun: "pinned-message", verb: "list", flags: { channel: { kind: "channel" }, direct: { kind: "identity" }, conversation: { kind: "id" } },
|
|
319
|
+
about: "A channel's, Direct's or conversation's pinned messages, newest pinned first.",
|
|
320
|
+
run: async (c, a) => {
|
|
321
|
+
const where = ["channel", "direct", "conversation"].filter((k) => a[k]);
|
|
322
|
+
if (where.length !== 1) refuse("say where: --channel, --direct or --conversation");
|
|
323
|
+
return listed((await c.get(`/api/pins${qs({ [where[0]]: a[where[0]] })}`)).pins);
|
|
324
|
+
} },
|
|
288
325
|
{ noun: "direct", verb: "send", target: { name: "identity", kind: "identity" }, flags: { text: TEXT }, about: "Writes to someone in your Direct with them, or to yourself as me. It passes through and is kept nowhere.",
|
|
289
326
|
run: (c, a) => c.post("/api/messages", { direct: a.identity, text: a.text }) },
|
|
290
327
|
{ noun: "channel", verb: "mark-read", target: { name: "channel", kind: "channel" }, flags: { through: { kind: "message", about: "up to and including this message" } }, about: "Read, on purpose: tells its senders their messages were read.",
|
|
291
328
|
run: (c, a) => c.post("/api/messages/read", { channel: a.channel, through: a.through }) },
|
|
292
329
|
{ noun: "direct", verb: "mark-read", target: { name: "identity", kind: "identity" }, flags: {}, about: "Read, on purpose: the whole Direct.",
|
|
293
330
|
run: (c, a) => c.post("/api/messages/read", { direct: a.identity }) },
|
|
331
|
+
// CONTEXT.md Opened: your own assistant reads it until you close it; the Direct tells them each time.
|
|
332
|
+
{ noun: "direct", verb: "edit", target: { name: "identity", kind: "identity" }, flags: { assistant: { kind: "choice", of: ["open", "closed"], need: true } },
|
|
333
|
+
about: "Opens or closes your Direct with them to your own assistant; it tells them.",
|
|
334
|
+
run: (c, a) => c.post(`/api/directs/${a.identity}/assistant`, { open: a.assistant === "open" }) },
|
|
294
335
|
{ 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" } },
|
|
295
336
|
about: "Marks notifications read, by their keys from notification list, or all of them.",
|
|
296
337
|
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 })) },
|
|
@@ -318,7 +359,7 @@ export const COMMANDS = [
|
|
|
318
359
|
about: "Writes the exact bytes kept for a revision to this computer, after checking them against the hash Centric kept.",
|
|
319
360
|
async run(c, a) {
|
|
320
361
|
const d = await c.get(`/api/documents/${a.document}`);
|
|
321
|
-
const r =
|
|
362
|
+
const r = revisionOf(d, a.revision);
|
|
322
363
|
const bytes = await c.bytes(`/api/files?path=${enc(r.zulip_path)}`), sha256_hex = createHash("sha256").update(bytes).digest("hex");
|
|
323
364
|
if (sha256_hex !== r.sha256_hex) throw new Broken(`what arrived does not match the hash kept for revision ${r.n}; nothing was written`);
|
|
324
365
|
const path = a.out ?? (basename(decodeURIComponent(r.zulip_path.split("/").pop())).replace(/^\.+/, "") || "file");
|
|
@@ -337,11 +378,14 @@ export const COMMANDS = [
|
|
|
337
378
|
{ 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.",
|
|
338
379
|
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 }); } },
|
|
339
380
|
...["project", "company"].flatMap((noun) => [
|
|
340
|
-
{ 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" }
|
|
341
|
-
|
|
381
|
+
{ 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" },
|
|
382
|
+
...(noun === "company" && { "documents-to-model": { kind: "choice", of: ["on", "off"], about: "off: none of its files go to a model" } }) },
|
|
383
|
+
about: noun === "project" ? "Renames a Project (its admins), or writes its brief (its staff)." : "Renames a Company, or writes its brief: its admins only.",
|
|
342
384
|
async run(c, a) {
|
|
343
|
-
|
|
344
|
-
|
|
385
|
+
const to = a["documents-to-model"];
|
|
386
|
+
if (a.name === undefined && a.brief === undefined && to === undefined) refuse(`say what changes: --name or --brief${noun === "company" ? " or --documents-to-model" : ""}`);
|
|
387
|
+
const patch = (b) => c.patch(`/api/containers/${a[noun]}`, b);
|
|
388
|
+
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" }))) };
|
|
345
389
|
} },
|
|
346
390
|
{ 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.",
|
|
347
391
|
run: (c, a) => page(c, `/api/containers/${a[noun]}/record-pack.html`, a.out) },
|
|
@@ -352,10 +396,10 @@ export const COMMANDS = [
|
|
|
352
396
|
about: verb === "archive" ? "Takes a Project out of the lists, everything in it kept: admins only." : "Brings an archived Project back.",
|
|
353
397
|
run: (c, a) => c.patch(`/api/containers/${a.project}`, { archived: verb === "archive" }) })),
|
|
354
398
|
{ 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 },
|
|
355
|
-
about: "The Project's report: open tasks by age, response time, overdue by zone. Staff only.",
|
|
399
|
+
about: "The Project's Daily report: open tasks by age, response time, overdue by zone. Staff only.",
|
|
356
400
|
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 })}`)) },
|
|
357
401
|
...[["project", "containers"], ["company", "containers"], ["channel", "channels"]].flatMap(([noun, route]) => [
|
|
358
|
-
{ noun, verb: "set-picture", target: { name: noun, kind: noun }, flags: { path: { ...PATH, about: "a square JPEG, PNG or WebP
|
|
402
|
+
{ noun, verb: "set-picture", target: { name: noun, kind: noun }, flags: { path: { ...PATH, about: "a square JPEG, PNG or WebP, 300 KB at most" } },
|
|
359
403
|
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) }) },
|
|
360
404
|
{ 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`) },
|
|
361
405
|
]),
|
|
@@ -395,10 +439,10 @@ export const COMMANDS = [
|
|
|
395
439
|
{ 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.",
|
|
396
440
|
run: (c, a) => c.post(`/api/containers/${a.company}/admins`, { identity_id: a.identity }) },
|
|
397
441
|
{ 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}`) },
|
|
398
|
-
{ noun: "identity", verb: "rotate-key", target: { name: "identity", kind: "identity" }, flags: { "dry-run": { ...DRY, about: "
|
|
442
|
+
{ 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" } },
|
|
399
443
|
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"] }) },
|
|
400
|
-
{ noun: "identity", verb: "dormant", target: { name: "identity", kind: "identity" }, flags: { in: { kind: "container", about: "where to look for their name" }, "dry-run": { ...DRY, about: "
|
|
401
|
-
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
|
|
444
|
+
{ 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" } },
|
|
445
|
+
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"] }) },
|
|
402
446
|
{ 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.",
|
|
403
447
|
run: (c, a) => c.post(`/api/identities/${a.identity}/reactivate`) },
|
|
404
448
|
{ 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" } },
|
|
@@ -427,17 +471,54 @@ export const COMMANDS = [
|
|
|
427
471
|
if (a.assistant !== undefined || a.language !== undefined) await c.patch("/api/me/assistant", { on: a.assistant && a.assistant === "on", lang: a.language });
|
|
428
472
|
return meShown(c);
|
|
429
473
|
} },
|
|
430
|
-
{ noun: "me", verb: "set-photo", flags: { path: { ...PATH, about: "a square JPEG, PNG or WebP
|
|
474
|
+
{ 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.",
|
|
431
475
|
run: (c, a) => c.put("/api/me/photo", { image: c.image(a.path) }) },
|
|
432
476
|
{ noun: "me", verb: "remove-photo", flags: {}, about: "Takes your photo away; it is gone, kept nowhere.", run: (c) => c.del("/api/me/photo") },
|
|
433
|
-
{ 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" }
|
|
477
|
+
{ 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" },
|
|
478
|
+
path: { kind: "path", about: "a file for it to read, kept with the conversation" } },
|
|
434
479
|
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.",
|
|
435
|
-
run
|
|
436
|
-
|
|
480
|
+
async run(c, a) {
|
|
481
|
+
const up = a.path && (await c.upload(a.path)); // uploaded as the person, linked to no message: theirs alone
|
|
482
|
+
return c.post("/api/assist", { q: a.text, conversation: a.conversation, focus: a.focus, ...(up && { files: [{ path: up.path, name: up.name }] }) });
|
|
483
|
+
} },
|
|
484
|
+
{ noun: "assistant", verb: "correct", target: { name: "text", kind: "words" }, flags: {},
|
|
437
485
|
about: "The assistant puts a message's spelling, grammar and punctuation right; it is still yours to send.",
|
|
438
|
-
run: (c, a) => c.post("/api/assist/correct", { text: a.text
|
|
486
|
+
run: (c, a) => c.post("/api/assist/correct", { text: a.text }) },
|
|
439
487
|
{ noun: "conversation", verb: "delete", target: { name: "conversation", kind: "id" }, flags: {}, about: "Deletes a conversation with the assistant outright; it leaves no tombstone.",
|
|
440
488
|
run: (c, a) => c.del(`/api/assistant/conversations/${a.conversation}`) },
|
|
489
|
+
// A firm's own system its assistant reads from (CONTEXT.md Linked system): its admins link and remove one; its people list them.
|
|
490
|
+
{ noun: "linked-system", verb: "list", flags: { company: { kind: "company", need: true } }, about: "The firm's systems its assistant reads from.",
|
|
491
|
+
run: async (c, a) => listed(await c.get(`/api/containers/${a.company}/linked-systems`)) },
|
|
492
|
+
{ noun: "linked-system", verb: "add", flags: { company: { kind: "company", need: true }, name: { kind: "words", need: true, about: "short, as stores" },
|
|
493
|
+
url: { kind: "url", need: true, about: "its https MCP address" }, credential: { kind: "text", need: true, about: "or - for standard input" } },
|
|
494
|
+
about: "Links the firm's MCP server; its reading tools reach its assistant. Admins only.",
|
|
495
|
+
run: (c, a) => c.post(`/api/containers/${a.company}/linked-systems`, { name: a.name, url: a.url, credential: a.credential }) },
|
|
496
|
+
{ noun: "linked-system", verb: "remove", target: { name: "linked-system", kind: "id" }, flags: {}, about: "Unlinks a firm's system.",
|
|
497
|
+
run: (c, a) => c.del(`/api/linked-systems/${a["linked-system"]}`) },
|
|
498
|
+
// What the assistant keeps about its person (CONTEXT.md Memory): written by the assistant when they tell it to keep
|
|
499
|
+
// something, or by them, keeping a line it offered after reading others' words; seen and deleted by them.
|
|
500
|
+
{ noun: "memory", verb: "list", flags: {}, about: "What the assistant keeps about you, as you told it to.",
|
|
501
|
+
run: async (c) => listed(await c.get("/api/assistant/memories")) },
|
|
502
|
+
{ 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.",
|
|
503
|
+
run: (c, a) => c.post("/api/assistant/memories", { text: a.text }) },
|
|
504
|
+
{ noun: "memory", verb: "delete", target: { name: "memory", kind: "id" }, flags: {}, about: "Stops the assistant keeping one line about you.",
|
|
505
|
+
run: (c, a) => c.del(`/api/assistant/memories/${a.memory}`) },
|
|
506
|
+
// A Report template (CONTEXT.md): yours, or a place's, written by its staff (a Company's by its admins). A report is
|
|
507
|
+
// written to one by asking the assistant for it.
|
|
508
|
+
...(() => {
|
|
509
|
+
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" };
|
|
510
|
+
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: "" }; });
|
|
511
|
+
const body = (a) => ({ name: a.name, ...(a.sections !== undefined && { sections: lines(a.sections) }), ...(a.period && { period: a.period.replace("-", "_") }) });
|
|
512
|
+
return [
|
|
513
|
+
{ noun: "report-template", verb: "list", flags: {}, about: "Report templates: yours and your places'.", run: async (c) => listed(await c.get("/api/report-templates")) },
|
|
514
|
+
{ 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"]}`) },
|
|
515
|
+
{ 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" } },
|
|
516
|
+
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"] }) },
|
|
517
|
+
{ noun: "report-template", verb: "edit", target: T, flags: { name: { kind: "words" }, sections: SECTIONS, period: PERIOD }, about: "Changes its name, sections or period.",
|
|
518
|
+
run: (c, a) => c.patch(`/api/report-templates/${a["report-template"]}`, body(a)) },
|
|
519
|
+
{ 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"]}`) },
|
|
520
|
+
];
|
|
521
|
+
})(),
|
|
441
522
|
|
|
442
523
|
// ── connected tools ──
|
|
443
524
|
{ noun: "connected-tool", verb: "list", flags: {}, about: "The tools connected to you, with when each was last used.",
|
package/package.json
CHANGED