@wirecat/cli-core 0.411.0 → 0.413.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 +32 -0
- package/dist/cli/bot/command.js +3 -3
- package/dist/cli/bot/messages-sync.d.ts +7 -0
- package/dist/cli/bot/{store.js → messages-sync.js} +4 -5
- package/dist/cli/bot/port.d.ts +3 -3
- package/dist/cli/config-command.js +1 -0
- package/dist/cli/entry.d.ts +59 -0
- package/dist/cli/entry.js +136 -0
- package/dist/cli/first-run.d.ts +1 -1
- package/dist/cli/first-run.js +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/messenger/tasks-command.js +2 -2
- package/dist/cli/moved.js +1 -0
- package/dist/cli/settings.d.ts +2 -0
- package/dist/cli/settings.js +5 -1
- package/dist/domain/providers.d.ts +25 -3
- package/dist/domain/providers.js +89 -5
- package/dist/entities/index.d.ts +29 -1
- package/dist/entities/index.js +5 -1
- package/dist/entities/knowledge-lists.d.ts +28 -0
- package/dist/entities/knowledge-lists.js +29 -0
- package/dist/errors/reasons.js +16 -1
- package/dist/files/native-writer-lease.js +17 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/oauth/authorize.d.ts +21 -0
- package/dist/oauth/authorize.js +130 -0
- package/dist/oauth/index.d.ts +5 -0
- package/dist/oauth/index.js +4 -0
- package/dist/oauth/keeper.d.ts +41 -0
- package/dist/oauth/keeper.js +136 -0
- package/dist/oauth/provider.d.ts +54 -0
- package/dist/oauth/provider.js +1 -0
- package/dist/oauth/requests.d.ts +11 -0
- package/dist/oauth/requests.js +129 -0
- package/dist/oauth/storage.d.ts +48 -0
- package/dist/oauth/storage.js +197 -0
- package/dist/search/account-list.d.ts +5 -2
- package/dist/search/account-list.js +27 -11
- package/dist/search/query.js +3 -1
- package/dist/sends/permission-aliases.js +1 -0
- package/dist/services/documents/knowledge.d.ts +5 -3
- package/dist/services/documents/knowledge.js +4 -4
- package/dist/services/meeting-links.d.ts +3 -1
- package/dist/services/meeting-links.js +2 -2
- package/dist/services/messages-search.js +15 -3
- package/dist/services/project-gathering.d.ts +2 -0
- package/dist/services/project-gathering.js +18 -2
- package/dist/services/search-all.js +5 -2
- package/dist/store/accounts.d.ts +3 -0
- package/dist/store/accounts.js +2 -0
- package/dist/store/exports.d.ts +1 -0
- package/dist/store/exports.js +1 -0
- package/dist/store/index.d.ts +3 -1
- package/dist/store/index.js +2 -0
- package/dist/store/knowledge-query.d.ts +6 -0
- package/dist/store/knowledge-query.js +34 -0
- package/dist/store/providers.d.ts +7 -0
- package/dist/store/providers.js +5 -0
- package/dist/store/sqlite/knowledge.d.ts +5 -4
- package/dist/store/sqlite/knowledge.js +50 -17
- package/dist/store/sqlite/mail-rules.d.ts +2 -1
- package/dist/store/sqlite/mail-rules.js +12 -2
- package/dist/store/sqlite/notes.d.ts +11 -5
- package/dist/store/sqlite/notes.js +9 -2
- package/dist/store/sqlite/open-store.js +2 -0
- package/dist/store/sqlite/providers.d.ts +12 -0
- package/dist/store/sqlite/providers.js +45 -0
- package/dist/store/sqlite/tags.d.ts +1 -1
- package/dist/store/sqlite/tags.js +3 -3
- package/package.json +5 -1
- package/parity.json +31 -35
- package/dist/cli/bot/store.d.ts +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
|
|
|
5
5
|
minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
|
|
6
6
|
shape of this file.
|
|
7
7
|
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
## 0.413.0 — 25.10.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`startCli`, one entry-point call for a CLI that builds its own program** (from `@wirecat/cli-core/cli`). It does what drive, zm and memo wire by hand in `src/bin/`: the crash catch, cancellation on SIGINT and SIGTERM, a quiet exit 0 on a closed pipe, the SQLite swap, the first-run questions (`entry.atStart`), the consent prompt (`entry.ask`) and the browser (`entry.openBrowser`). The program and its failure report stay the CLI's own. Nothing changes for a CLI that keeps its own wiring.
|
|
15
|
+
- **Sign-in module `@wirecat/cli-core/oauth`** (available from cli-core 0.413.0). A CLI declares its provider (URLs, client, loopback or relay redirect) and gets authorization code with PKCE S256 (state check, timeout, cancel, paste fallback), refresh and revoke over plain `fetch`, and a `tokenKeeper` whose refresh takes a lock per keyring entry shared by every process, so two processes refreshing at once send one request. `credentialStorage` sits on cli-base's `Credentials`; `standardCodec` writes new entries as `oauth:<provider>:<profile>`, and `driveSessionCodec` and `zoomTokenCodec` read and write drive's and zm's existing entries unchanged, so nobody signs in again.
|
|
16
|
+
- **`gatherProjectEvidence` caps each linked chat per invocation.** A linked chat returns at most `max(20, floor(maxItems / (linked chats read + 1)))` new items per run (50 for one chat at the default 100 items, 20 from four chats up); the rest stays in the checkpoint for the next `--cursor` run, which gives it a fresh share. Every linked chat still gets its first page before any chat gets a second. Each `linkedChats` entry now has `items` (new items it returned in this run) and `capped` (it hit the limit and has more to read). Before, one busy chat could fill the whole run. Checkpoints stay version 1, and old ones still resume.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **A file lock no longer fails with "no such file" when another writer releases at the same moment** (available from cli-core 0.413.0). The releaser removes the empty marker folder; an acquirer that had just created it now creates it again instead of throwing `ENOENT`. `acquireLock`, the archive writer lease and the sign-in module's refresh lock all use it.
|
|
21
|
+
- **The 0.412.0 notes for the provider registry, unknown providers and the `events` kind say "available from cli-core 0.413.0"; they shipped in 0.412.0.** The release folded them in from `main`'s head. Nothing else in those entries changes.
|
|
22
|
+
|
|
23
|
+
## 0.412.0 — 24.10.2026
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **A stored provider registry** (available from cli-core 0.413.0). `registerProvider(store, { name, kind, aliases, command })` (from `@wirecat/cli-core/store`) writes a CLI's entry into `store_settings` under the key `providers`; every store handle loads the stored entries when it opens, so `kindOf`, `providerNamed` and `--source` in every CLI know a provider cli-core does not list. The built-ins stay the seed. `useProviders(entries)`, `kindIn(provider)`, `providerNamed`, `kindOf`, `providers()`, `KINDS` and `PROVIDERS` are exported from `@wirecat/cli-core`. Two entries that disagree on a kind, or an alias another provider has, are a `validation_error` naming both; a stored entry that clashes with a built-in is left out when the store opens, so it never stops the store opening.
|
|
28
|
+
- **Knowledge lists filter at their native readers.** Organizations, projects, reminders and mail rules accept optional queries before SQL list bounds. Document tags and links accept native selectors, and meeting-link reads can select one URL before aggregation. Existing unfiltered calls keep their behavior.
|
|
29
|
+
|
|
30
|
+
### Changed — may break callers
|
|
31
|
+
|
|
32
|
+
- **A provider no registry entry names is skipped, never searched as messages** (available from cli-core 0.413.0). `search messages`, `search all` and `--source` lists report each of its accounts under `skipped` with the reason "unknown provider — run its CLI once" (code `not_found`). The account the search runs as is still read, whatever its provider. `kindOf` keeps its signature and its `messages` default; `kindIn` answers `undefined` for such a provider. `ListedAccount.kind` is now `Kind | undefined`.
|
|
33
|
+
- **`KINDS` and `Kind` gain `events`** (available from cli-core 0.413.0). No search reads events yet; `--source events` says so.
|
|
34
|
+
- **`bot store fetch` is now `bot messages sync`, and the `bot store` group is gone** (tg and max). It does what the personal `messages sync` does, so it carries the same name; `store` is for the local database. The old path answers with a pointer to the new one, and a saved `permissions.bot.store.fetch` is refused until it is renamed to `permissions.bot.messages.sync`. A CLI that customizes the command must move its override to the new path.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **`config set` and `config unset` work while a moved permission key is saved.** Every other command still refuses the old key and names its replacement; `config unset permissions.<old>` and `config set permissions.<new>` now fix it instead of failing with the same error.
|
|
39
|
+
- **`tasks update <task> --parent none` clears the parent.** The CLI keeps the sentinel through option parsing and converts it to null when building the plan, so Commander cannot turn it into an empty task id.
|
|
8
40
|
|
|
9
41
|
## 0.411.0 — 24.10.2026
|
|
10
42
|
|
package/dist/cli/bot/command.js
CHANGED
|
@@ -16,12 +16,12 @@ import { botExamples } from "./examples.js";
|
|
|
16
16
|
import { BOT_CHATS_QUERY, BOT_PROFILES_QUERY, botChatFilter, botProfileName } from "./list-queries.js";
|
|
17
17
|
import { botMcpCommand } from "./mcp.js";
|
|
18
18
|
import { botCan, botIdOf, botMessagesCommand } from "./messages.js";
|
|
19
|
+
import { botMessagesSyncCommand } from "./messages-sync.js";
|
|
19
20
|
import { botModerateCommand, botRulesCommand } from "./moderation.js";
|
|
20
21
|
import { addBotCopyReads, botContactsCommand, botSearchCommand } from "./people.js";
|
|
21
22
|
import { BOT_ACTIONS } from "./port.js";
|
|
22
23
|
import { registryProfiles } from "./registry.js";
|
|
23
24
|
import { botCallbacksCommand, botMenuCommand, botWebhooksCommand } from "./setup.js";
|
|
24
|
-
import { botStoreCommand } from "./store.js";
|
|
25
25
|
import { BotTokenStore } from "./token.js";
|
|
26
26
|
import { botWatchCommand } from "./watch.js";
|
|
27
27
|
/** Where each of the messenger's other secrets comes from, `null` when it is not set; never the value. */
|
|
@@ -289,6 +289,8 @@ const chatsCommand = (bot) => {
|
|
|
289
289
|
*/
|
|
290
290
|
const withCopyReads = (messages, bot) => {
|
|
291
291
|
addBotCopyReads(messages, bot);
|
|
292
|
+
if (bot.fetching)
|
|
293
|
+
messages.addCommand(botMessagesSyncCommand(bot, bot.fetching));
|
|
292
294
|
return messages;
|
|
293
295
|
};
|
|
294
296
|
export const botCommand = (bot) => {
|
|
@@ -310,7 +312,5 @@ export const botCommand = (bot) => {
|
|
|
310
312
|
.addCommand(botContactsCommand(bot));
|
|
311
313
|
if (bot.identity)
|
|
312
314
|
command.addCommand(meCommand(bot));
|
|
313
|
-
if (bot.fetching)
|
|
314
|
-
command.addCommand(botStoreCommand(bot, bot.fetching));
|
|
315
315
|
return bot.mcp ? command.addCommand(botMcpCommand(bot)) : command;
|
|
316
316
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import type { BotMessenger } from "./port.js";
|
|
3
|
+
/**
|
|
4
|
+
* `bot messages sync`: a chat's history into the bot's local copy, newest to oldest and resumable, as the
|
|
5
|
+
* personal `messages sync` does. Mounted for a messenger whose Bot API pages back through a chat.
|
|
6
|
+
*/
|
|
7
|
+
export declare const botMessagesSyncCommand: (bot: BotMessenger, fetching: NonNullable<BotMessenger["fetching"]>) => Command;
|
|
@@ -16,12 +16,11 @@ const wholeNumber = (value) => {
|
|
|
16
16
|
return parsed;
|
|
17
17
|
};
|
|
18
18
|
/**
|
|
19
|
-
* `bot
|
|
19
|
+
* `bot messages sync`: a chat's history into the bot's local copy, newest to oldest and resumable, as the
|
|
20
20
|
* personal `messages sync` does. Mounted for a messenger whose Bot API pages back through a chat.
|
|
21
21
|
*/
|
|
22
|
-
export const
|
|
23
|
-
const
|
|
24
|
-
const fetch = annotate(command.command("fetch"), { effects: FETCH })
|
|
22
|
+
export const botMessagesSyncCommand = (bot, fetching) => {
|
|
23
|
+
const fetch = annotate(new Command("sync"), { effects: FETCH })
|
|
25
24
|
.description("fetch a chat's history into the bot's local copy, newest first; run it again to continue")
|
|
26
25
|
.argument("<chat>", "a chat id, or the title of a chat this bot has seen")
|
|
27
26
|
.option("--limit <n>", `at most this many messages in this run; ${fetching.maxPages * fetching.page} if not given`, wholeNumber)
|
|
@@ -85,5 +84,5 @@ export const botStoreCommand = (bot, fetching) => {
|
|
|
85
84
|
stop.release();
|
|
86
85
|
}
|
|
87
86
|
});
|
|
88
|
-
return
|
|
87
|
+
return fetch;
|
|
89
88
|
};
|
package/dist/cli/bot/port.d.ts
CHANGED
|
@@ -53,7 +53,7 @@ export interface BotHistory {
|
|
|
53
53
|
messages: Message[];
|
|
54
54
|
more: boolean;
|
|
55
55
|
}>;
|
|
56
|
-
/** One page back from `before` (or from now), newest first: what `bot
|
|
56
|
+
/** One page back from `before` (or from now), newest first: what `bot messages sync` walks. */
|
|
57
57
|
historyBefore(chat: BotChatRef, window: {
|
|
58
58
|
limit: number;
|
|
59
59
|
before?: string;
|
|
@@ -250,7 +250,7 @@ export interface BotNativeApi {
|
|
|
250
250
|
export interface BotConnectOptions {
|
|
251
251
|
stop?: AbortSignal;
|
|
252
252
|
events?: EventSink;
|
|
253
|
-
/** Only `bot
|
|
253
|
+
/** Only `bot messages sync` asks for a history reader; other commands stay on the Bot API. */
|
|
254
254
|
history?: {
|
|
255
255
|
from?: string;
|
|
256
256
|
pauseMs: number;
|
|
@@ -295,7 +295,7 @@ export interface BotMessenger {
|
|
|
295
295
|
readSecret?: (command: Command, prompt: string) => Promise<string>;
|
|
296
296
|
/** What kind of chat a stored message came from; without it a positive id is a dialog, a negative one a group. */
|
|
297
297
|
chatKindOf?: (hit: StoredHit) => ChatKind;
|
|
298
|
-
/** How its history pages, for `bot
|
|
298
|
+
/** How its history pages, for `bot messages sync`, which is mounted only when this is set. */
|
|
299
299
|
/** `from`: the help of `--until-id`, the message the fetch starts at; the option is mounted only when set. */
|
|
300
300
|
fetching?: Fetching & {
|
|
301
301
|
from?: string;
|
|
@@ -64,6 +64,7 @@ export const configCommand = (app, config, { permissionKey } = {}) => {
|
|
|
64
64
|
const context = baseContext(this, (flags, options) => config.resolveSettings(flags, {
|
|
65
65
|
...options,
|
|
66
66
|
needsProfile: !everyone && setting !== "default_profile" && !isStoreSetting(setting),
|
|
67
|
+
skipMovedPermissions: true,
|
|
67
68
|
}));
|
|
68
69
|
const { settings, renderer, env } = context;
|
|
69
70
|
if (isStoreSetting(setting)) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type ChildProcess, type SpawnOptions } from "node:child_process";
|
|
2
|
+
import type { ErrorReason } from "@wirecat/cli-base/standards";
|
|
3
|
+
import type { Command } from "commander";
|
|
4
|
+
import { type AppIdentity } from "../ports/app.js";
|
|
5
|
+
/** What `startCli` hands the CLI's own program: the process, wired once. */
|
|
6
|
+
export interface EntryContext {
|
|
7
|
+
/** `process.argv` without the runtime and the script. */
|
|
8
|
+
args: string[];
|
|
9
|
+
env: NodeJS.ProcessEnv;
|
|
10
|
+
/** Whether stdout is a terminal. */
|
|
11
|
+
tty: boolean;
|
|
12
|
+
/** Aborted on SIGINT, SIGTERM and a closed or failing stdout. */
|
|
13
|
+
signal: AbortSignal;
|
|
14
|
+
/** The first-run questions, before the command acts: call it from a `preAction` hook with the action command. */
|
|
15
|
+
atStart: (command: Command) => Promise<void>;
|
|
16
|
+
/** A yes/no question on stderr; false without asking when there is no terminal, under CI, `--json`, `--jsonl` or `--no-input`. */
|
|
17
|
+
ask: (question: string, command?: Command) => Promise<boolean>;
|
|
18
|
+
/** Opens the URL in the system browser, or in `browser` when given; never waits for the browser to close. */
|
|
19
|
+
openBrowser: (url: string, browser?: string) => Promise<void>;
|
|
20
|
+
/** True once a reader closed the output (`| head`): nothing more reaches the terminal. */
|
|
21
|
+
outputClosed: () => boolean;
|
|
22
|
+
}
|
|
23
|
+
/** The parts of the process `startCli` touches, replaced in tests. */
|
|
24
|
+
export interface EntryIo {
|
|
25
|
+
argv: readonly string[];
|
|
26
|
+
env: NodeJS.ProcessEnv;
|
|
27
|
+
platform: NodeJS.Platform;
|
|
28
|
+
stdin: NodeJS.ReadableStream & {
|
|
29
|
+
isTTY?: boolean;
|
|
30
|
+
};
|
|
31
|
+
stdout: NodeJS.WritableStream & {
|
|
32
|
+
isTTY?: boolean;
|
|
33
|
+
};
|
|
34
|
+
stderr: NodeJS.WritableStream & {
|
|
35
|
+
isTTY?: boolean;
|
|
36
|
+
};
|
|
37
|
+
signals: Pick<NodeJS.Process, "on" | "off">;
|
|
38
|
+
setExitCode: (code: number) => void;
|
|
39
|
+
spawn: (command: string, args: readonly string[], options: SpawnOptions) => ChildProcess;
|
|
40
|
+
}
|
|
41
|
+
export interface CliEntry {
|
|
42
|
+
app: AppIdentity;
|
|
43
|
+
/** Builds and runs the CLI's program; a returned number is the exit code, nothing keeps the one already set. */
|
|
44
|
+
run: (entry: EntryContext) => Promise<number | undefined>;
|
|
45
|
+
/** Says a failure the program threw, once, and gives its exit code. */
|
|
46
|
+
fail: (error: unknown, entry: EntryContext) => number;
|
|
47
|
+
/** Whether a crash may print the human form; stdout being a terminal when left out. */
|
|
48
|
+
interactive?: (args: readonly string[], tty: boolean) => boolean;
|
|
49
|
+
reasons?: readonly ErrorReason[];
|
|
50
|
+
/** Swap a system SQLite that cannot hold the store before anything runs (`ensureSqlite`). */
|
|
51
|
+
sqlite?: boolean;
|
|
52
|
+
io?: Partial<EntryIo>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The entry point of a CLI that builds its own commander program: crash catch, cancellation, a quiet end on a closed
|
|
56
|
+
* pipe, the first-run questions, the consent prompt and the browser. It sets `process.exitCode` and never exits, so
|
|
57
|
+
* output still flushes.
|
|
58
|
+
*/
|
|
59
|
+
export declare const startCli: (entry: CliEntry) => Promise<void>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { CliError, catchCrashes } from "@wirecat/cli-base";
|
|
3
|
+
import { readOnlyLockOf } from "../ports/app.js";
|
|
4
|
+
import { ensureSqlite } from "../sqlite-runtime.js";
|
|
5
|
+
import { readSecret } from "../terminal/prompt.js";
|
|
6
|
+
import { commandPathOf } from "./command-contract.js";
|
|
7
|
+
import { setupAtStart } from "./first-run.js";
|
|
8
|
+
const processIo = () => ({
|
|
9
|
+
argv: process.argv,
|
|
10
|
+
env: process.env,
|
|
11
|
+
platform: process.platform,
|
|
12
|
+
stdin: process.stdin,
|
|
13
|
+
stdout: process.stdout,
|
|
14
|
+
stderr: process.stderr,
|
|
15
|
+
signals: process,
|
|
16
|
+
setExitCode: (code) => {
|
|
17
|
+
process.exitCode = code;
|
|
18
|
+
},
|
|
19
|
+
spawn,
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* The entry point of a CLI that builds its own commander program: crash catch, cancellation, a quiet end on a closed
|
|
23
|
+
* pipe, the first-run questions, the consent prompt and the browser. It sets `process.exitCode` and never exits, so
|
|
24
|
+
* output still flushes.
|
|
25
|
+
*/
|
|
26
|
+
export const startCli = async (entry) => {
|
|
27
|
+
const io = { ...processIo(), ...entry.io };
|
|
28
|
+
const args = io.argv.slice(2);
|
|
29
|
+
const tty = io.stdout.isTTY === true;
|
|
30
|
+
const release = catchCrashes({
|
|
31
|
+
cli: entry.app.command,
|
|
32
|
+
interactive: entry.interactive?.(args, tty) ?? tty,
|
|
33
|
+
...(entry.reasons ? { reasons: entry.reasons } : {}),
|
|
34
|
+
target: io.signals,
|
|
35
|
+
});
|
|
36
|
+
const controller = new AbortController();
|
|
37
|
+
let closed = false;
|
|
38
|
+
let unwritable = false;
|
|
39
|
+
const interrupted = (signal) => () => controller.abort(new CliError("cancelled", `command interrupted by ${signal}`, { reason: signal, retryable: false }));
|
|
40
|
+
const sigint = interrupted("SIGINT");
|
|
41
|
+
const sigterm = interrupted("SIGTERM");
|
|
42
|
+
const outputFailed = (error) => {
|
|
43
|
+
if (error.code === "EPIPE") {
|
|
44
|
+
closed = true;
|
|
45
|
+
controller.abort(new CliError("cancelled", "output pipe closed", { retryable: false }));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
unwritable = true;
|
|
49
|
+
controller.abort(new CliError("configuration_error", "could not write command output"));
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const headless = (command) => {
|
|
53
|
+
const flags = command?.optsWithGlobals() ?? {};
|
|
54
|
+
return Boolean(flags.json || flags.jsonl || flags.input === false || io.env.CI) || !tty;
|
|
55
|
+
};
|
|
56
|
+
const context = {
|
|
57
|
+
args,
|
|
58
|
+
env: io.env,
|
|
59
|
+
tty,
|
|
60
|
+
signal: controller.signal,
|
|
61
|
+
atStart: (command) => setupAtStart({
|
|
62
|
+
command: entry.app.command,
|
|
63
|
+
env: io.env,
|
|
64
|
+
path: commandPathOf(command),
|
|
65
|
+
headless: headless(command),
|
|
66
|
+
readOnlyLock: readOnlyLockOf(entry.app, io.env),
|
|
67
|
+
input: io.stdin,
|
|
68
|
+
}),
|
|
69
|
+
ask: async (question, command) => {
|
|
70
|
+
if (headless(command) || io.stdin.isTTY !== true)
|
|
71
|
+
return false;
|
|
72
|
+
const answer = await readSecret(`${question} [y/N] `, {
|
|
73
|
+
input: io.stdin,
|
|
74
|
+
output: io.stderr,
|
|
75
|
+
echo: true,
|
|
76
|
+
signal: controller.signal,
|
|
77
|
+
});
|
|
78
|
+
return /^(y|yes)$/i.test(answer.trim());
|
|
79
|
+
},
|
|
80
|
+
openBrowser: (url, browser) => openBrowser(io, url, browser),
|
|
81
|
+
outputClosed: () => closed,
|
|
82
|
+
};
|
|
83
|
+
let code;
|
|
84
|
+
try {
|
|
85
|
+
if (entry.sqlite)
|
|
86
|
+
await ensureSqlite();
|
|
87
|
+
io.signals.on("SIGINT", sigint).on("SIGTERM", sigterm);
|
|
88
|
+
io.stdout.on("error", outputFailed);
|
|
89
|
+
io.stderr.on("error", outputFailed);
|
|
90
|
+
const returned = await entry.run(context);
|
|
91
|
+
if (typeof returned === "number")
|
|
92
|
+
code = returned;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
code = commanderExit(error) ?? entry.fail(error, context);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
io.signals.off("SIGINT", sigint).off("SIGTERM", sigterm);
|
|
99
|
+
release();
|
|
100
|
+
}
|
|
101
|
+
// The listeners stay: output still flushing after the command can meet the closed pipe too.
|
|
102
|
+
if (closed)
|
|
103
|
+
io.setExitCode(0);
|
|
104
|
+
else if (unwritable)
|
|
105
|
+
io.setExitCode(3);
|
|
106
|
+
else if (code !== undefined)
|
|
107
|
+
io.setExitCode(code);
|
|
108
|
+
};
|
|
109
|
+
/** Help and the version were printed already; help for a missing command is a usage mistake, as in `run()`. */
|
|
110
|
+
const commanderExit = (error) => {
|
|
111
|
+
if (!(error instanceof Error))
|
|
112
|
+
return undefined;
|
|
113
|
+
const { code, exitCode } = error;
|
|
114
|
+
if (typeof code !== "string" || !code.startsWith("commander.") || typeof exitCode !== "number")
|
|
115
|
+
return undefined;
|
|
116
|
+
if (exitCode === 0)
|
|
117
|
+
return 0;
|
|
118
|
+
return code === "commander.help" ? 2 : undefined;
|
|
119
|
+
};
|
|
120
|
+
const openBrowser = (io, url, browser) => {
|
|
121
|
+
const [command, prefix] = browser !== undefined
|
|
122
|
+
? [browser, []]
|
|
123
|
+
: io.platform === "darwin"
|
|
124
|
+
? ["open", []]
|
|
125
|
+
: io.platform === "win32"
|
|
126
|
+
? ["rundll32.exe", ["url.dll,FileProtocolHandler"]]
|
|
127
|
+
: ["xdg-open", []];
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const child = io.spawn(command, [...prefix, url], { detached: true, stdio: "ignore", windowsHide: true });
|
|
130
|
+
child.once("error", () => reject(new CliError("configuration_error", `could not start ${command} — open the printed URL in a browser`)));
|
|
131
|
+
child.once("spawn", () => {
|
|
132
|
+
child.unref();
|
|
133
|
+
resolve();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
};
|
package/dist/cli/first-run.d.ts
CHANGED
|
@@ -53,7 +53,7 @@ export declare const askSetupQuestions: (run: FirstRun) => Promise<Question[]>;
|
|
|
53
53
|
/**
|
|
54
54
|
* The first-run step a tool runs before each command: asks when `asksAtStart` says so, and lets the command run
|
|
55
55
|
* whatever happens to the questions. `setup` asks all three again, since a refusal sends the owner there.
|
|
56
|
-
* A tool that builds its own program
|
|
56
|
+
* A tool that builds its own program gets it from `startCli` as `entry.atStart`.
|
|
57
57
|
*/
|
|
58
58
|
export declare const setupAtStart: (start: {
|
|
59
59
|
command: string;
|
package/dist/cli/first-run.js
CHANGED
|
@@ -203,7 +203,7 @@ export const askSetupQuestions = async (run) => {
|
|
|
203
203
|
/**
|
|
204
204
|
* The first-run step a tool runs before each command: asks when `asksAtStart` says so, and lets the command run
|
|
205
205
|
* whatever happens to the questions. `setup` asks all three again, since a refusal sends the owner there.
|
|
206
|
-
* A tool that builds its own program
|
|
206
|
+
* A tool that builds its own program gets it from `startCli` as `entry.atStart`.
|
|
207
207
|
*/
|
|
208
208
|
export const setupAtStart = async (start) => {
|
|
209
209
|
const input = start.input ?? process.stdin;
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -59,6 +59,7 @@ export { type Closeable, withDeadline } from "./deadline.js";
|
|
|
59
59
|
export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
|
|
60
60
|
export { DISCOVERY_DESCRIPTIONS } from "./discovery-descriptions.js";
|
|
61
61
|
export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, } from "./effects.js";
|
|
62
|
+
export { type CliEntry, type EntryContext, type EntryIo, startCli } from "./entry.js";
|
|
62
63
|
export { type AnswerOutput, answerSchema, type Body, type ItemBody, isBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, type ListBody, type ListInput, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
|
|
63
64
|
export { withAbort } from "./execution.js";
|
|
64
65
|
export { askSetupQuestions, asksAtStart, type FirstRun, type FirstRunIo, LANGUAGES, setupAtStart, systemLanguage, } from "./first-run.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -52,6 +52,7 @@ export { withDeadline } from "./deadline.js";
|
|
|
52
52
|
export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
|
|
53
53
|
export { DISCOVERY_DESCRIPTIONS } from "./discovery-descriptions.js";
|
|
54
54
|
export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, } from "./effects.js";
|
|
55
|
+
export { startCli } from "./entry.js";
|
|
55
56
|
export { answerSchema, isBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
|
|
56
57
|
export { withAbort } from "./execution.js";
|
|
57
58
|
export { askSetupQuestions, asksAtStart, LANGUAGES, setupAtStart, systemLanguage, } from "./first-run.js";
|
|
@@ -3,9 +3,9 @@ import type { CacheDatabase } from "../../store/driver.js";
|
|
|
3
3
|
import type { Messenger } from "./context.js";
|
|
4
4
|
/** `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. */
|
|
5
5
|
export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
|
|
6
|
+
error?: undefined;
|
|
6
7
|
path: string;
|
|
7
8
|
exists: boolean;
|
|
8
|
-
error?: undefined;
|
|
9
9
|
} | {
|
|
10
10
|
path: string;
|
|
11
11
|
exists: boolean;
|
|
@@ -111,7 +111,7 @@ const planFrom = (options) => {
|
|
|
111
111
|
if (options.priority !== undefined)
|
|
112
112
|
plan.priority = options.priority;
|
|
113
113
|
if (options.parent !== undefined)
|
|
114
|
-
plan.parent = options.parent;
|
|
114
|
+
plan.parent = options.parent === "none" ? null : options.parent;
|
|
115
115
|
if (options.category !== undefined)
|
|
116
116
|
plan.category = options.category;
|
|
117
117
|
if (options.title !== undefined)
|
|
@@ -203,7 +203,7 @@ export const tasksCommand = (messenger) => {
|
|
|
203
203
|
throw new CliError("validation_error", "--priority takes 0 through 4");
|
|
204
204
|
return Number(value);
|
|
205
205
|
})
|
|
206
|
-
.option("--parent <id>", "parent task in the same project; none clears it"
|
|
206
|
+
.option("--parent <id>", "parent task in the same project; none clears it"), "send Alex the API token").action(async function (id) {
|
|
207
207
|
const context = messengerContext(this, messenger);
|
|
208
208
|
refuseLocalWrite(context, messenger.app.command, "tasks.update");
|
|
209
209
|
const plan = planFrom(this.opts());
|
package/dist/cli/moved.js
CHANGED
|
@@ -32,6 +32,7 @@ export const RENAMED_COMMANDS = {
|
|
|
32
32
|
"mcp setup": "mcp add",
|
|
33
33
|
"mcp doctor": "mcp check",
|
|
34
34
|
"commands schema": "commands info",
|
|
35
|
+
"bot store fetch": "bot messages sync",
|
|
35
36
|
"store gaps plan": "store gaps show",
|
|
36
37
|
"store reindex": "store rebuild",
|
|
37
38
|
"stats messages counters refresh": "stats messages counters sync",
|
package/dist/cli/settings.d.ts
CHANGED
|
@@ -140,6 +140,8 @@ export interface ResolveOptions {
|
|
|
140
140
|
needsProfile?: boolean;
|
|
141
141
|
/** Read the config file without writing it: no default file. For a dry run. */
|
|
142
142
|
readOnly?: boolean;
|
|
143
|
+
/** Skip moved permission keys instead of refusing them, so `config set` and `unset` can fix one. */
|
|
144
|
+
skipMovedPermissions?: boolean;
|
|
143
145
|
}
|
|
144
146
|
/** Where `profile` came from when none was named and the command needs none. */
|
|
145
147
|
export declare const NONE_NAMED = "none named";
|
package/dist/cli/settings.js
CHANGED
|
@@ -4,6 +4,7 @@ import * as v from "valibot";
|
|
|
4
4
|
import { AI_ENTRIES, resolveAISettings } from "../analysis/settings.js";
|
|
5
5
|
import { parseDuration } from "../domain/duration.js";
|
|
6
6
|
import { envName, readOnlyLockOf } from "../ports/app.js";
|
|
7
|
+
import { canonicalPermissionKey } from "../sends/permission-aliases.js";
|
|
7
8
|
import { assertOverridesTighten, CEILING_KEY, LEVELS, layerPermissions, permissionOverrides, RESOURCES, } from "../sends/permissions.js";
|
|
8
9
|
import { storePath } from "../store/path.js";
|
|
9
10
|
import { sharedConfigPath } from "../store/shared-config.js";
|
|
@@ -77,6 +78,7 @@ const SHARED_DEFAULTS_ONLY = {
|
|
|
77
78
|
update_check: v.optional(flag),
|
|
78
79
|
skill_hint: v.optional(flag),
|
|
79
80
|
};
|
|
81
|
+
const withoutMoved = (levels) => levels && Object.fromEntries(Object.entries(levels).filter(([key]) => canonicalPermissionKey(key) === key));
|
|
80
82
|
/** Where `profile` came from when none was named and the command needs none. */
|
|
81
83
|
export const NONE_NAMED = "none named";
|
|
82
84
|
/** A name for a command that speaks for no account; never chosen for one that does. */
|
|
@@ -230,7 +232,9 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
230
232
|
: { value: false, from: "default" };
|
|
231
233
|
const fileLayers = layers.map(([from, scope]) => [
|
|
232
234
|
from,
|
|
233
|
-
|
|
235
|
+
options.skipMovedPermissions
|
|
236
|
+
? withoutMoved(scope?.permissions)
|
|
237
|
+
: scope?.permissions,
|
|
234
238
|
]);
|
|
235
239
|
const overrides = permissionOverrides(flags.permission);
|
|
236
240
|
assertOverridesTighten(overrides, layerPermissions(fileLayers).levels);
|
|
@@ -1,20 +1,42 @@
|
|
|
1
1
|
import type { Provider } from "./models.js";
|
|
2
2
|
/** What a search reads; the same names `--only`, `searched` and `skipped` use. */
|
|
3
|
-
export declare const KINDS: readonly ["messages", "mail", "notes", "documents", "meetings"];
|
|
3
|
+
export declare const KINDS: readonly ["messages", "mail", "notes", "documents", "meetings", "events"];
|
|
4
4
|
export type Kind = (typeof KINDS)[number];
|
|
5
5
|
export interface ProviderEntry {
|
|
6
6
|
name: Provider;
|
|
7
7
|
kind: Kind;
|
|
8
8
|
aliases: readonly string[];
|
|
9
|
+
/** The CLI that wrote the entry (`zm`, `drive`). One CLI may write several providers, so it is not an alias. */
|
|
10
|
+
command?: string;
|
|
9
11
|
}
|
|
10
|
-
/**
|
|
12
|
+
/** The seed: what cli-core knows before any CLI has written its entry into the store. */
|
|
11
13
|
export declare const PROVIDERS: readonly ProviderEntry[];
|
|
12
14
|
/** The store keeps a notes folder as an `accounts` row of this provider; people call it `notes`. */
|
|
13
15
|
export declare const FOLDER_PROVIDER = "folder";
|
|
14
16
|
/** Notes written in memo have no account row; this is their account. */
|
|
15
17
|
export declare const INTERNAL_NOTES = "internal";
|
|
18
|
+
/** The reason a search gives for an account whose provider no entry names. */
|
|
19
|
+
export declare const UNKNOWN_PROVIDER = "unknown provider \u2014 run its CLI once";
|
|
20
|
+
/** An entry as a CLI or the store gives it, checked: anything else is a `validation_error`. */
|
|
21
|
+
export declare const providerEntryOf: (value: unknown) => ProviderEntry;
|
|
22
|
+
/** The built-ins and the entries the store or the CLI running gave; what `kindOf` and `providerNamed` read. */
|
|
23
|
+
export declare const providers: () => readonly ProviderEntry[];
|
|
24
|
+
/**
|
|
25
|
+
* Replaces the entries beyond the built-ins. Opening the store calls it with the stored entries; a test or a CLI
|
|
26
|
+
* with no store may call it too. Two entries that disagree on a kind, or one word naming two providers, throw.
|
|
27
|
+
*/
|
|
28
|
+
export declare const useProviders: (entries: readonly ProviderEntry[]) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Like `useProviders`, but an entry that clashes is left out instead of failing: what the store holds was written
|
|
31
|
+
* by CLIs of other versions, and one stale entry must not stop the store opening. Answers the entries left out.
|
|
32
|
+
*/
|
|
33
|
+
export declare const useStoredProviders: (entries: readonly unknown[]) => unknown[];
|
|
34
|
+
/** Adds one entry to those in use, strictly: a clash throws and leaves the registry as it was. */
|
|
35
|
+
export declare const addProvider: (entry: ProviderEntry) => void;
|
|
16
36
|
export declare const isKind: (word: string) => word is Kind;
|
|
17
37
|
/** The registry's provider a name or alias stands for. */
|
|
18
38
|
export declare const providerNamed: (word: string) => ProviderEntry | undefined;
|
|
19
|
-
/**
|
|
39
|
+
/** The kind of a provider the registry knows; `undefined` for one no entry names, which a search skips. */
|
|
40
|
+
export declare const kindIn: (provider: Provider) => Kind | undefined;
|
|
41
|
+
/** A provider the registry does not list counts as a messenger here; a search asks `kindIn` and skips it. */
|
|
20
42
|
export declare const kindOf: (provider: Provider) => Kind;
|
package/dist/domain/providers.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
1
2
|
/** What a search reads; the same names `--only`, `searched` and `skipped` use. */
|
|
2
|
-
export const KINDS = ["messages", "mail", "notes", "documents", "meetings"];
|
|
3
|
-
/**
|
|
3
|
+
export const KINDS = ["messages", "mail", "notes", "documents", "meetings", "events"];
|
|
4
|
+
/** The seed: what cli-core knows before any CLI has written its entry into the store. */
|
|
4
5
|
export const PROVIDERS = [
|
|
5
6
|
{ name: "telegram", kind: "messages", aliases: ["tg"] },
|
|
6
7
|
{ name: "telegram-bot", kind: "messages", aliases: ["tg-bot"] },
|
|
@@ -16,11 +17,94 @@ export const PROVIDERS = [
|
|
|
16
17
|
export const FOLDER_PROVIDER = "folder";
|
|
17
18
|
/** Notes written in memo have no account row; this is their account. */
|
|
18
19
|
export const INTERNAL_NOTES = "internal";
|
|
20
|
+
/** The reason a search gives for an account whose provider no entry names. */
|
|
21
|
+
export const UNKNOWN_PROVIDER = "unknown provider — run its CLI once";
|
|
22
|
+
const WORD = /^[a-z0-9][a-z0-9_-]*$/;
|
|
23
|
+
const invalid = (message) => {
|
|
24
|
+
throw new CliError("validation_error", message);
|
|
25
|
+
};
|
|
26
|
+
const wordOf = (value, what) => typeof value === "string" && WORD.test(value)
|
|
27
|
+
? value
|
|
28
|
+
: invalid(`${what} is lowercase letters, digits, - and _ — not ${JSON.stringify(value)}`);
|
|
29
|
+
/** An entry as a CLI or the store gives it, checked: anything else is a `validation_error`. */
|
|
30
|
+
export const providerEntryOf = (value) => {
|
|
31
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
32
|
+
return invalid("a provider entry is an object");
|
|
33
|
+
const { name, kind, aliases = [], command } = value;
|
|
34
|
+
const named = wordOf(name, "a provider's name");
|
|
35
|
+
if (typeof kind !== "string" || !KINDS.includes(kind))
|
|
36
|
+
invalid(`provider ${named}'s kind is one of ${KINDS.join(", ")} — not ${JSON.stringify(kind)}`);
|
|
37
|
+
if (!Array.isArray(aliases))
|
|
38
|
+
invalid(`provider ${named}'s aliases are a list`);
|
|
39
|
+
return {
|
|
40
|
+
name: named,
|
|
41
|
+
kind: kind,
|
|
42
|
+
aliases: [...new Set(aliases.map((alias) => wordOf(alias, `an alias of ${named}`)))],
|
|
43
|
+
...(command === undefined ? {} : { command: wordOf(command, `the command of ${named}`) }),
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
const describe = ({ name, kind, command }) => `${name} (${kind}${command ? `, from ${command}` : ""})`;
|
|
47
|
+
const wordsOf = ({ name, aliases }) => [name, ...aliases];
|
|
48
|
+
/** `entry` joined to `known`: the same name keeps one entry with both entries' aliases; a clash is an error. */
|
|
49
|
+
const joined = (known, entry) => {
|
|
50
|
+
const same = known.find(({ name }) => name === entry.name);
|
|
51
|
+
if (same && same.kind !== entry.kind)
|
|
52
|
+
invalid(`two provider entries disagree on the kind: ${describe(same)} and ${describe(entry)}`);
|
|
53
|
+
const merged = same
|
|
54
|
+
? {
|
|
55
|
+
...same,
|
|
56
|
+
aliases: [...new Set([...same.aliases, ...entry.aliases])],
|
|
57
|
+
...(entry.command ? { command: entry.command } : {}),
|
|
58
|
+
}
|
|
59
|
+
: entry;
|
|
60
|
+
const others = known.filter((one) => one !== same);
|
|
61
|
+
for (const word of wordsOf(merged)) {
|
|
62
|
+
const owner = others.find((one) => wordsOf(one).includes(word));
|
|
63
|
+
if (owner)
|
|
64
|
+
invalid(`"${word}" names two providers: ${describe(owner)} and ${describe(merged)}`);
|
|
65
|
+
}
|
|
66
|
+
return same ? known.map((one) => (one === same ? merged : one)) : [...known, merged];
|
|
67
|
+
};
|
|
68
|
+
let registry = PROVIDERS;
|
|
69
|
+
/** The built-ins and the entries the store or the CLI running gave; what `kindOf` and `providerNamed` read. */
|
|
70
|
+
export const providers = () => registry;
|
|
71
|
+
/**
|
|
72
|
+
* Replaces the entries beyond the built-ins. Opening the store calls it with the stored entries; a test or a CLI
|
|
73
|
+
* with no store may call it too. Two entries that disagree on a kind, or one word naming two providers, throw.
|
|
74
|
+
*/
|
|
75
|
+
export const useProviders = (entries) => {
|
|
76
|
+
registry = entries.map(providerEntryOf).reduce(joined, PROVIDERS);
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Like `useProviders`, but an entry that clashes is left out instead of failing: what the store holds was written
|
|
80
|
+
* by CLIs of other versions, and one stale entry must not stop the store opening. Answers the entries left out.
|
|
81
|
+
*/
|
|
82
|
+
export const useStoredProviders = (entries) => {
|
|
83
|
+
const left = [];
|
|
84
|
+
registry = entries.reduce((known, entry) => {
|
|
85
|
+
try {
|
|
86
|
+
return joined(known, providerEntryOf(entry));
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (!(error instanceof CliError))
|
|
90
|
+
throw error;
|
|
91
|
+
left.push(entry);
|
|
92
|
+
return known;
|
|
93
|
+
}
|
|
94
|
+
}, PROVIDERS);
|
|
95
|
+
return left;
|
|
96
|
+
};
|
|
97
|
+
/** Adds one entry to those in use, strictly: a clash throws and leaves the registry as it was. */
|
|
98
|
+
export const addProvider = (entry) => {
|
|
99
|
+
registry = joined(registry, providerEntryOf(entry));
|
|
100
|
+
};
|
|
19
101
|
export const isKind = (word) => KINDS.includes(word.toLowerCase());
|
|
20
102
|
/** The registry's provider a name or alias stands for. */
|
|
21
103
|
export const providerNamed = (word) => {
|
|
22
104
|
const wanted = word.toLowerCase();
|
|
23
|
-
return
|
|
105
|
+
return registry.find((entry) => wordsOf(entry).includes(wanted));
|
|
24
106
|
};
|
|
25
|
-
/**
|
|
26
|
-
export const
|
|
107
|
+
/** The kind of a provider the registry knows; `undefined` for one no entry names, which a search skips. */
|
|
108
|
+
export const kindIn = (provider) => provider === FOLDER_PROVIDER ? "notes" : registry.find(({ name }) => name === provider)?.kind;
|
|
109
|
+
/** A provider the registry does not list counts as a messenger here; a search asks `kindIn` and skips it. */
|
|
110
|
+
export const kindOf = (provider) => kindIn(provider) ?? "messages";
|