@wirecat/cli-core 0.545.0 → 0.547.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/CHANGELOG.md +228 -0
- package/dist/calendar/busy.js +4 -1
- package/dist/calendar/cli/answers.d.ts +31 -0
- package/dist/calendar/cli/answers.js +8 -0
- package/dist/calendar/cli/index.d.ts +6 -0
- package/dist/calendar/cli/index.js +43 -4
- package/dist/calendar/cli/remind.js +2 -1
- package/dist/calendar/expand.d.ts +17 -0
- package/dist/calendar/expand.js +340 -0
- package/dist/calendar/feeds.d.ts +96 -0
- package/dist/calendar/feeds.js +324 -0
- package/dist/calendar/ics.d.ts +15 -4
- package/dist/calendar/ics.js +18 -7
- package/dist/calendar/index.d.ts +3 -1
- package/dist/calendar/index.js +2 -0
- package/dist/calendar/server-search.js +3 -1
- package/dist/calendar/writes.js +15 -4
- package/dist/cli/after-dashes.d.ts +16 -0
- package/dist/cli/after-dashes.js +41 -0
- package/dist/cli/bot/context.d.ts +1 -1
- package/dist/cli/bot/context.js +9 -1
- package/dist/cli/bot/messages.js +2 -0
- package/dist/cli/bot/setup.js +1 -0
- package/dist/cli/contract-templates.js +2 -2
- package/dist/cli/entry.d.ts +2 -0
- package/dist/cli/entry.js +3 -2
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/messenger/admin-commands.js +5 -1
- package/dist/cli/messenger/admin-contacts-command.js +5 -1
- package/dist/cli/messenger/admin-folders-command.js +5 -1
- package/dist/cli/messenger/complete-command.js +1 -0
- package/dist/cli/messenger/history-query.d.ts +10 -4
- package/dist/cli/messenger/history-query.js +87 -31
- package/dist/cli/messenger/messages-command.js +12 -23
- package/dist/cli/messenger/messages-comments-command.js +4 -2
- package/dist/cli/messenger/messages-edit-command.js +1 -0
- package/dist/cli/messenger/messages-press-command.js +1 -0
- package/dist/cli/messenger/messages-send-command.js +1 -0
- package/dist/cli/messenger/observed.js +2 -2
- package/dist/cli/messenger/polls-command.js +1 -0
- package/dist/cli/messenger/private-people-command.js +5 -1
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/messenger/stored.js +56 -3
- package/dist/cli/messenger/topics-command.js +1 -1
- package/dist/cli/option-hints.d.ts +6 -0
- package/dist/cli/option-hints.js +15 -0
- package/dist/cli/program.js +17 -6
- package/dist/cli/schedule/schedule-command.js +1 -0
- package/dist/domain/message-link.d.ts +3 -0
- package/dist/domain/message-link.js +9 -14
- package/dist/domain/money-words.d.ts +11 -0
- package/dist/domain/money-words.js +38 -0
- package/dist/domain/providers.d.ts +2 -0
- package/dist/domain/providers.js +15 -3
- package/dist/domain/ref-arguments.d.ts +31 -0
- package/dist/domain/ref-arguments.js +79 -0
- package/dist/domain/references.d.ts +20 -2
- package/dist/domain/references.js +132 -47
- package/dist/errors/reasons.js +31 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mail/cli/index.js +4 -4
- package/dist/mail/index.d.ts +2 -0
- package/dist/mail/index.js +2 -0
- package/dist/mail/partial.d.ts +34 -0
- package/dist/mail/partial.js +94 -0
- package/dist/mail/server-search.js +17 -9
- package/dist/mail/source.d.ts +13 -2
- package/dist/mail/source.js +7 -1
- package/dist/mail/sync-types.d.ts +112 -0
- package/dist/mail/sync-types.js +1 -0
- package/dist/mail/sync.d.ts +4 -106
- package/dist/mail/sync.js +49 -4
- package/dist/mail/testing/fake.d.ts +3 -1
- package/dist/mail/testing/fake.js +10 -2
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/oauth/client-file.d.ts +14 -0
- package/dist/oauth/client-file.js +28 -0
- package/dist/oauth/index.d.ts +1 -0
- package/dist/oauth/index.js +1 -0
- package/dist/ports/messenger.d.ts +3 -2
- package/dist/resolve.d.ts +3 -1
- package/dist/resolve.js +8 -0
- package/dist/search/task-signature.d.ts +5 -1
- package/dist/search/task-signature.js +27 -9
- package/dist/services/agenda.d.ts +5 -0
- package/dist/services/agenda.js +3 -1
- package/dist/services/deps.d.ts +0 -2
- package/dist/services/deps.js +0 -2
- package/dist/services/documents/ingestion.js +1 -1
- package/dist/services/documents/knowledge.d.ts +10 -6
- package/dist/services/documents/knowledge.js +25 -11
- package/dist/services/index.d.ts +6 -1
- package/dist/services/index.js +6 -1
- package/dist/services/message-resolution.d.ts +4 -1
- package/dist/services/message-resolution.js +19 -3
- package/dist/services/messages.d.ts +2 -0
- package/dist/services/messages.js +40 -36
- package/dist/services/money-words.d.ts +3 -0
- package/dist/services/money-words.js +5 -0
- package/dist/services/proposal-tasks.d.ts +3 -2
- package/dist/services/proposal-tasks.js +5 -2
- package/dist/services/proposals/ids.d.ts +5 -0
- package/dist/services/proposals/ids.js +43 -0
- package/dist/services/proposals/index.d.ts +3 -0
- package/dist/services/proposals/index.js +2 -0
- package/dist/services/proposals/kinds/action.d.ts +7 -0
- package/dist/services/proposals/kinds/action.js +45 -0
- package/dist/services/proposals/kinds/fact.d.ts +4 -0
- package/dist/services/proposals/kinds/fact.js +43 -0
- package/dist/services/proposals/kinds/findings.d.ts +7 -0
- package/dist/services/proposals/kinds/findings.js +75 -0
- package/dist/services/proposals/kinds/key-document.d.ts +9 -0
- package/dist/services/proposals/kinds/key-document.js +75 -0
- package/dist/services/proposals/kinds/memory.d.ts +4 -0
- package/dist/services/proposals/kinds/memory.js +42 -0
- package/dist/services/proposals/kinds/relationship.d.ts +10 -0
- package/dist/services/proposals/kinds/relationship.js +82 -0
- package/dist/services/proposals/kinds/shared.d.ts +25 -0
- package/dist/services/proposals/kinds/shared.js +49 -0
- package/dist/services/proposals/registry.d.ts +9 -0
- package/dist/services/proposals/registry.js +129 -0
- package/dist/services/proposals/types.d.ts +85 -0
- package/dist/services/proposals/types.js +1 -0
- package/dist/services/questions.d.ts +7 -2
- package/dist/services/questions.js +13 -4
- package/dist/services/server-search.d.ts +7 -4
- package/dist/services/server-search.js +5 -2
- package/dist/services/stored-project-gathering.d.ts +9 -0
- package/dist/services/stored-project-gathering.js +9 -4
- package/dist/services/task-categories.d.ts +2 -0
- package/dist/services/task-categories.js +19 -8
- package/dist/services/tasks.js +8 -4
- package/dist/store/decisions.d.ts +65 -0
- package/dist/store/decisions.js +55 -0
- package/dist/store/evidence-findings.d.ts +6 -6
- package/dist/store/index.d.ts +4 -3
- package/dist/store/index.js +2 -1
- package/dist/store/meeting-findings.d.ts +2 -0
- package/dist/store/messages.d.ts +4 -9
- package/dist/store/sqlite/actors.d.ts +13 -2
- package/dist/store/sqlite/actors.js +24 -1
- package/dist/store/sqlite/email-locations.d.ts +16 -0
- package/dist/store/sqlite/email-locations.js +26 -0
- package/dist/store/sqlite/emails.d.ts +6 -2
- package/dist/store/sqlite/emails.js +2 -1
- package/dist/store/sqlite/evidence-findings.js +13 -3
- package/dist/store/sqlite/evidence-sources.js +2 -2
- package/dist/store/sqlite/facts.d.ts +16 -8
- package/dist/store/sqlite/facts.js +48 -15
- package/dist/store/sqlite/findings-core.d.ts +2 -1
- package/dist/store/sqlite/findings-core.js +4 -4
- package/dist/store/sqlite/identities.js +1 -1
- package/dist/store/sqlite/knowledge.d.ts +43 -7
- package/dist/store/sqlite/knowledge.js +62 -17
- package/dist/store/sqlite/meeting-findings.js +6 -6
- package/dist/store/sqlite/memories.d.ts +12 -8
- package/dist/store/sqlite/memories.js +33 -10
- package/dist/store/sqlite/notes.d.ts +5 -1
- package/dist/store/sqlite/notes.js +22 -6
- package/dist/store/sqlite/open-store.d.ts +4 -1
- package/dist/store/sqlite/open-store.js +8 -2
- package/dist/store/sqlite/open.d.ts +3 -0
- package/dist/store/sqlite/proposed-actions.d.ts +21 -8
- package/dist/store/sqlite/proposed-actions.js +29 -9
- package/dist/store/sqlite/reads.d.ts +13 -2
- package/dist/store/sqlite/reads.js +8 -4
- package/dist/store/sqlite/relation-proposals.d.ts +34 -0
- package/dist/store/sqlite/relation-proposals.js +114 -0
- package/dist/store/sqlite/things.d.ts +0 -1
- package/dist/store/sqlite/things.js +25 -24
- package/dist/store/store.d.ts +3 -0
- package/dist/store/store.js +2 -0
- package/package.json +1 -1
package/dist/cli/program.js
CHANGED
|
@@ -13,6 +13,7 @@ import { recorded, wasSettled } from "../runs/recording.js";
|
|
|
13
13
|
import { runContextOf } from "../runs/run-context.js";
|
|
14
14
|
import { processRun, useAsProcessRun } from "../runs/scope.js";
|
|
15
15
|
import { AUTHORITY_KEY, BOT_API_KEY, isBotApi, keyForCommand } from "../sends/permissions.js";
|
|
16
|
+
import { liftOptionsAfterDashes } from "./after-dashes.js";
|
|
16
17
|
import { logParsedArgv, logVersion } from "./argv-log.js";
|
|
17
18
|
import { commandAvailability } from "./command-availability.js";
|
|
18
19
|
import { commandPathOf } from "./command-contract.js";
|
|
@@ -135,15 +136,16 @@ export const run = async (argv, definition, options = {}) => {
|
|
|
135
136
|
useAsProcessRun(outer);
|
|
136
137
|
}
|
|
137
138
|
};
|
|
139
|
+
const isMachine = (words) => words.some((word) => word === "--json" || word.startsWith("--json=") || word === "--jsonl");
|
|
138
140
|
const runOnce = async (argv, definition, options) => {
|
|
139
141
|
const streams = options.streams ?? processStreams;
|
|
140
142
|
if (!options.streams)
|
|
141
143
|
quietBrokenPipes();
|
|
142
144
|
const delimiter = argv.indexOf("--");
|
|
143
145
|
const flags = argv.slice(0, delimiter < 0 ? undefined : delimiter);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
let machine = isMachine(flags);
|
|
147
|
+
let debug = flags.includes("--debug");
|
|
148
|
+
let reporting = machine ? { ...options, tty: false } : options;
|
|
147
149
|
const control = execution(streams, { maxOutputBytes: 0 });
|
|
148
150
|
let restoreInput;
|
|
149
151
|
let signalExit;
|
|
@@ -439,12 +441,21 @@ const runOnce = async (argv, definition, options) => {
|
|
|
439
441
|
forEachCommand(program, (child) => child.exitOverride());
|
|
440
442
|
// Before commander, not inside it: commander has no hook that runs before it decides which
|
|
441
443
|
// subcommand it is looking at.
|
|
442
|
-
const
|
|
444
|
+
const profiled = liftProfile(argv, commandWords(program), valuedOptions(program));
|
|
445
|
+
const { profile } = profiled;
|
|
446
|
+
// An agent's call builds its own argv, with its values after `--` on purpose: none of them may become an option.
|
|
447
|
+
const lifted = options.agent
|
|
448
|
+
? profiled.rest
|
|
449
|
+
: liftOptionsAfterDashes(profiled.rest, (words) => commandAt(program, commandPath(program, words)));
|
|
450
|
+
const liftedEnd = lifted.indexOf("--");
|
|
451
|
+
const liftedFlags = lifted.slice(0, liftedEnd < 0 ? undefined : liftedEnd);
|
|
452
|
+
machine ||= isMachine(liftedFlags);
|
|
453
|
+
debug ||= liftedFlags.includes("--debug");
|
|
454
|
+
reporting = machine ? { ...options, tty: false } : options;
|
|
443
455
|
// `wa 'chats list' --help` reads "chats list" as a profile and prints the root help with exit 0.
|
|
444
456
|
try {
|
|
445
457
|
// Only before `--`: what follows is a positional, an agent's text among them, never a command word.
|
|
446
|
-
const
|
|
447
|
-
for (const words of [flags, end < 0 ? lifted : lifted.slice(0, end)])
|
|
458
|
+
for (const words of [flags, liftedFlags])
|
|
448
459
|
refuseJoinedPath(program, words.filter((word) => !word.startsWith("-")), profile !== undefined && !/\s/.test(profile) ? [profile] : []);
|
|
449
460
|
}
|
|
450
461
|
catch (failure) {
|
|
@@ -110,6 +110,7 @@ export const scheduleCommandFor = ({ app, context: contextOf }) => {
|
|
|
110
110
|
return { context, system, state, say, timer, environment, save };
|
|
111
111
|
};
|
|
112
112
|
withWhen(grantsAuthority(annotate(command.command("command"), {
|
|
113
|
+
literal_after_dashes: true,
|
|
113
114
|
effects: LOCAL_WRITE,
|
|
114
115
|
examples: [
|
|
115
116
|
`${app.command} personal schedule command health --every 1h -- doctor`,
|
|
@@ -24,13 +24,16 @@ export type MessageLink = MessagePermalink & {
|
|
|
24
24
|
files?: FileLink[];
|
|
25
25
|
};
|
|
26
26
|
export declare const fileLinks: (attachments: readonly Attachment[]) => FileLink[];
|
|
27
|
+
/** A chat and a message id, or one reference naming both; a reference's account is for the caller to check. */
|
|
27
28
|
export declare const messageLinkTarget: (reference: string, message: string | undefined, provider: string) => {
|
|
28
29
|
chat: string;
|
|
29
30
|
message: string;
|
|
30
31
|
account: string;
|
|
32
|
+
text: string;
|
|
31
33
|
} | {
|
|
32
34
|
chat: string;
|
|
33
35
|
message: string;
|
|
34
36
|
account: undefined;
|
|
37
|
+
text: string;
|
|
35
38
|
};
|
|
36
39
|
export declare const validatePermalink: (link: MessagePermalink) => MessagePermalink;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { hitFiles } from "./attachments.js";
|
|
3
|
-
import {
|
|
3
|
+
import { chatReferenceOf } from "./ref-arguments.js";
|
|
4
4
|
export const fileLinks = (attachments) => hitFiles(attachments).map(({ position, kind, name }) => ({
|
|
5
5
|
position,
|
|
6
6
|
kind,
|
|
@@ -8,28 +8,23 @@ export const fileLinks = (attachments) => hitFiles(attachments).map(({ position,
|
|
|
8
8
|
url: null,
|
|
9
9
|
reason: "unsupported_provider",
|
|
10
10
|
}));
|
|
11
|
+
/** A chat and a message id, or one reference naming both; a reference's account is for the caller to check. */
|
|
11
12
|
export const messageLinkTarget = (reference, message, provider) => {
|
|
12
|
-
|
|
13
|
+
const named = chatReferenceOf(reference, provider, "message");
|
|
14
|
+
if (named?.message !== undefined) {
|
|
13
15
|
if (message !== undefined)
|
|
14
16
|
throw new CliError("validation_error", "a locator already names the message");
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
target = parseLocator(reference);
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
throw new CliError("validation_error", "expected msg:<provider>/<account>/<chat>/<message>");
|
|
21
|
-
}
|
|
22
|
-
if (target.provider !== provider)
|
|
23
|
-
throw new CliError("validation_error", "that locator belongs to another messenger");
|
|
24
|
-
validMessageId(target.message);
|
|
25
|
-
return { chat: target.chat, message: target.message, account: target.account };
|
|
17
|
+
validMessageId(named.message);
|
|
18
|
+
return { chat: named.chat, message: named.message, account: named.account, text: named.text };
|
|
26
19
|
}
|
|
27
20
|
if (message === undefined)
|
|
28
21
|
throw new CliError("validation_error", "which message? give its id after the chat");
|
|
29
22
|
validMessageId(message);
|
|
23
|
+
if (named)
|
|
24
|
+
return { chat: named.chat, message, account: named.account, text: named.text };
|
|
30
25
|
if (!reference.trim())
|
|
31
26
|
throw new CliError("validation_error", "which chat? give its id or title");
|
|
32
|
-
return { chat: reference, message, account: undefined };
|
|
27
|
+
return { chat: reference, message, account: undefined, text: reference };
|
|
33
28
|
};
|
|
34
29
|
const validMessageId = (id) => {
|
|
35
30
|
if (!id ||
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TaskCategory } from "./task-categories.js";
|
|
2
|
+
/**
|
|
3
|
+
* Verb classes for reading money in a chat, matched by stem as task categories are. A stem cannot tell the imperative
|
|
4
|
+
* from the past tense (`оплати`, `оплатил` → `оплат`) or from a promise (`pagué`, `pagaré` → `pag`), so a stem that
|
|
5
|
+
* several classes share is listed once, under the request, and a word only counts where its stem is its own. The
|
|
6
|
+
* signal that reads these keeps the sender's side (owner or other) to decide the direction.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MONEY_REQUEST = "money-request";
|
|
9
|
+
export declare const MONEY_DONE = "money-done";
|
|
10
|
+
export declare const PROMISE = "promise";
|
|
11
|
+
export declare const MONEY_WORDS: readonly TaskCategory[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verb classes for reading money in a chat, matched by stem as task categories are. A stem cannot tell the imperative
|
|
3
|
+
* from the past tense (`оплати`, `оплатил` → `оплат`) or from a promise (`pagué`, `pagaré` → `pag`), so a stem that
|
|
4
|
+
* several classes share is listed once, under the request, and a word only counts where its stem is its own. The
|
|
5
|
+
* signal that reads these keeps the sender's side (owner or other) to decide the direction.
|
|
6
|
+
*/
|
|
7
|
+
export const MONEY_REQUEST = "money-request";
|
|
8
|
+
export const MONEY_DONE = "money-done";
|
|
9
|
+
export const PROMISE = "promise";
|
|
10
|
+
export const MONEY_WORDS = [
|
|
11
|
+
{
|
|
12
|
+
name: MONEY_REQUEST,
|
|
13
|
+
meaning: "asks for a payment or sends an invoice",
|
|
14
|
+
triggers: {
|
|
15
|
+
en: ["pay", "send", "transfer", "invoice", "owe", "refund", "remit"],
|
|
16
|
+
ru: ["переведи", "оплати", "заплати", "вышли", "отправь", "скинь", "верни", "счёт", "выстави", "должен"],
|
|
17
|
+
es: ["envía", "transfiere", "factura", "debes", "devuelve", "abona"],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: MONEY_DONE,
|
|
22
|
+
meaning: "says a payment was made or received",
|
|
23
|
+
triggers: {
|
|
24
|
+
en: ["paid", "sent", "received", "settled"],
|
|
25
|
+
ru: ["перевёл", "перевела", "выслал", "вернул", "скинул", "получил", "получила"],
|
|
26
|
+
es: ["recibido"],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: PROMISE,
|
|
31
|
+
meaning: "promises to pay or send later",
|
|
32
|
+
triggers: {
|
|
33
|
+
en: ["promise", "promised", "guarantee"],
|
|
34
|
+
ru: ["оплачу", "заплачу", "отправлю", "обещаю", "гарантирую"],
|
|
35
|
+
es: ["prometo", "transferiré", "devolveré"],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
];
|
|
@@ -11,6 +11,8 @@ export interface ProviderEntry {
|
|
|
11
11
|
}
|
|
12
12
|
/** The seed: what cli-core knows before any CLI has written its entry into the store. */
|
|
13
13
|
export declare const PROVIDERS: readonly ProviderEntry[];
|
|
14
|
+
/** How a text names a provider to a person: `MAX`, not `max`. */
|
|
15
|
+
export declare const providerLabel: (provider: Provider) => string;
|
|
14
16
|
/**
|
|
15
17
|
* A notes folder is an `accounts` row of one of these providers; people call every one `notes`. `obsidian` is a vault,
|
|
16
18
|
* `local` a plain folder (store version 32). `folder` is what they were before, and what Drive's stand-in folders and
|
package/dist/domain/providers.js
CHANGED
|
@@ -3,11 +3,11 @@ import { CliError } from "@wirecat/cli-base";
|
|
|
3
3
|
export const KINDS = ["messages", "mail", "notes", "documents", "meetings", "events"];
|
|
4
4
|
/** The seed: what cli-core knows before any CLI has written its entry into the store. */
|
|
5
5
|
export const PROVIDERS = [
|
|
6
|
-
{ name: "telegram", kind: "messages", aliases: ["tg"] },
|
|
6
|
+
{ name: "telegram", kind: "messages", aliases: ["tg"], command: "tg" },
|
|
7
7
|
{ name: "telegram-bot", kind: "messages", aliases: ["tg-bot"] },
|
|
8
|
-
{ name: "max", kind: "messages", aliases: [] },
|
|
8
|
+
{ name: "max", kind: "messages", aliases: [], command: "max" },
|
|
9
9
|
{ name: "max-bot", kind: "messages", aliases: [] },
|
|
10
|
-
{ name: "whatsapp", kind: "messages", aliases: ["wa"] },
|
|
10
|
+
{ name: "whatsapp", kind: "messages", aliases: ["wa"], command: "wa" },
|
|
11
11
|
{ name: "whatsapp-bot", kind: "messages", aliases: ["wa-bot"] },
|
|
12
12
|
{ name: "email", kind: "mail", aliases: [] },
|
|
13
13
|
{ name: "notes", kind: "notes", aliases: [] },
|
|
@@ -15,6 +15,18 @@ export const PROVIDERS = [
|
|
|
15
15
|
{ name: "obsidian", kind: "notes", aliases: [] },
|
|
16
16
|
{ name: "zoom", kind: "meetings", aliases: [] },
|
|
17
17
|
];
|
|
18
|
+
const LABELS = {
|
|
19
|
+
telegram: "Telegram",
|
|
20
|
+
"telegram-bot": "Telegram bot",
|
|
21
|
+
max: "MAX",
|
|
22
|
+
"max-bot": "MAX bot",
|
|
23
|
+
whatsapp: "WhatsApp",
|
|
24
|
+
"whatsapp-bot": "WhatsApp bot",
|
|
25
|
+
"google-drive": "Google Drive",
|
|
26
|
+
email: "mail",
|
|
27
|
+
};
|
|
28
|
+
/** How a text names a provider to a person: `MAX`, not `max`. */
|
|
29
|
+
export const providerLabel = (provider) => LABELS[provider] ?? provider;
|
|
18
30
|
/**
|
|
19
31
|
* A notes folder is an `accounts` row of one of these providers; people call every one `notes`. `obsidian` is a vault,
|
|
20
32
|
* `local` a plain folder (store version 32). `folder` is what they were before, and what Drive's stand-in folders and
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import type { Id, Provider } from "./models.js";
|
|
3
|
+
/** A `chat:` or `msg:` reference given where a messenger command takes a chat or a message. */
|
|
4
|
+
export interface ChatReference {
|
|
5
|
+
text: string;
|
|
6
|
+
provider: Provider;
|
|
7
|
+
account: Id;
|
|
8
|
+
chat: Id;
|
|
9
|
+
message?: Id;
|
|
10
|
+
}
|
|
11
|
+
/** The account a reference must belong to; `account` is `undefined` while this profile's is not known yet. */
|
|
12
|
+
export interface RefScope {
|
|
13
|
+
account: Id | undefined;
|
|
14
|
+
profile?: string;
|
|
15
|
+
}
|
|
16
|
+
/** The profiles of this app the store saw on an account; `undefined` when that cannot be told. */
|
|
17
|
+
export type ProfilesOn = (account: Id) => Promise<readonly string[] | undefined>;
|
|
18
|
+
/**
|
|
19
|
+
* The chat or message a reference names, when `text` is one of this provider's; `undefined` for text that is no
|
|
20
|
+
* reference, which stays an id or a title. Another kind or another messenger is refused, never read as a title.
|
|
21
|
+
*/
|
|
22
|
+
export declare const chatReferenceOf: (text: string, provider: Provider, takes?: "chat" | "message") => ChatReference | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* The identity a `contact:` reference names, when it is one of this provider's; `undefined` for any other text. A
|
|
25
|
+
* `person:` reference is the store's person, which the callers that take one read before this.
|
|
26
|
+
*/
|
|
27
|
+
export declare const contactReferenceOf: (text: string, provider: Provider | undefined) => Id | undefined;
|
|
28
|
+
/** The refusal of another account's reference: switching to it silently would read or send as somebody else. */
|
|
29
|
+
export declare const wrongAccount: (reference: Pick<ChatReference, "text" | "account">, scope: RefScope, profiles?: readonly string[]) => CliError;
|
|
30
|
+
/** Refuses a reference of another account than the scope's, naming the profiles that have it. */
|
|
31
|
+
export declare const ensureAccount: (reference: Pick<ChatReference, "text" | "account">, scope: RefScope, profilesOn?: ProfilesOn) => Promise<void>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { providerLabel, providerNamed } from "./providers.js";
|
|
3
|
+
import { looksLikeReference, parseReference } from "./references.js";
|
|
4
|
+
const TAKES = {
|
|
5
|
+
chat: "a chat: chat:<provider>/<account>/<chat>, its id or its title",
|
|
6
|
+
message: "a message: msg:<provider>/<account>/<chat>/<message>, or a chat and the message's id",
|
|
7
|
+
};
|
|
8
|
+
const OPENS = {
|
|
9
|
+
chat: "messages list",
|
|
10
|
+
message: "messages show",
|
|
11
|
+
contact: "contacts show",
|
|
12
|
+
};
|
|
13
|
+
const article = (word) => (/^[aeiou]/.test(word) ? "an" : "a");
|
|
14
|
+
const wrongKind = (text, kind, takes) => new CliError("validation_error", `${text} names ${article(kind)} ${kind}; this takes ${TAKES[takes]}`, {
|
|
15
|
+
reason: "ref.wrong-kind",
|
|
16
|
+
});
|
|
17
|
+
const wrongProvider = (text, provider, kind) => {
|
|
18
|
+
const command = providerNamed(provider)?.command;
|
|
19
|
+
const opens = OPENS[kind];
|
|
20
|
+
const how = command && opens ? `open it with ${command} <profile> ${opens} ${text}` : "open it with its own CLI";
|
|
21
|
+
const label = providerLabel(provider);
|
|
22
|
+
return new CliError("validation_error", `${text} is ${article(label.toLowerCase())} ${label} ${kind}; ${how}`, {
|
|
23
|
+
reason: "ref.wrong-provider",
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The chat or message a reference names, when `text` is one of this provider's; `undefined` for text that is no
|
|
28
|
+
* reference, which stays an id or a title. Another kind or another messenger is refused, never read as a title.
|
|
29
|
+
*/
|
|
30
|
+
export const chatReferenceOf = (text, provider, takes = "chat") => {
|
|
31
|
+
const trimmed = text.trim();
|
|
32
|
+
if (!looksLikeReference(trimmed))
|
|
33
|
+
return undefined;
|
|
34
|
+
const reference = parseReference(trimmed);
|
|
35
|
+
if (reference.type !== "chat" && reference.type !== "message")
|
|
36
|
+
throw wrongKind(trimmed, reference.type, takes);
|
|
37
|
+
if (reference.provider !== provider)
|
|
38
|
+
throw wrongProvider(trimmed, reference.provider, reference.type);
|
|
39
|
+
const found = {
|
|
40
|
+
text: trimmed,
|
|
41
|
+
provider: reference.provider,
|
|
42
|
+
account: reference.account,
|
|
43
|
+
chat: reference.chat,
|
|
44
|
+
...(reference.type === "message" ? { message: reference.message } : {}),
|
|
45
|
+
};
|
|
46
|
+
return found;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The identity a `contact:` reference names, when it is one of this provider's; `undefined` for any other text. A
|
|
50
|
+
* `person:` reference is the store's person, which the callers that take one read before this.
|
|
51
|
+
*/
|
|
52
|
+
export const contactReferenceOf = (text, provider) => {
|
|
53
|
+
const trimmed = text.trim();
|
|
54
|
+
if (!trimmed.startsWith("contact:"))
|
|
55
|
+
return undefined;
|
|
56
|
+
const reference = parseReference(trimmed);
|
|
57
|
+
if (reference.type !== "contact")
|
|
58
|
+
return undefined;
|
|
59
|
+
if (provider !== undefined && reference.provider !== provider)
|
|
60
|
+
throw wrongProvider(trimmed, reference.provider, reference.type);
|
|
61
|
+
return reference.id;
|
|
62
|
+
};
|
|
63
|
+
/** The refusal of another account's reference: switching to it silently would read or send as somebody else. */
|
|
64
|
+
export const wrongAccount = (reference, scope, profiles) => {
|
|
65
|
+
const here = scope.account === undefined
|
|
66
|
+
? "the account of this profile is not known yet"
|
|
67
|
+
: `${scope.profile === undefined ? "this profile" : `profile ${scope.profile}`} is account ${scope.account}`;
|
|
68
|
+
const others = profiles === undefined
|
|
69
|
+
? ""
|
|
70
|
+
: `. Profiles signed in to ${reference.account}: ${profiles.length > 0 ? profiles.join(", ") : "none here"}`;
|
|
71
|
+
return new CliError("validation_error", `${reference.text} belongs to account ${reference.account}; ${here}${others}`, { reason: "ref.wrong-account" });
|
|
72
|
+
};
|
|
73
|
+
/** Refuses a reference of another account than the scope's, naming the profiles that have it. */
|
|
74
|
+
export const ensureAccount = async (reference, scope, profilesOn) => {
|
|
75
|
+
if (scope.account !== undefined && scope.account === reference.account)
|
|
76
|
+
return;
|
|
77
|
+
const profiles = await profilesOn?.(reference.account).catch(() => undefined);
|
|
78
|
+
throw wrongAccount(reference, scope, profiles);
|
|
79
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type MeetingReference } from "../meetings/index.js";
|
|
1
2
|
/**
|
|
2
3
|
* One typed form for every kind of thing a link, a tag or an argument can name. The prefix makes a
|
|
3
4
|
* wrong kind fail loudly instead of matching another row. Parts that are ids of someone else's system
|
|
@@ -27,14 +28,31 @@ export type Reference = {
|
|
|
27
28
|
type: "folder";
|
|
28
29
|
id: string;
|
|
29
30
|
path: string | null;
|
|
30
|
-
};
|
|
31
|
+
} | MeetingReference;
|
|
31
32
|
/**
|
|
32
33
|
* Things named by their store id (a task also by its key or the task package's id). `entity` is the name
|
|
33
34
|
* before organizations and projects: it still parses, so an old file's reference resolves as not found.
|
|
34
35
|
*/
|
|
35
|
-
export declare const SIMPLE_REFERENCES: readonly ["note", "document", "person", "organization", "project", "task", "memory", "fact", "bot", "entity"];
|
|
36
|
+
export declare const SIMPLE_REFERENCES: readonly ["note", "document", "person", "organization", "project", "task", "memory", "fact", "bot", "event", "entity"];
|
|
36
37
|
export type SimpleReference = (typeof SIMPLE_REFERENCES)[number];
|
|
38
|
+
export type RefKindName = Exclude<Reference["type"], "entity">;
|
|
39
|
+
export interface RefKindSpec<R extends Reference = Reference> {
|
|
40
|
+
/** The word before the first colon: `chat`, `msg`, `document`. */
|
|
41
|
+
prefix: string;
|
|
42
|
+
type: R["type"];
|
|
43
|
+
/** For error texts: `chat:<provider>/<account>/<chat>`. */
|
|
44
|
+
shape: string;
|
|
45
|
+
parse(rest: string, text: string): R;
|
|
46
|
+
format(reference: R): string;
|
|
47
|
+
}
|
|
48
|
+
/** Every kind of reference. A new kind is one row here; nothing else switches on the prefix. */
|
|
49
|
+
export declare const REF_KINDS: readonly RefKindSpec[];
|
|
37
50
|
export declare const formatReference: (reference: Reference) => string;
|
|
38
51
|
export declare const parseReference: (text: string) => Reference;
|
|
39
52
|
/** The stored spelling of a reference, so two spellings of one thing compare equal. */
|
|
40
53
|
export declare const canonicalReference: (text: string) => string;
|
|
54
|
+
/**
|
|
55
|
+
* Strict: the whole text is one reference of a known kind, with no space. A title such as "Note: plan" is not, so an
|
|
56
|
+
* argument that takes either a reference or a title can tell them apart. The shape is checked only by parsing it.
|
|
57
|
+
*/
|
|
58
|
+
export declare const looksLikeReference: (text: string) => boolean;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError, singleLine } from "@wirecat/cli-base";
|
|
2
|
+
import { formatMeetingReference, parseMeetingReference } from "../meetings/index.js";
|
|
2
3
|
import { formatLocator, parseLocator } from "./locator.js";
|
|
3
4
|
/**
|
|
4
5
|
* Things named by their store id (a task also by its key or the task package's id). `entity` is the name
|
|
@@ -14,70 +15,154 @@ export const SIMPLE_REFERENCES = [
|
|
|
14
15
|
"memory",
|
|
15
16
|
"fact",
|
|
16
17
|
"bot",
|
|
18
|
+
"event",
|
|
17
19
|
"entity",
|
|
18
20
|
];
|
|
19
|
-
const
|
|
21
|
+
const malformed = (text, shape) => new CliError("validation_error", `"${singleLine(text)}" is not a reference — expected ${shape}`, {
|
|
22
|
+
reason: "ref.malformed",
|
|
23
|
+
});
|
|
24
|
+
const parts = (rest, count, text, shape) => {
|
|
20
25
|
const split = rest.split("/");
|
|
21
26
|
if (split.length !== count || split.some((part) => part === ""))
|
|
22
|
-
throw
|
|
27
|
+
throw malformed(text, shape);
|
|
23
28
|
return split.map(decodeURIComponent);
|
|
24
29
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
case "folder":
|
|
34
|
-
return reference.path === null
|
|
35
|
-
? `folder:${encodeURIComponent(reference.id)}`
|
|
36
|
-
: `folder:${encodeURIComponent(reference.id)}/${encodeURIComponent(reference.path)}`;
|
|
37
|
-
default:
|
|
38
|
-
return `${reference.type}:${reference.id}`;
|
|
39
|
-
}
|
|
30
|
+
const encoded = (...values) => values.map(encodeURIComponent).join("/");
|
|
31
|
+
/** A subfolder spelled one way: no leading, trailing or doubled slash, and `.` for the folder itself. */
|
|
32
|
+
const folderPath = (path) => {
|
|
33
|
+
const clean = path
|
|
34
|
+
.split("/")
|
|
35
|
+
.filter((part) => part !== "" && part !== ".")
|
|
36
|
+
.join("/");
|
|
37
|
+
return clean === "" ? (path.trim() === "" ? "" : null) : clean;
|
|
40
38
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
const MEETING_SHAPE = "meeting:<account>/<meeting>[/<transcript>[/<cue>]] with safe integer ids";
|
|
40
|
+
const kind = (spec) => spec;
|
|
41
|
+
const simple = (prefix) => kind({
|
|
42
|
+
prefix,
|
|
43
|
+
type: prefix,
|
|
44
|
+
shape: `${prefix}:<id>`,
|
|
45
|
+
parse: (rest, text) => {
|
|
46
|
+
if (!rest.trim())
|
|
47
|
+
throw new CliError("validation_error", `"${singleLine(text)}" names no ${prefix}`, {
|
|
48
|
+
reason: "ref.malformed",
|
|
49
|
+
});
|
|
50
|
+
return { type: prefix, id: rest };
|
|
51
|
+
},
|
|
52
|
+
format: (reference) => `${reference.type}:${reference.id}`,
|
|
53
|
+
});
|
|
54
|
+
/** Every kind of reference. A new kind is one row here; nothing else switches on the prefix. */
|
|
55
|
+
export const REF_KINDS = [
|
|
56
|
+
kind({
|
|
57
|
+
prefix: "msg",
|
|
58
|
+
type: "message",
|
|
59
|
+
shape: "msg:<provider>/<account>/<chat>/<message>",
|
|
60
|
+
parse: (_, text) => {
|
|
61
|
+
try {
|
|
62
|
+
return { type: "message", ...parseLocator(text) };
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
throw malformed(text, "msg:<provider>/<account>/<chat>/<message>");
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
format: formatLocator,
|
|
69
|
+
}),
|
|
70
|
+
kind({
|
|
71
|
+
prefix: "chat",
|
|
72
|
+
type: "chat",
|
|
73
|
+
shape: "chat:<provider>/<account>/<chat>",
|
|
74
|
+
parse: (rest, text) => {
|
|
75
|
+
const [provider, account, chat] = parts(rest, 3, text, "chat:<provider>/<account>/<chat>");
|
|
51
76
|
return { type: "chat", provider, account, chat };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
77
|
+
},
|
|
78
|
+
format: ({ provider, account, chat }) => `chat:${encoded(provider, account, chat)}`,
|
|
79
|
+
}),
|
|
80
|
+
kind({
|
|
81
|
+
prefix: "contact",
|
|
82
|
+
type: "contact",
|
|
83
|
+
shape: "contact:<provider>/<id>",
|
|
84
|
+
parse: (rest, text) => {
|
|
85
|
+
const [provider, id] = parts(rest, 2, text, "contact:<provider>/<id>");
|
|
55
86
|
return { type: "contact", provider, id };
|
|
56
|
-
}
|
|
57
|
-
|
|
87
|
+
},
|
|
88
|
+
format: ({ provider, id }) => `contact:${encoded(provider, id)}`,
|
|
89
|
+
}),
|
|
90
|
+
kind({
|
|
91
|
+
prefix: "folder",
|
|
92
|
+
type: "folder",
|
|
93
|
+
shape: "folder:<id>[/<path>]",
|
|
94
|
+
parse: (rest, text) => {
|
|
58
95
|
const slash = rest.indexOf("/");
|
|
59
96
|
const id = decodeURIComponent(slash < 0 ? rest : rest.slice(0, slash));
|
|
60
97
|
const path = slash < 0 ? null : folderPath(decodeURIComponent(rest.slice(slash + 1)));
|
|
61
98
|
if (!id.trim() || path === "")
|
|
62
|
-
throw
|
|
99
|
+
throw malformed(text, "folder:<id>[/<path>]");
|
|
63
100
|
return { type: "folder", id, path };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
101
|
+
},
|
|
102
|
+
format: ({ id, path }) => (path === null ? `folder:${encodeURIComponent(id)}` : `folder:${encoded(id, path)}`),
|
|
103
|
+
}),
|
|
104
|
+
kind({
|
|
105
|
+
prefix: "meeting",
|
|
106
|
+
type: "meeting",
|
|
107
|
+
shape: MEETING_SHAPE,
|
|
108
|
+
parse: (_, text) => {
|
|
109
|
+
try {
|
|
110
|
+
return parseMeetingReference(text);
|
|
70
111
|
}
|
|
71
|
-
|
|
112
|
+
catch {
|
|
113
|
+
throw malformed(text, MEETING_SHAPE);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
format: formatMeetingReference,
|
|
117
|
+
}),
|
|
118
|
+
...SIMPLE_REFERENCES.map(simple),
|
|
119
|
+
];
|
|
120
|
+
const byPrefix = new Map(REF_KINDS.map((spec) => [spec.prefix, spec]));
|
|
121
|
+
const byType = new Map(REF_KINDS.map((spec) => [spec.type, spec]));
|
|
122
|
+
/** The prefixes people are told about: `entity` only still parses. */
|
|
123
|
+
const offered = REF_KINDS.map(({ prefix }) => prefix).filter((prefix) => prefix !== "entity");
|
|
124
|
+
const distance = (a, b) => {
|
|
125
|
+
let row = Array.from({ length: b.length + 1 }, (_, at) => at);
|
|
126
|
+
for (const [i, x] of [...a].entries()) {
|
|
127
|
+
const next = [i + 1];
|
|
128
|
+
for (const [j, y] of [...b].entries())
|
|
129
|
+
next.push(Math.min(row[j + 1] + 1, next[j] + 1, row[j] + (x === y ? 0 : 1)));
|
|
130
|
+
row = next;
|
|
72
131
|
}
|
|
132
|
+
return row[b.length];
|
|
73
133
|
};
|
|
74
|
-
/**
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
.
|
|
78
|
-
.filter((
|
|
79
|
-
.
|
|
80
|
-
|
|
134
|
+
/** `doc` reads as `document`, not as `bot` one letter away: a typed start of a prefix wins over spelling. */
|
|
135
|
+
const nearest = (typed) => offered.find((prefix) => typed.length >= 2 && prefix.startsWith(typed)) ??
|
|
136
|
+
offered
|
|
137
|
+
.map((prefix) => ({ prefix, far: distance(typed, prefix) }))
|
|
138
|
+
.filter(({ far }) => far <= 2)
|
|
139
|
+
.sort((a, b) => a.far - b.far)[0]?.prefix;
|
|
140
|
+
const unknownKind = (text, prefix, rest) => {
|
|
141
|
+
const near = prefix === "" ? undefined : nearest(prefix);
|
|
142
|
+
const kinds = offered.map((one) => `${one}:`).join(", ");
|
|
143
|
+
const hint = near === undefined ? `; kinds: ${kinds}` : `; did you mean ${near}:${singleLine(rest)}? Kinds: ${kinds}`;
|
|
144
|
+
return new CliError("validation_error", `"${singleLine(text)}" is no kind of reference${hint}`, {
|
|
145
|
+
reason: "ref.unknown-kind",
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
export const formatReference = (reference) => byType.get(reference.type).format(reference);
|
|
149
|
+
export const parseReference = (text) => {
|
|
150
|
+
const reference = text.trim();
|
|
151
|
+
const colon = reference.indexOf(":");
|
|
152
|
+
const prefix = colon < 0 ? "" : reference.slice(0, colon);
|
|
153
|
+
const rest = reference.slice(colon + 1);
|
|
154
|
+
const spec = byPrefix.get(prefix);
|
|
155
|
+
if (!spec)
|
|
156
|
+
throw unknownKind(reference, prefix, rest);
|
|
157
|
+
return spec.parse(rest, reference);
|
|
81
158
|
};
|
|
82
159
|
/** The stored spelling of a reference, so two spellings of one thing compare equal. */
|
|
83
160
|
export const canonicalReference = (text) => formatReference(parseReference(text));
|
|
161
|
+
/**
|
|
162
|
+
* Strict: the whole text is one reference of a known kind, with no space. A title such as "Note: plan" is not, so an
|
|
163
|
+
* argument that takes either a reference or a title can tell them apart. The shape is checked only by parsing it.
|
|
164
|
+
*/
|
|
165
|
+
export const looksLikeReference = (text) => {
|
|
166
|
+
const match = /^([a-z]+):\S+$/.exec(text.trim());
|
|
167
|
+
return match !== null && byPrefix.has(match[1]);
|
|
168
|
+
};
|