connection-centric 0.1.0 → 0.2.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 +7 -6
- package/README.md +2 -2
- package/centric.mjs +19 -9
- package/commands.mjs +23 -13
- package/package.json +1 -1
package/GUIDE.md
CHANGED
|
@@ -8,7 +8,7 @@ Connection-Centric is where a construction firm, its site staff and its subcontr
|
|
|
8
8
|
|
|
9
9
|
centric connect --name "Claude Code on the site laptop"
|
|
10
10
|
|
|
11
|
-
This prints
|
|
11
|
+
This prints an 8-character code. The person types it on their own phone under Me, Connected tools, Connect a tool, and approves the tool by name, which they may change. The code works once, for 10 minutes. There is no link to open instead: a link can be sent to anyone, and one tap would connect the sender's tool. The credential is kept in the person's own config directory, readable by them alone. For a script on a server, `CENTRIC_CREDENTIAL` and `CENTRIC_SERVER` take its place; `--server` names another server for one command.
|
|
12
12
|
|
|
13
13
|
A tool lapses after 90 days unused. The person disconnects it from Me, or with `centric connected-tool disconnect --this`; Centric can too. Rotating the person's key, or putting them to sleep, disconnects every tool they have. A Company's admin cannot disconnect one.
|
|
14
14
|
|
|
@@ -20,7 +20,7 @@ A tool lapses after 90 days unused. The person disconnects it from Me, or with `
|
|
|
20
20
|
- A target is an id, or an exact name that only one thing the person can see has, in any case. Two things with that name, and the command is refused, listing both with their ids. Output always carries ids: pass ids.
|
|
21
21
|
- A person is named by identity id, by phone number in E.164 (`+` and the country code, then the number, no spaces: `+91` and ten digits in India), or by exact display name.
|
|
22
22
|
- `--in <project|company>` says where, for anything inside a container, and narrows a name to that container.
|
|
23
|
-
- The audience is never assumed. Making a channel
|
|
23
|
+
- The audience is never assumed. Making a channel or document in a Project needs `--externals visible` or `--externals hidden`. In a Company everyone sees it, and the flag is refused. A task takes its audience from where it is talked about: posted in a channel (`task make --channel`), that channel's; raised from a message (`--from-message`), that message's channel's, or staff only with `--externals hidden`.
|
|
24
24
|
- A mention is only ever a flag: `--mention <identity>`, again for each, or `--mention-all`. Writing `@Name` in the text reaches nobody.
|
|
25
25
|
- `--text -` reads the words from standard input.
|
|
26
26
|
- `--path` names a file on this computer. Over MCP it is a data: URL instead: `data:<type>;name=<file name>;base64,<bytes>`.
|
|
@@ -32,8 +32,8 @@ 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
|
-
- **direct**: a conversation between two people who share a container. Nothing in
|
|
36
|
-
- **task**: an open piece of work in one container, which anyone who can see it may close or reopen. 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.
|
|
35
|
+
- **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
|
+
- **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
37
|
- **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.
|
|
38
38
|
- **issuance**: a document revision or an instruction put to a named person, who must acknowledge it. Acknowledging is deliberate and is the record; reading or downloading is never one.
|
|
39
39
|
- **document**: a file kept as a series of revisions; a new revision never replaces an old one. Every photo or file sent in a channel is one.
|
|
@@ -54,7 +54,7 @@ A tool lapses after 90 days unused. The person disconnects it from Me, or with `
|
|
|
54
54
|
## What is refused, and why
|
|
55
55
|
|
|
56
56
|
- Anything the person could not do in the app. An external never sees a staff-only channel, task or document; a private channel is read only by the people named in it; only an admin changes who belongs, and only staff delete.
|
|
57
|
-
- **The cap.** What reaches someone else's phone (asks, issuances, mentions, `@all`, Directs) is capped at 30 an hour for each tool. Past it, the command is refused with `retry_after`, the seconds until it may try again. Reading and closing are never capped.
|
|
57
|
+
- **The cap.** What reaches someone else's phone (asks, issuances, mentions, `@all`, Directs with someone else) is capped at 30 an hour for each tool. Past it, the command is refused with `retry_after`, the seconds until it may try again. Reading and closing are never capped.
|
|
58
58
|
- A tool is never connected from a tool: approving one is the person's own act, on their phone.
|
|
59
59
|
- `--dry-run`, on the acts the app warns about before doing them, says what would change and changes nothing: `channel edit --externals` (how many messages change who can see them), `channel private` and `channel open-to-company` (who gains it and who loses it), `membership revoke` (what they lose), and `identity dormant` and `identity rotate-key` (every firm they belong to, and every tool that would disconnect).
|
|
60
60
|
|
|
@@ -74,7 +74,8 @@ Fetching never marks anything read. A sender told their message was read must ha
|
|
|
74
74
|
|
|
75
75
|
- Text for a person by default. With `--json`, exactly one JSON value on stdout: the object for `show`, `make`, `edit` and every act, or `{"items": […], "more": <cursor or null>}` for `list` and the places. Field names are the API's own.
|
|
76
76
|
- Days are `YYYY-MM-DD` in India's time zone; moments are ISO 8601 with `+05:30`.
|
|
77
|
-
- `
|
|
77
|
+
- A message has `id`, `at`, `sender`, `text` and where it was said, `channel` or `direct`, among its fields. One that shows a photo or file has `document` too, its `id`, `name` and `revision_n`: `document download <id> --revision <n>` fetches what the message showed. Among a channel's messages, a deleted one stands where it was as `id` and `tombstone` (`deleted_at`, `deleted_by_name`), with no `at`, `sender` or `text`.
|
|
78
|
+
- `0` done. `1` refused: not allowed, not valid, over a limit, or an ambiguous target. `2` broken: the network, the server, or a command line older than the server accepts, which says how to update. The command line asks a read again once before calling it broken; over MCP, that is the client's to do. A write is never asked again; broken on the way, it says it may have been done, so look before doing it twice.
|
|
78
79
|
- A refusal under `--json` prints `{"error": {"kind": "refused" | "broken", "message": …, "retry_after"?: …, "candidates"?: […]}}` on stderr.
|
|
79
80
|
|
|
80
81
|
## Over MCP
|
package/README.md
CHANGED
|
@@ -5,11 +5,11 @@ The command line for [Connection-Centric](https://app.centric.business): what on
|
|
|
5
5
|
npm install --global connection-centric
|
|
6
6
|
centric connect --name "Claude Code on the site laptop"
|
|
7
7
|
|
|
8
|
-
`connect` prints a
|
|
8
|
+
`connect` prints a code. Type it on your own phone, in the app under Me, Connected tools, Connect a tool, and approve the tool. Never type a code someone sent you: it would connect their tool as you. From then on:
|
|
9
9
|
|
|
10
10
|
centric home # what needs you now
|
|
11
11
|
centric round # what has something unread, addressed to you first
|
|
12
|
-
centric task make --
|
|
12
|
+
centric task make --channel "Structural works" --title "Check level 5 props"
|
|
13
13
|
centric guide # everything else: every command, what is refused and why
|
|
14
14
|
|
|
15
15
|
Everything the tool does carries its name, for everyone who sees it. It is disconnected from Me in the app, and lapses after 90 days unused. Node 22 or later; no dependencies. The same commands are tools of the MCP server at `https://app.centric.business/api/mcp`.
|
package/centric.mjs
CHANGED
|
@@ -54,14 +54,22 @@ function caller(server, credential) {
|
|
|
54
54
|
// JSON both ways; or a file's bytes up (file), or down (bytes: true).
|
|
55
55
|
const call = async (method, path, body, { anon = false, raw = false, file = null, bytes = false } = {}) => {
|
|
56
56
|
if (!anon && !credential) throw new Refused("this machine is not connected; run centric connect");
|
|
57
|
+
const headers = { "x-centric-cli": CLI_VERSION, ...(anon ? {} : { authorization: `Bearer ${credential}` }), ...(file ? { "content-type": TYPES[extname(file.name).toLowerCase()] ?? "application/octet-stream", "x-centric-name": encodeURIComponent(file.name) } : body ? { "content-type": "application/json" } : {}) };
|
|
58
|
+
// A read lost to a restart (a deploy's is under a second: the connection drops, or the proxy answers 502 to 504) is
|
|
59
|
+
// asked again once; a write never is, since it may have been done, and an agent told so looks before doing it twice.
|
|
60
|
+
const lost = (status) => status >= 502 && status <= 504, unsure = method === "GET" ? "" : "; whether it was done is not known, so look before doing it again";
|
|
57
61
|
let r;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
for (let tries = method === "GET" ? 2 : 1; ; await sleep(2000)) {
|
|
63
|
+
try { r = await fetch(new URL(path, server), { method, headers, body: file ? file.bytes : body ? JSON.stringify(body) : undefined }); } catch (e) {
|
|
64
|
+
if (--tries) continue;
|
|
65
|
+
throw new Broken(`could not reach ${server}: ${e.cause?.code ?? e.message}${unsure}`);
|
|
66
|
+
}
|
|
67
|
+
if (!lost(r.status) || !--tries) break;
|
|
68
|
+
await r.body?.cancel(); // its connection goes back, not held for an answer nobody reads
|
|
69
|
+
}
|
|
62
70
|
if (bytes && r.ok) return Buffer.from(await r.arrayBuffer());
|
|
63
71
|
const j = await r.json().catch(() => null);
|
|
64
|
-
if (r.status === 426 || r.status >= 500) throw new Broken(j?.error ?? `the server answered ${r.status}`);
|
|
72
|
+
if (r.status === 426 || r.status >= 500) throw new Broken(`${j?.error ?? `the server answered ${r.status}`}${lost(r.status) ? unsure : ""}`);
|
|
65
73
|
if (r.status >= 400) throw new Refused(j?.error ?? `refused (${r.status})`, { ...(j?.retry_after != null && { retry_after: j.retry_after }) });
|
|
66
74
|
return raw ? { status: r.status, body: j } : j;
|
|
67
75
|
};
|
|
@@ -81,14 +89,15 @@ function caller(server, credential) {
|
|
|
81
89
|
return c;
|
|
82
90
|
}
|
|
83
91
|
|
|
84
|
-
// The pairing (ADR 0008): a
|
|
92
|
+
// The pairing (ADR 0008): a code the person types on their phone, then the credential once they approve. Never a
|
|
93
|
+
// link: one can be sent to anyone, and a tap would connect the sender's tool.
|
|
85
94
|
async function connect(c, a, server) {
|
|
86
95
|
const name = a.name ?? hostname().slice(0, 60);
|
|
87
96
|
let got;
|
|
88
97
|
if (a["dev-phone"]) got = await c.call("POST", "/api/connected-tools/dev", { phone: a["dev-phone"], name }, { anon: true });
|
|
89
98
|
else {
|
|
90
99
|
const p = await c.call("POST", "/api/connected-tools", { name }, { anon: true });
|
|
91
|
-
process.stderr.write(`To connect "${p.name}",
|
|
100
|
+
process.stderr.write(`To connect "${p.name}", type ${p.code} in the app, under Me, Connected tools, Connect a tool.\nThe code works once, for ${Math.round(p.expires_in / 60)} minutes. Type it on your own phone; never send it to anyone.\n`);
|
|
92
101
|
for (;;) {
|
|
93
102
|
const r = await c.call("POST", `/api/connected-tools/${p.id}/collect`, { poll: p.poll }, { anon: true, raw: true });
|
|
94
103
|
if (r.status !== 202) { got = r.body; break; }
|
|
@@ -104,9 +113,10 @@ async function connect(c, a, server) {
|
|
|
104
113
|
// ── printing ──
|
|
105
114
|
const clip = (s, n = 90) => { s = String(s).replace(/\s+/g, " "); return s.length > n ? `${s.slice(0, n - 1)}…` : s; };
|
|
106
115
|
const label = (x) => x.name ?? x.title ?? x.topic_name ?? x.task_title ?? x.document_name ?? x.display_name ?? x.firm_name
|
|
107
|
-
?? (x.text != null ? `${x.sender?.display_name ?? "?"}${x.via ? ` (via ${x.via.name})` : ""}: ${clip(x.text, 70)}` : x.tombstone ? `deleted by ${x.tombstone.deleted_by_name}` : null);
|
|
116
|
+
?? (x.text != null ? `${x.sender?.display_name ?? "?"}${x.via ? ` (via ${x.via.name})` : ""}: ${clip(x.text, 70)}` : (x.tombstone ?? x).deleted_by_name ? `deleted by ${(x.tombstone ?? x).deleted_by_name}` : null);
|
|
108
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);
|
|
109
|
-
|
|
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
|
+
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));
|
|
110
120
|
// Words others wrote, on a terminal: control characters (escape sequences among them) go, but a line and a tab stay.
|
|
111
121
|
const plain = (s) => s.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "");
|
|
112
122
|
// JSON escapes the first 32 itself; the C1 ones a terminal may also obey are escaped too, so nothing is lost.
|
package/commands.mjs
CHANGED
|
@@ -96,7 +96,7 @@ export async function findTarget(get, kind, ref, within) {
|
|
|
96
96
|
refuse(rows.length ? `${rows.length} things you can see are called "${ref}"; name one by its id` : `nothing called "${ref}" that you can see is a ${kind}${within ? " there" : ""}`, rows.length ? { candidates: rows } : {});
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
const IN = { kind: "container", need: true, about: "the Project or Company
|
|
99
|
+
const IN = { kind: "container", need: true, about: "the Project or Company" };
|
|
100
100
|
const EXTERNALS = { kind: "choice", of: ["visible", "hidden"], about: "in a Project, whether its externals see it; never in a Company" };
|
|
101
101
|
const PRIORITY = { kind: "choice", of: ["high", "normal", "low"] };
|
|
102
102
|
const TEXT = { kind: "text", need: true, about: "the words, or - to read them from standard input" };
|
|
@@ -225,9 +225,19 @@ export const COMMANDS = [
|
|
|
225
225
|
async run(c) { const m = await c.get("/api/me"); return listed(m.operator ? await c.get("/api/applications") : m.application ? [m.application] : []); } },
|
|
226
226
|
|
|
227
227
|
// ── the day's work ──
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
// Every task is part of one channel's conversation (CONTEXT.md Task): posted there as your message, or raised from one.
|
|
229
|
+
{ noun: "task", verb: "make", flags: { channel: { kind: "channel", about: "the channel it is posted in, as your message; its audience is the channel's" }, "from-message": { kind: "message", about: "the message it is raised from, instead" },
|
|
230
|
+
in: { kind: "container", about: "with --from-message: the Project or Company it is in" }, title: { kind: "words", need: true }, externals: { kind: "choice", of: ["hidden"], about: "with --from-message: staff only, narrower than the message's channel" },
|
|
231
|
+
details: { kind: "text" }, due: { kind: "day" }, priority: PRIORITY, zone: { kind: "zone" }, document: { kind: "document", about: "a document it points at" } },
|
|
232
|
+
about: "Makes a task: posted in a channel as your message, which is where it is talked about; or raised from a message already said.",
|
|
233
|
+
async run(c, a) {
|
|
234
|
+
if (!a.channel === !a["from-message"]) refuse("say where it is talked about: --channel to post it there, or --from-message with --in");
|
|
235
|
+
if (a.channel && (a.in || a.externals)) refuse("a task posted in a channel is that channel's container and audience: leave --in and --externals off");
|
|
236
|
+
if (a["from-message"] && !a.in) refuse("--from-message needs --in, the Project or Company the message is in");
|
|
237
|
+
const fields = { title: a.title, details: a.details, due_on: a.due, priority: a.priority, zone_id: a.zone, document_id: a.document };
|
|
238
|
+
if (a.channel) return c.post(`/api/containers/${(await c.get(`/api/channels/${a.channel}`)).container_id}/tasks`, { ...fields, channel_id: a.channel });
|
|
239
|
+
return c.post(`/api/containers/${a.in}/tasks`, { ...fields, source_message_id: a["from-message"], ...(a.externals && { externals_visible: false }) });
|
|
240
|
+
} },
|
|
231
241
|
{ noun: "task", verb: "edit", target: { name: "task", kind: "task" }, flags: { title: { kind: "words" }, details: { kind: "text", none: true }, due: { kind: "day", none: true }, priority: PRIORITY, zone: { kind: "zone", none: true } },
|
|
232
242
|
about: "Changes the named fields of a task; only what is sent changes. none clears the details, due day or zone.",
|
|
233
243
|
async run(c, a) {
|
|
@@ -237,11 +247,11 @@ export const COMMANDS = [
|
|
|
237
247
|
...[["close", "Marks a task done: whoever can see a task may close it, and the asks on it close with it."], ["reopen", "Opens a done task again."]].map(([verb, about]) => (
|
|
238
248
|
{ noun: "task", verb, target: { name: "task", kind: "task" }, flags: {}, about, run: (c, a) => c.post(`/api/tasks/${a.task}/${verb}`) })),
|
|
239
249
|
{ 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}`) },
|
|
240
|
-
{ noun: "ask", verb: "make", flags: { task: { kind: "task", about: "the task to ask about" }, to: { kind: "identity", need: true },
|
|
241
|
-
about: "Asks someone to do a task; or, given --
|
|
250
|
+
{ 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, given --channel and --title instead of --task, makes the task too, posted in that channel, as the app does.",
|
|
242
252
|
async run(c, a) {
|
|
243
|
-
if (a.task ? a.title || a.
|
|
244
|
-
const task = a.task ?? (await c.post(`/api/containers/${a.
|
|
253
|
+
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
|
+
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;
|
|
245
255
|
return c.post(`/api/tasks/${task}/asks`, { to_id: a.to });
|
|
246
256
|
} },
|
|
247
257
|
{ noun: "ask", verb: "not-yet", target: { name: "ask", kind: "id" }, flags: { reason: { kind: "choice", of: Object.keys(REASONS), need: true }, again: { kind: "choice", of: ["today", "tomorrow", "due", "week"], about: "when to be asked again; tomorrow if left out" } },
|
|
@@ -258,9 +268,9 @@ export const COMMANDS = [
|
|
|
258
268
|
} },
|
|
259
269
|
{ 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.",
|
|
260
270
|
run: (c, a) => c.post("/api/issuances", { zulip_message_id: a.message, issued_to: a.to }) },
|
|
261
|
-
{ noun: "message", verb: "send", flags: { channel: { kind: "channel" }, task: { kind: "task", about: "
|
|
271
|
+
{ 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" },
|
|
262
272
|
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" } },
|
|
263
|
-
about: "Sends a message to a channel, or to a task's
|
|
273
|
+
about: "Sends a message to a channel, or a reply to a task's message where it is talked about. A mention is only ever a flag: @Name in the text reaches nobody. Files sent with --path are kept as documents, the words the last one's caption.",
|
|
264
274
|
async run(c, a) {
|
|
265
275
|
if (!a.channel === !a.task) refuse("say where: --channel or --task");
|
|
266
276
|
if (!a.text && !a.path) refuse("say something: --text, or --path for a file");
|
|
@@ -275,7 +285,7 @@ export const COMMANDS = [
|
|
|
275
285
|
run: (c, a) => c.post(`/api/messages/${a.message}/reactions`, { emoji: a.emoji }) },
|
|
276
286
|
{ noun: "message", verb: "unreact", target: { name: "message", kind: "message" }, flags: { emoji: EMOJI }, about: "Takes your reaction back.",
|
|
277
287
|
run: (c, a) => c.del(`/api/messages/${a.message}/reactions`, { emoji: a.emoji }) },
|
|
278
|
-
{ noun: "direct", verb: "send", target: { name: "identity", kind: "identity" }, flags: { text: TEXT }, about: "Writes to someone in your Direct with them. It passes through and is kept nowhere.",
|
|
288
|
+
{ 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.",
|
|
279
289
|
run: (c, a) => c.post("/api/messages", { direct: a.identity, text: a.text }) },
|
|
280
290
|
{ 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.",
|
|
281
291
|
run: (c, a) => c.post("/api/messages/read", { channel: a.channel, through: a.through }) },
|
|
@@ -388,8 +398,8 @@ export const COMMANDS = [
|
|
|
388
398
|
{ noun: "identity", verb: "rotate-key", target: { name: "identity", kind: "identity" }, flags: { "dry-run": { ...DRY, about: "every firm they belong to, and every tool of theirs that would disconnect" } },
|
|
389
399
|
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"] }) },
|
|
390
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: "every firm they belong to, and every tool of theirs that would disconnect" } },
|
|
391
|
-
about: "Puts a person to sleep, as after their number changed hands: every membership ends, and every tool of theirs is disconnected.", run: (c, a) => c.post(`/api/identities/${a.identity}/dormant`, { dry_run: a["dry-run"] }) },
|
|
392
|
-
{ 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.",
|
|
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 the id a firm's admin gives them from identity show; an admin revokes the membership.", run: (c, a) => c.post(`/api/identities/${a.identity}/dormant`, { dry_run: a["dry-run"] }) },
|
|
402
|
+
{ 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.",
|
|
393
403
|
run: (c, a) => c.post(`/api/identities/${a.identity}/reactivate`) },
|
|
394
404
|
{ 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" } },
|
|
395
405
|
about: "Makes an access link for someone with no account: what externals may see, and one reply. Staff only.",
|
package/package.json
CHANGED