@wirecat/cli-core 0.529.0 → 0.530.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 +12 -0
- package/dist/cli/config-command.d.ts +11 -1
- package/dist/cli/config-command.js +12 -10
- package/dist/cli/messenger/download-command.d.ts +2 -1
- package/dist/cli/messenger/download-command.js +2 -1
- package/dist/cli/shared-command-exports.d.ts +1 -0
- package/dist/files/index.d.ts +1 -0
- package/dist/files/index.js +1 -0
- package/dist/files/safe-name.d.ts +5 -0
- package/dist/files/safe-name.js +18 -0
- package/dist/services/file-download.d.ts +2 -5
- package/dist/services/file-download.js +3 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ 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.530.0 — 01.11.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`configCommand(app, config, { ownerOnly })`**: settings and permission keys an agent with `mcp.authority` still
|
|
15
|
+
cannot change over MCP, beside `mcp.*` and `bot.api` — for a mail CLI, the server hosts its password goes to, and
|
|
16
|
+
the key that lets an agent mail other people. A permission key there covers the keys under it.
|
|
17
|
+
- **`safeName` from `./files`**: the file-name cleaner downloads use (no path, no leading dot, no control or direction
|
|
18
|
+
character; Windows' reserved names refused), for a CLI that saves a file someone else named.
|
|
19
|
+
|
|
8
20
|
## 0.529.0 — 01.11.2026
|
|
9
21
|
|
|
10
22
|
### Added
|
|
@@ -6,8 +6,18 @@ import { type Configuration } from "./settings.js";
|
|
|
6
6
|
* The settings in force and where each came from, and changing them. Printed whole: no field the
|
|
7
7
|
* configuration accepts can hold a secret — the schema has nowhere to put one.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Settings and permission keys an agent with `mcp.authority` still may not change, beyond the ones that let it in:
|
|
11
|
+
* where the password goes (a server host), or a permission the app reads only from the owner's file.
|
|
12
|
+
*/
|
|
13
|
+
export interface OwnerOnlySettings {
|
|
14
|
+
settings?: readonly string[];
|
|
15
|
+
/** A key here covers the keys under it too: `messages.send.others` covers `messages.send.others.x`. */
|
|
16
|
+
permissions?: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
export declare const configCommand: (app: AppIdentity, config: Configuration, { permissionKey, ownerOnly, }?: {
|
|
10
19
|
permissionKey?: PermissionKeySource | undefined;
|
|
20
|
+
ownerOnly?: OwnerOnlySettings;
|
|
11
21
|
}) => Command;
|
|
12
22
|
/**
|
|
13
23
|
* A key no command or write is checked against would be saved and do nothing (BUG-137) — whether it
|
|
@@ -10,11 +10,7 @@ import { knownBeside, knownPermissionKeys, unknownPermissionIssues, } from "./pe
|
|
|
10
10
|
import { fromFile, NONE_NAMED } from "./settings.js";
|
|
11
11
|
import { nextStep } from "./steps.js";
|
|
12
12
|
import { changeStoreSetting, isStoreSetting, offersStoreSetting, storeSettings, storeSettingsOf, } from "./store-settings.js";
|
|
13
|
-
|
|
14
|
-
* The settings in force and where each came from, and changing them. Printed whole: no field the
|
|
15
|
-
* configuration accepts can hold a secret — the schema has nowhere to put one.
|
|
16
|
-
*/
|
|
17
|
-
export const configCommand = (app, config, { permissionKey = config.permissions } = {}) => {
|
|
13
|
+
export const configCommand = (app, config, { permissionKey = config.permissions, ownerOnly = {}, } = {}) => {
|
|
18
14
|
const command = new Command("config").description("the settings in force, and where each one came from");
|
|
19
15
|
// Only a messenger has bots, and with them the personal and bot sections of the file. Elsewhere the options stay
|
|
20
16
|
// accepted, so a script that passes one still runs, but help, `commands --json` and MCP leave them out.
|
|
@@ -120,7 +116,7 @@ export const configCommand = (app, config, { permissionKey = config.permissions
|
|
|
120
116
|
throw new CliError("permission_error", `this process is locked to profile ${settings.profile} (${lock}) — ${root ? setting : "--defaults"} changes other profiles`);
|
|
121
117
|
}
|
|
122
118
|
if (environmentOf(this).agent)
|
|
123
|
-
refuseGuardedKey(setting, action === "set" ? String(given) : undefined);
|
|
119
|
+
refuseGuardedKey(setting, action === "set" ? String(given) : undefined, ownerOnly);
|
|
124
120
|
if (action === "set")
|
|
125
121
|
refuseUnknownKey(this, setting, String(given), permissionKey);
|
|
126
122
|
const saved = config.changeSetting(settings.configPath, {
|
|
@@ -166,7 +162,11 @@ export const refuseUnknownKey = (command, setting, value, permissionKey) => {
|
|
|
166
162
|
* profile, in the defaults, or by replacing `permissions` whole. Only the owner, in a terminal, turns it on or off
|
|
167
163
|
* (NEED-1662, NEED-1679).
|
|
168
164
|
*/
|
|
169
|
-
const refuseGuardedKey = (setting, value) => {
|
|
165
|
+
const refuseGuardedKey = (setting, value, ownerOnly) => {
|
|
166
|
+
if (ownerOnly.settings?.includes(setting))
|
|
167
|
+
throw new CliError("permission_error", `an agent cannot change ${setting} — the owner sets it in a terminal`, {
|
|
168
|
+
setting,
|
|
169
|
+
});
|
|
170
170
|
const keys = setting.startsWith("permissions.")
|
|
171
171
|
? [setting.slice("permissions.".length)]
|
|
172
172
|
: setting === "permissions"
|
|
@@ -174,11 +174,13 @@ const refuseGuardedKey = (setting, value) => {
|
|
|
174
174
|
? ["mcp"]
|
|
175
175
|
: objectKeys(value)
|
|
176
176
|
: [];
|
|
177
|
-
const
|
|
177
|
+
const under = (key, guarded) => key === guarded || key.startsWith(`${guarded}.`);
|
|
178
|
+
const listed = keys.flatMap((key) => (ownerOnly.permissions ?? []).filter((guarded) => under(key, guarded)));
|
|
179
|
+
const guarded = keys.some((key) => under(key, BOT_API_KEY))
|
|
178
180
|
? BOT_API_KEY
|
|
179
|
-
: keys.some((key) => key
|
|
181
|
+
: keys.some((key) => under(key, "mcp"))
|
|
180
182
|
? AUTHORITY_KEY
|
|
181
|
-
:
|
|
183
|
+
: listed[0];
|
|
182
184
|
if (!guarded)
|
|
183
185
|
return;
|
|
184
186
|
throw new CliError("permission_error", `an agent cannot change permissions.${guarded} — the owner sets it in a terminal`, { permission: guarded });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import type { Id } from "../../domain/models.js";
|
|
3
|
-
export {
|
|
3
|
+
export { safeName } from "../../files/safe-name.js";
|
|
4
|
+
export { downloadMessage, type Saved, save } from "../../services/file-download.js";
|
|
4
5
|
import { type Messenger } from "./context.js";
|
|
5
6
|
/** `messages download`: every file of one message, or with `--all` of a whole chat, never over a file already there. */
|
|
6
7
|
export declare const downloadSubcommand: (messages: Command, messenger: Messenger) => Command;
|
|
@@ -7,7 +7,8 @@ import { NOT_FILES } from "../../domain/attachments.js";
|
|
|
7
7
|
import { FETCHING, keyOf } from "../../services/archive.js";
|
|
8
8
|
import { batchProgress, mergeBatches } from "../../services/batch.js";
|
|
9
9
|
import { saveFiles } from "../../services/file-download.js";
|
|
10
|
-
export {
|
|
10
|
+
export { safeName } from "../../files/safe-name.js";
|
|
11
|
+
export { downloadMessage, save } from "../../services/file-download.js";
|
|
11
12
|
import { parseDuration } from "../../domain/duration.js";
|
|
12
13
|
import { OFFLINE } from "../../services/deps.js";
|
|
13
14
|
import { patiently } from "../../services/patience.js";
|
|
@@ -528,6 +528,7 @@ export declare const configCommand: (app: import("./entry.js").AppIdentity, conf
|
|
|
528
528
|
permissions?: import("./permission-keys.js").CommandPermissions | undefined;
|
|
529
529
|
}, args_2?: {
|
|
530
530
|
permissionKey?: import("./permission-keys.js").PermissionKeySource | undefined;
|
|
531
|
+
ownerOnly?: import("./config-command.js").OwnerOnlySettings;
|
|
531
532
|
} | undefined) => import("commander").Command;
|
|
532
533
|
export declare const guideCommand: (app: Pick<import("./entry.js").AppIdentity, "command">, options: import("./guide.js").GuideOptions) => import("commander").Command;
|
|
533
534
|
export declare const addMeetingBatchCommand: (program: import("commander").Command, deps: {
|
package/dist/files/index.d.ts
CHANGED
|
@@ -4,4 +4,5 @@ export { acquireLock, type FileLock } from "./lock.js";
|
|
|
4
4
|
export type { NativeWriterLeaseLayout } from "./native-writer-lease.js";
|
|
5
5
|
export { nativeWriterLeaseStore } from "./native-writer-lease.js";
|
|
6
6
|
export { openOrdinaryFile, sameOrdinaryFile, syncDirectory } from "./ordinary-file.js";
|
|
7
|
+
export { safeName } from "./safe-name.js";
|
|
7
8
|
export type { WriterLease, WriterLeaseAcquisition, WriterLeaseInfo, WriterLeaseOptions, WriterLeaseStore, } from "./writer-lease.js";
|
package/dist/files/index.js
CHANGED
|
@@ -3,3 +3,4 @@ export { readBoundedUtf8File } from "./bounded-utf8.js";
|
|
|
3
3
|
export { acquireLock } from "./lock.js";
|
|
4
4
|
export { nativeWriterLeaseStore } from "./native-writer-lease.js";
|
|
5
5
|
export { openOrdinaryFile, sameOrdinaryFile, syncDirectory } from "./ordinary-file.js";
|
|
6
|
+
export { safeName } from "./safe-name.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A name another person chose cannot climb out of the folder, cannot be a dot file we would then
|
|
3
|
+
* hide, and carries no control or direction character to rewrite the terminal or disguise its extension.
|
|
4
|
+
*/
|
|
5
|
+
export declare const safeName: (name: string | undefined, platform?: NodeJS.Platform) => string | undefined;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* A name another person chose cannot climb out of the folder, cannot be a dot file we would then
|
|
4
|
+
* hide, and carries no control or direction character to rewrite the terminal or disguise its extension.
|
|
5
|
+
*/
|
|
6
|
+
export const safeName = (name, platform = process.platform) => {
|
|
7
|
+
let plain = name === undefined
|
|
8
|
+
? ""
|
|
9
|
+
: basename(name.replaceAll("\\", "/"))
|
|
10
|
+
.replace(/[\p{Cc}---]/gu, "")
|
|
11
|
+
.replace(/^\.+/, "");
|
|
12
|
+
if (platform === "win32") {
|
|
13
|
+
plain = plain.replace(/[<>:"|?*]/g, "_").replace(/[. ]+$/, "");
|
|
14
|
+
if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(plain))
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
return plain === "" ? undefined : plain;
|
|
18
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { RefreshedFile } from "../domain/attachments.js";
|
|
2
2
|
import type { Id } from "../domain/models.js";
|
|
3
|
+
import { safeName } from "../files/safe-name.js";
|
|
3
4
|
import type { RemoteFile } from "../ports/messenger.js";
|
|
4
5
|
import { batchProgress } from "./batch.js";
|
|
5
6
|
import type { MessagesService } from "./messages.js";
|
|
@@ -39,11 +40,7 @@ export declare const saveFiles: (messages: Pick<MessagesService, "keepDownloaded
|
|
|
39
40
|
maxErrorPercent: number;
|
|
40
41
|
};
|
|
41
42
|
}>;
|
|
42
|
-
|
|
43
|
-
* A name another person chose cannot climb out of the folder, cannot be a dot file we would then
|
|
44
|
-
* hide, and carries no control or direction character to rewrite the terminal or disguise its extension.
|
|
45
|
-
*/
|
|
46
|
-
export declare const safeName: (name: string | undefined, platform?: NodeJS.Platform) => string | undefined;
|
|
43
|
+
export { safeName };
|
|
47
44
|
/**
|
|
48
45
|
* The bytes go to a temporary name first and are then hard-linked to the real one, which fails if the
|
|
49
46
|
* name is taken — so an existing file survives, and an interrupted download leaves no half file under
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createWriteStream, mkdirSync } from "node:fs";
|
|
2
2
|
import { link, rm } from "node:fs/promises";
|
|
3
|
-
import {
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
4
|
import { Readable, Transform } from "node:stream";
|
|
5
5
|
import { pipeline } from "node:stream/promises";
|
|
6
6
|
import { CliError } from "@wirecat/cli-base";
|
|
7
|
+
import { safeName } from "../files/safe-name.js";
|
|
7
8
|
import { batchProgress } from "./batch.js";
|
|
8
9
|
/** The files the messenger gave a new reference for while their bytes were read, to save without a path. */
|
|
9
10
|
export const refreshedFiles = (files) => files.flatMap(({ kind, name, position, providerRef }) => providerRef === undefined || Object.keys(providerRef).length === 0
|
|
@@ -90,23 +91,7 @@ const EXTENSIONS = {
|
|
|
90
91
|
};
|
|
91
92
|
/** Telegram sends a photo with no name and no type; it is always a JPEG. */
|
|
92
93
|
const BY_KIND = { photo: "jpg" };
|
|
93
|
-
|
|
94
|
-
* A name another person chose cannot climb out of the folder, cannot be a dot file we would then
|
|
95
|
-
* hide, and carries no control or direction character to rewrite the terminal or disguise its extension.
|
|
96
|
-
*/
|
|
97
|
-
export const safeName = (name, platform = process.platform) => {
|
|
98
|
-
let plain = name === undefined
|
|
99
|
-
? ""
|
|
100
|
-
: basename(name.replaceAll("\\", "/"))
|
|
101
|
-
.replace(/[\p{Cc}---]/gu, "")
|
|
102
|
-
.replace(/^\.+/, "");
|
|
103
|
-
if (platform === "win32") {
|
|
104
|
-
plain = plain.replace(/[<>:"|?*]/g, "_").replace(/[. ]+$/, "");
|
|
105
|
-
if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(plain))
|
|
106
|
-
return undefined;
|
|
107
|
-
}
|
|
108
|
-
return plain === "" ? undefined : plain;
|
|
109
|
-
};
|
|
94
|
+
export { safeName };
|
|
110
95
|
/**
|
|
111
96
|
* The bytes go to a temporary name first and are then hard-linked to the real one, which fails if the
|
|
112
97
|
* name is taken — so an existing file survives, and an interrupted download leaves no half file under
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.530.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|