@wirecat/cli-core 0.565.0 → 0.567.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 +59 -1
- package/constants.json +13 -0
- package/dist/background/schedules.d.ts +5 -1
- package/dist/background/schedules.js +10 -4
- package/dist/cli/app-doctor-command.d.ts +7 -13
- package/dist/cli/app-doctor-command.js +1 -1
- package/dist/cli/bot/command.js +5 -2
- package/dist/cli/bot/context.d.ts +2 -2
- package/dist/cli/bot/context.js +2 -2
- package/dist/cli/effects.d.ts +10 -0
- package/dist/cli/effects.js +19 -0
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/messenger/account-command.js +8 -2
- package/dist/cli/messenger/mcp-command.js +4 -1
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/program.js +2 -2
- package/dist/cli/schedule/schedule-command.d.ts +7 -1
- package/dist/cli/schedule/schedule-command.js +12 -9
- package/dist/cli/settings.d.ts +9 -1
- package/dist/cli/settings.js +30 -4
- package/dist/constants/index.d.ts +83 -0
- package/dist/constants/index.js +77 -0
- package/dist/constants/obscure.d.ts +1 -0
- package/dist/constants/obscure.js +9 -0
- package/dist/entities/account-profile.d.ts +2 -0
- package/dist/entities/account-profile.js +2 -0
- package/dist/entities/index.d.ts +1 -0
- package/dist/errors/reasons.js +2 -1
- package/dist/oauth/index.d.ts +1 -1
- package/dist/oauth/provider.d.ts +5 -0
- package/dist/oauth/storage.js +12 -2
- package/dist/store/sqlite/work-tasks.js +1 -1
- package/package.json +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,65 @@ 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
|
-
##
|
|
8
|
+
## 0.567.0 — 05.11.2026
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`<tool> bot list` runs without a profile word**, as `account list` does; `<tool> <profile> bot list` still
|
|
13
|
+
works.
|
|
14
|
+
- **A scheduled agent may run other CLIs the host declares** (`ScheduleHost.agentRuns`, type `AgentCli`): memo's
|
|
15
|
+
mail triage runs `mail`. Claude gets `Bash(<cli>:*)` for each; codex may write each one's state, config and cache
|
|
16
|
+
folders; every run sets `<PREFIX>_READ_ONLY=1` and `<PREFIX>_SCHEDULED` for each, whatever the schedule file says.
|
|
17
|
+
`runSchedule` takes the same list as `agentRuns`.
|
|
18
|
+
- **`<PREFIX>_READ_ONLY` lets through one authority change, `rules add`, when the command is marked
|
|
19
|
+
`narrowsUnderLock`** (`LOCKED_AUTHORITY_COMMANDS`, `admittedUnderLock`). The command narrows itself under the lock;
|
|
20
|
+
every other command marked `grantsAuthority` is still refused, and MCP still needs `mcp.authority`.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **`account list` no longer lists a bot as an account that is not signed in.** A name that only the bot side
|
|
25
|
+
uses (the file's `bot` section, a bot's own files) and that has no personal section, recorded login or session is
|
|
26
|
+
left out; `bot list` has it. A profile that is an account and a bot too says `bot: true`, a new optional field of
|
|
27
|
+
`account_profile`. `mcp serve --profiles all` serves the same account profiles. `Settings` gains the optional
|
|
28
|
+
`accountProfiles` and `botProfiles`.
|
|
29
|
+
- **`config set` keeps a long id as the digits typed.** `config set --bot phone_number_id 100000000000001` was
|
|
30
|
+
parsed as a number and refused by a setting that takes text; when the parsed value is refused and the text as
|
|
31
|
+
typed passes, the text is kept, never through a JS number.
|
|
32
|
+
|
|
33
|
+
### Changed — may break callers
|
|
34
|
+
|
|
35
|
+
- **A write on a stale task revision fails with `conflict`, exit 15, not `configuration_error`, exit 3.** The reason
|
|
36
|
+
stays `task.stale` with the task's current `revision` in `details`; only the code and the exit number change
|
|
37
|
+
(cli-base's new `conflict`). A caller that branched on `configuration_error` for it should branch on the reason or
|
|
38
|
+
the new code. `vocabulary.stale`, `tag-review.stale` and `meeting-project.stale` keep `configuration_error` for now.
|
|
39
|
+
|
|
40
|
+
## 0.566.0 — 05.11.2026
|
|
41
|
+
|
|
42
|
+
### Changed — may break callers
|
|
43
|
+
|
|
44
|
+
- **cli-core now needs `@wirecat/cli-base` 0.76.0 or later** (peer range `>=0.76.0 <0.78.0`), which holds
|
|
45
|
+
`loadConstants`.
|
|
46
|
+
- **`appDoctorCommand`'s `oauthClient` returns `oauthClientReport`'s shape, `{ client, source, reason? }`**, and a
|
|
47
|
+
throw there is reported as `{ client: null, source: null, reason }` (was `error`). drive's and gcal's callers
|
|
48
|
+
change with their bump.
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- **`@wirecat/cli-core/constants`: WireCat's sign-in apps ship in cli-core's `constants.json`.** `constants()` reads
|
|
53
|
+
cli-base's, cli-core's and the tool's `constants.json` once per process, merged, validated and frozen; a tool
|
|
54
|
+
passes its own layer and a schema built on `constantsSchema`. It holds WireCat's Google Desktop app
|
|
55
|
+
(`google.app.client_id`, `client_secret`) and Microsoft client id (`microsoft.app.client_id`), which drive and mail
|
|
56
|
+
kept in source and gcal expected from a file CI wrote. `googleApp({ env, prefix, clientFile, clientId,
|
|
57
|
+
configClientId })` and `microsoftApp(…)` pick the app in WC-SECR-25's order: `--client-file` or `--client-id`
|
|
58
|
+
(`flag`), `<PREFIX>_<PROVIDER>_CLIENT_ID` (`env`), the account's `<provider>.client_id` (`config`), WireCat's app
|
|
59
|
+
(`constants`); a `_SECRET` with no id is a `configuration_error`, reason `sign-in.secret-without-id`. `oauthApp`
|
|
60
|
+
does the same for a provider whose app is in a tool's layer, such as Zoom, and returns nothing when there is none.
|
|
61
|
+
`oauthClientReport(app, reason?)` is `doctor`'s `oauth_client`: `{ client: "wirecat" | "own" | null, source }`.
|
|
62
|
+
See [Constants](docs/dev/CONSTANTS.md).
|
|
63
|
+
- **A sign-in entry records where its app came from** (`clientSource` in the standard and drive entries, read back
|
|
64
|
+
as `client.source`), so `doctor` reports the app the account signed in with. The standard entry also keeps
|
|
65
|
+
WireCat's public secret, so a refresh still works after a release ships another default app; an owner's own
|
|
66
|
+
secret is still never written.
|
|
9
67
|
|
|
10
68
|
## 0.565.0 — 05.11.2026
|
|
11
69
|
|
package/constants.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"google": {
|
|
3
|
+
"app": {
|
|
4
|
+
"client_id": "_WAlQe_1Jamd97lrbGU1kcXOJ8TWiJSbdfyTOEEF6IqtlI9-L2lr8iTW7MmAqUn7XMpK1OXeRZNLCFlYULdV3C5HGV28xT2CL0QFEI3FC3oHZp-ak5TX9g",
|
|
5
|
+
"client_secret": "X6afcHNZ_v0d_N6-jCWHFQSeT2O-saKLY8_WY_W5aIlswI6q2EZ6GtF3IFhy6WjIHUzB"
|
|
6
|
+
}
|
|
7
|
+
},
|
|
8
|
+
"microsoft": {
|
|
9
|
+
"app": {
|
|
10
|
+
"client_id": "f7f05d3a-b948-4d50-b3ce-59c816ce69e9"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -20,6 +20,8 @@ export interface Schedule {
|
|
|
20
20
|
/** A holder whose mark gates the run: it starts only when mail is stored past that mark. */
|
|
21
21
|
onNewMail?: string;
|
|
22
22
|
}
|
|
23
|
+
/** Another CLI a scheduled agent may run by name, as the host declares it: `memo`'s agent runs `mail`. */
|
|
24
|
+
export type AgentCli = Pick<AppIdentity, "command" | "appName" | "envPrefix">;
|
|
23
25
|
export interface LastRun {
|
|
24
26
|
at: string;
|
|
25
27
|
exitCode: number;
|
|
@@ -113,11 +115,13 @@ export declare const CREDENTIALS_UNREACHABLE = "credentials.unreachable";
|
|
|
113
115
|
* keyring, so a run that fails for that reason is tried again, a minute then up to 15 minutes apart, for up to
|
|
114
116
|
* 12 hours. A schedule that repeats every few minutes is left to its next tick; a run by hand fails at once.
|
|
115
117
|
*/
|
|
116
|
-
export declare const runSchedule: (app: AppIdentity, system: ServerSystem, env: NodeJS.ProcessEnv, file: string, { now, timer, signal, killAfterMs, untilUnlocked, wait, }?: {
|
|
118
|
+
export declare const runSchedule: (app: AppIdentity, system: ServerSystem, env: NodeJS.ProcessEnv, file: string, { now, timer, signal, killAfterMs, untilUnlocked, wait, agentRuns, }?: {
|
|
117
119
|
now?: () => Date;
|
|
118
120
|
timer?: Timer;
|
|
119
121
|
signal?: AbortSignal;
|
|
120
122
|
killAfterMs?: number;
|
|
121
123
|
untilUnlocked?: boolean;
|
|
122
124
|
wait?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
125
|
+
/** The other CLIs an agent may run; each runs under its own read-only lock and knows it runs from a schedule. */
|
|
126
|
+
agentRuns?: readonly AgentCli[];
|
|
123
127
|
}) => Promise<LastRun>;
|
|
@@ -358,7 +358,7 @@ const pause = (ms, signal) => new Promise((resolve) => {
|
|
|
358
358
|
* keyring, so a run that fails for that reason is tried again, a minute then up to 15 minutes apart, for up to
|
|
359
359
|
* 12 hours. A schedule that repeats every few minutes is left to its next tick; a run by hand fails at once.
|
|
360
360
|
*/
|
|
361
|
-
export const runSchedule = async (app, system, env, file, { now = () => new Date(), timer, signal, killAfterMs = KILL_AFTER_MS, untilUnlocked = false, wait = pause, } = {}) => {
|
|
361
|
+
export const runSchedule = async (app, system, env, file, { now = () => new Date(), timer, signal, killAfterMs = KILL_AFTER_MS, untilUnlocked = false, wait = pause, agentRuns = [], } = {}) => {
|
|
362
362
|
const schedule = readSchedule(file);
|
|
363
363
|
const folder = join(file, "..");
|
|
364
364
|
const runs = join(folder, "runs");
|
|
@@ -366,12 +366,12 @@ export const runSchedule = async (app, system, env, file, { now = () => new Date
|
|
|
366
366
|
const agent = schedule.kind === "agent";
|
|
367
367
|
const retries = untilUnlocked && !("every" in schedule.when);
|
|
368
368
|
const firstAt = now().getTime();
|
|
369
|
-
let last = await attemptSchedule(app, system, env, schedule, folder, { now, signal, killAfterMs });
|
|
369
|
+
let last = await attemptSchedule(app, system, env, schedule, folder, { now, signal, killAfterMs, agentRuns });
|
|
370
370
|
for (let delay = LOCKED_FIRST_WAIT_MS; retries && lockedOut(last) && !signal?.aborted && now().getTime() - firstAt + delay <= LOCKED_GIVE_UP_MS; delay = Math.min(delay * 2, LOCKED_LONGEST_WAIT_MS)) {
|
|
371
371
|
await wait(delay, signal);
|
|
372
372
|
if (signal?.aborted)
|
|
373
373
|
break;
|
|
374
|
-
last = await attemptSchedule(app, system, env, schedule, folder, { now, signal, killAfterMs });
|
|
374
|
+
last = await attemptSchedule(app, system, env, schedule, folder, { now, signal, killAfterMs, agentRuns });
|
|
375
375
|
}
|
|
376
376
|
const { exitCode, output } = last;
|
|
377
377
|
const errors = agent ? output.replace(/\.md$/, ".log") : output;
|
|
@@ -399,7 +399,7 @@ export const runSchedule = async (app, system, env, file, { now = () => new Date
|
|
|
399
399
|
return last;
|
|
400
400
|
};
|
|
401
401
|
/** One run of the schedule's command or agent: its output in `runs/`, its result in `last.json`. */
|
|
402
|
-
const attemptSchedule = async (app, system, env, schedule, folder, { now, signal, killAfterMs }) => {
|
|
402
|
+
const attemptSchedule = async (app, system, env, schedule, folder, { now, signal, killAfterMs, agentRuns, }) => {
|
|
403
403
|
const runs = join(folder, "runs");
|
|
404
404
|
const stamp = now()
|
|
405
405
|
.toISOString()
|
|
@@ -417,6 +417,12 @@ const attemptSchedule = async (app, system, env, schedule, folder, { now, signal
|
|
|
417
417
|
[`${app.envPrefix}_SCHEDULED`]: schedule.name,
|
|
418
418
|
// A run started under the lock stays under it, whatever the schedule file says.
|
|
419
419
|
...(agent || readOnlyLockOf(app, env) ? { [`${app.envPrefix}_READ_ONLY`]: "1" } : {}),
|
|
420
|
+
...(agent
|
|
421
|
+
? Object.fromEntries(agentRuns.flatMap((other) => [
|
|
422
|
+
[`${other.envPrefix}_READ_ONLY`, "1"],
|
|
423
|
+
[`${other.envPrefix}_SCHEDULED`, schedule.name],
|
|
424
|
+
]))
|
|
425
|
+
: {}),
|
|
420
426
|
};
|
|
421
427
|
// The child's output is data, and an agent's is another program's: it is kept as it came. These lines say
|
|
422
428
|
// only when it ran and how it ended.
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import type { OAuthClientReport } from "../constants/index.js";
|
|
2
3
|
import { type AppIdentity } from "../ports/app.js";
|
|
3
4
|
import type { Configuration } from "./settings.js";
|
|
4
5
|
export interface AppDoctorOptions {
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* The OAuth app a new sign-in would use (WC-SECR-25), read from the environment and files only, usually
|
|
8
|
+
* `oauthClientReport` of the tool's `googleApp`/`oauthApp`. A throw is reported as a null client with its message as
|
|
9
|
+
* the reason.
|
|
8
10
|
*/
|
|
9
|
-
oauthClient?: (env: NodeJS.ProcessEnv) =>
|
|
10
|
-
client: "wirecat" | "own";
|
|
11
|
-
};
|
|
11
|
+
oauthClient?: (env: NodeJS.ProcessEnv) => OAuthClientReport;
|
|
12
12
|
/** The npm package, so `--online` can say whether a newer version is published; without it that check is left out. */
|
|
13
13
|
packageName?: string;
|
|
14
14
|
/**
|
|
@@ -48,10 +48,7 @@ export declare const appDoctorReport: (app: AppIdentity, configuration: Configur
|
|
|
48
48
|
path: string;
|
|
49
49
|
exists: boolean;
|
|
50
50
|
};
|
|
51
|
-
oauth_client?:
|
|
52
|
-
client: "wirecat" | "own" | null;
|
|
53
|
-
error?: string;
|
|
54
|
-
} | undefined;
|
|
51
|
+
oauth_client?: OAuthClientReport | undefined;
|
|
55
52
|
read_only: string | null;
|
|
56
53
|
profile_lock: string | null;
|
|
57
54
|
config: {
|
|
@@ -90,10 +87,7 @@ export declare const appDoctorReport: (app: AppIdentity, configuration: Configur
|
|
|
90
87
|
path: string;
|
|
91
88
|
exists: boolean;
|
|
92
89
|
};
|
|
93
|
-
oauth_client?:
|
|
94
|
-
client: "wirecat" | "own" | null;
|
|
95
|
-
error?: string;
|
|
96
|
-
} | undefined;
|
|
90
|
+
oauth_client?: OAuthClientReport | undefined;
|
|
97
91
|
read_only: string | null;
|
|
98
92
|
profile_lock: string | null;
|
|
99
93
|
config: {
|
|
@@ -85,7 +85,7 @@ export const appDoctorReport = (app, configuration, env, command, options = {})
|
|
|
85
85
|
oauth = options.oauthClient(env);
|
|
86
86
|
}
|
|
87
87
|
catch (error) {
|
|
88
|
-
oauth = { client: null,
|
|
88
|
+
oauth = { client: null, source: null, reason: failure(error) };
|
|
89
89
|
}
|
|
90
90
|
const common = {
|
|
91
91
|
version: app.version,
|
package/dist/cli/bot/command.js
CHANGED
|
@@ -119,13 +119,16 @@ const meCommand = (bot) => annotate(new Command("me"), { effects: REMOTE_READ, e
|
|
|
119
119
|
}
|
|
120
120
|
});
|
|
121
121
|
});
|
|
122
|
-
const listCommand = (bot, tokenVariable) => withQuery(annotate(new Command("list"), {
|
|
122
|
+
const listCommand = (bot, tokenVariable) => withQuery(annotate(new Command("list"), {
|
|
123
|
+
effects: LOCAL_READ,
|
|
124
|
+
examples: [`${bot.app.command} bot list`, `${bot.app.command} bot list --check`],
|
|
125
|
+
}), BOT_PROFILES_QUERY)
|
|
123
126
|
.description(`every name on this machine that has a bot token; --check asks ${bot.name ?? "the messenger"} which bot each is`)
|
|
124
127
|
.summary("every bot profile on this machine; --check asks which bot each is")
|
|
125
128
|
.option("--check", "ask the messenger who each bot is, with its token")
|
|
126
129
|
.action(async function (words) {
|
|
127
130
|
const wanted = botProfileName(words);
|
|
128
|
-
const context = botContext(this, bot);
|
|
131
|
+
const context = botContext(this, bot, { needsProfile: false });
|
|
129
132
|
const check = this.opts().check === true;
|
|
130
133
|
if (check)
|
|
131
134
|
online(context, this);
|
|
@@ -2,14 +2,14 @@ import type { Command } from "commander";
|
|
|
2
2
|
import { SendJournal } from "../../sends/journal.js";
|
|
3
3
|
import { type Level, type PermissionKey } from "../../sends/permissions.js";
|
|
4
4
|
import { RecipientList } from "../../sends/recipients.js";
|
|
5
|
-
import type { Settings } from "../settings.js";
|
|
5
|
+
import type { ResolveOptions, Settings } from "../settings.js";
|
|
6
6
|
import type { BotAdapter, BotChatRef, BotConnectOptions, BotMessenger } from "./port.js";
|
|
7
7
|
import { ChatRegistry } from "./registry.js";
|
|
8
8
|
import { BotTokenStore } from "./token.js";
|
|
9
9
|
/** How the owner types this bot — `max sales bot` — so a refusal names the command that fixes it. */
|
|
10
10
|
export declare const botWords: (bot: BotMessenger, profile: string) => string;
|
|
11
11
|
/** What a bot command needs, resolved once: the bot's settings, its token, its lists. */
|
|
12
|
-
export declare const botContext: (command: Command, bot: BotMessenger) => {
|
|
12
|
+
export declare const botContext: (command: Command, bot: BotMessenger, resolving?: Pick<ResolveOptions, "needsProfile">) => {
|
|
13
13
|
settings: Settings;
|
|
14
14
|
renderer: import("@wirecat/cli-base").Renderer;
|
|
15
15
|
format: import("@wirecat/cli-base").RenderFormat;
|
package/dist/cli/bot/context.js
CHANGED
|
@@ -28,8 +28,8 @@ const refuseBotPermissions = (command, bot, settings) => {
|
|
|
28
28
|
}
|
|
29
29
|
};
|
|
30
30
|
/** What a bot command needs, resolved once: the bot's settings, its token, its lists. */
|
|
31
|
-
export const botContext = (command, bot) => {
|
|
32
|
-
const base = baseContext(command, (flags, options) => bot.resolveSettings(flags, { ...options, kind: "bot" }));
|
|
31
|
+
export const botContext = (command, bot, resolving = {}) => {
|
|
32
|
+
const base = baseContext(command, (flags, options) => bot.resolveSettings(flags, { ...options, ...resolving, kind: "bot" }));
|
|
33
33
|
const { settings, env, keyring } = base;
|
|
34
34
|
const profile = settings.profile;
|
|
35
35
|
const files = botFiles(bot.app, profile, env);
|
package/dist/cli/effects.d.ts
CHANGED
|
@@ -49,6 +49,16 @@ export declare const PROPOSE_COMMANDS: readonly ["messages acknowledge", "mail f
|
|
|
49
49
|
/** Marks a command of the `propose` tier; the lock lets it through only when its path is in `PROPOSE_COMMANDS`. */
|
|
50
50
|
export declare const proposesOnly: <T extends Command>(command: T) => T;
|
|
51
51
|
export declare const isProposal: (command: Command) => boolean;
|
|
52
|
+
/**
|
|
53
|
+
* The authority changes `<PREFIX>_READ_ONLY` lets through, a closed list: `mail rules add`, which the owner lets the
|
|
54
|
+
* scheduled mail triage run. Such a command is marked `narrowsUnderLock` **and** has one of these paths, and under the
|
|
55
|
+
* lock it narrows itself: it only adds what the owner can list and remove, and replaces nothing. Adding a path widens
|
|
56
|
+
* what a read-only agent decides for later runs, so it needs its own review.
|
|
57
|
+
*/
|
|
58
|
+
export declare const LOCKED_AUTHORITY_COMMANDS: readonly ["rules add"];
|
|
59
|
+
/** Marks an authority change that checks the lock itself; the lock lets it through only when its path is listed. */
|
|
60
|
+
export declare const narrowsUnderLock: <T extends Command>(command: T) => T;
|
|
61
|
+
export declare const admittedUnderLock: (command: Command) => boolean;
|
|
52
62
|
/** A local edit remains an edit even when it first fetches metadata from the messenger. */
|
|
53
63
|
export declare const changesStore: <T extends Command>(command: T) => T;
|
|
54
64
|
/**
|
package/dist/cli/effects.js
CHANGED
|
@@ -79,6 +79,25 @@ export const isProposal = (command) => {
|
|
|
79
79
|
const path = pathOf(command).join(" ");
|
|
80
80
|
return PROPOSE_COMMANDS.some((listed) => path === listed || path.endsWith(` ${listed}`));
|
|
81
81
|
};
|
|
82
|
+
/**
|
|
83
|
+
* The authority changes `<PREFIX>_READ_ONLY` lets through, a closed list: `mail rules add`, which the owner lets the
|
|
84
|
+
* scheduled mail triage run. Such a command is marked `narrowsUnderLock` **and** has one of these paths, and under the
|
|
85
|
+
* lock it narrows itself: it only adds what the owner can list and remove, and replaces nothing. Adding a path widens
|
|
86
|
+
* what a read-only agent decides for later runs, so it needs its own review.
|
|
87
|
+
*/
|
|
88
|
+
export const LOCKED_AUTHORITY_COMMANDS = ["rules add"];
|
|
89
|
+
const NARROWS = new WeakSet();
|
|
90
|
+
/** Marks an authority change that checks the lock itself; the lock lets it through only when its path is listed. */
|
|
91
|
+
export const narrowsUnderLock = (command) => {
|
|
92
|
+
NARROWS.add(command);
|
|
93
|
+
return command;
|
|
94
|
+
};
|
|
95
|
+
export const admittedUnderLock = (command) => {
|
|
96
|
+
if (!NARROWS.has(command))
|
|
97
|
+
return false;
|
|
98
|
+
const path = pathOf(command).join(" ");
|
|
99
|
+
return LOCKED_AUTHORITY_COMMANDS.some((listed) => path === listed || path.endsWith(` ${listed}`));
|
|
100
|
+
};
|
|
82
101
|
const STORE_WRITES = new WeakSet();
|
|
83
102
|
/** A local edit remains an edit even when it first fetches metadata from the messenger. */
|
|
84
103
|
export const changesStore = (command) => {
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { AI_ENTRIES, AI_SETTING_KEYS, type AISettings, resolveAISettings } from "../analysis/settings.js";
|
|
2
|
+
export type { AgentCli } from "../background/schedules.js";
|
|
2
3
|
export { parseDuration } from "../domain/duration.js";
|
|
3
4
|
export { openOf, PROFILE_PLACEHOLDER, type ProfilesFor, type RefOpen, type RefSummary, refLabel, } from "../domain/ref-summaries.js";
|
|
4
5
|
export { botTemplate } from "../entities/list-results.js";
|
|
@@ -70,7 +71,7 @@ export { sharedCommandTemplate } from "./contract-templates.js";
|
|
|
70
71
|
export { type Closeable, withDeadline } from "./deadline.js";
|
|
71
72
|
export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
|
|
72
73
|
export { DISCOVERY_DESCRIPTIONS } from "./discovery-descriptions.js";
|
|
73
|
-
export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, fetchesOnly, fetchesOnlyAt, grantsAuthority, isLogin, isOtherServiceCall, isProposal, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, PROPOSE_COMMANDS, previewFlag, proposesOnly, REMOTE_READ, REMOTE_WRITE, reachesOtherService, readsOverMcp, readsOverMcpAt, refuseUnderReadOnly, storeChangeRefusal, writesFileOnlyWith, } from "./effects.js";
|
|
74
|
+
export { admittedUnderLock, changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, fetchesOnly, fetchesOnlyAt, grantsAuthority, isLogin, isOtherServiceCall, isProposal, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, LOCKED_AUTHORITY_COMMANDS, logsInOrOut, NO_EFFECTS, narrowsUnderLock, ownerOnlyOption, PROPOSE_COMMANDS, previewFlag, proposesOnly, REMOTE_READ, REMOTE_WRITE, reachesOtherService, readsOverMcp, readsOverMcpAt, refuseUnderReadOnly, storeChangeRefusal, writesFileOnlyWith, } from "./effects.js";
|
|
74
75
|
export { type CliEntry, type EntryContext, type EntryIo, openBrowser, startCli } from "./entry.js";
|
|
75
76
|
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";
|
|
76
77
|
export { withAbort } from "./execution.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -62,7 +62,7 @@ export { sharedCommandTemplate } from "./contract-templates.js";
|
|
|
62
62
|
export { withDeadline } from "./deadline.js";
|
|
63
63
|
export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
|
|
64
64
|
export { DISCOVERY_DESCRIPTIONS } from "./discovery-descriptions.js";
|
|
65
|
-
export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, fetchesOnly, fetchesOnlyAt, grantsAuthority, isLogin, isOtherServiceCall, isProposal, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, PROPOSE_COMMANDS, previewFlag, proposesOnly, REMOTE_READ, REMOTE_WRITE, reachesOtherService, readsOverMcp, readsOverMcpAt, refuseUnderReadOnly, storeChangeRefusal, writesFileOnlyWith, } from "./effects.js";
|
|
65
|
+
export { admittedUnderLock, changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNothing, FETCH, fetchesOnly, fetchesOnlyAt, grantsAuthority, isLogin, isOtherServiceCall, isProposal, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, LOCKED_AUTHORITY_COMMANDS, logsInOrOut, NO_EFFECTS, narrowsUnderLock, ownerOnlyOption, PROPOSE_COMMANDS, previewFlag, proposesOnly, REMOTE_READ, REMOTE_WRITE, reachesOtherService, readsOverMcp, readsOverMcpAt, refuseUnderReadOnly, storeChangeRefusal, writesFileOnlyWith, } from "./effects.js";
|
|
66
66
|
export { openBrowser, startCli } from "./entry.js";
|
|
67
67
|
export { answerSchema, isBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
|
|
68
68
|
export { withAbort } from "./execution.js";
|
|
@@ -22,13 +22,18 @@ export const accountCommand = (messenger) => {
|
|
|
22
22
|
.description("the logged-in account")
|
|
23
23
|
.addCommand(withQuery(annotate(new Command("list"), { effects: REMOTE_READ, examples: [`${tool} account list`] }), ACCOUNT_PROFILES_QUERY)
|
|
24
24
|
.summary("profiles on this computer and their signed-in accounts")
|
|
25
|
-
.description("every profile on this computer, and the account each is logged in as;
|
|
25
|
+
.description("every account profile on this computer, and the account each is logged in as; a name that is only a bot " +
|
|
26
|
+
"is left out (`bot list` has it), one that is a bot too says `bot: true`; asks the messenger nothing")
|
|
26
27
|
.action(async function (words) {
|
|
27
28
|
const terms = queryTerms(words, ACCOUNT_PROFILES_QUERY, "account list");
|
|
28
29
|
const context = messengerContext(this, messenger, { needsProfile: false });
|
|
29
30
|
const { app, provider } = messenger;
|
|
30
31
|
const current = context.settings.sources.profile === NONE_NAMED ? undefined : context.settings.profile;
|
|
31
|
-
const
|
|
32
|
+
const { profilesHere, accountProfiles = profilesHere, botProfiles = [] } = context.settings;
|
|
33
|
+
const isAccount = (profile) => accountProfiles.includes(profile) || messenger.profileState?.(profile, context.env) === "signed-in";
|
|
34
|
+
const profiles = [
|
|
35
|
+
...new Set([...(current === undefined ? [] : [current]), ...profilesHere.filter(isAccount)]),
|
|
36
|
+
]
|
|
32
37
|
.filter((profile) => terms.get("profile") === undefined || profile === terms.get("profile"))
|
|
33
38
|
.filter((profile) => terms.get("current") === undefined ||
|
|
34
39
|
(profile === current) === (terms.get("current")?.toLowerCase() === "true"))
|
|
@@ -48,6 +53,7 @@ export const accountCommand = (messenger) => {
|
|
|
48
53
|
...(messenger.profileState
|
|
49
54
|
? { signed_in: messenger.profileState(profile, context.env) !== "signed-out" }
|
|
50
55
|
: {}),
|
|
56
|
+
...(botProfiles.includes(profile) ? { bot: true } : {}),
|
|
51
57
|
});
|
|
52
58
|
}
|
|
53
59
|
renderAnswer(this, context, wholeList("account list", items));
|
|
@@ -56,7 +56,10 @@ export const servedProfiles = (self, messenger, { settings, env }, given, words,
|
|
|
56
56
|
if (lock)
|
|
57
57
|
return { set: { option: lock, profiles: () => [lock], key: `_${lock}` } };
|
|
58
58
|
if (names.length === 1 && names[0] === EVERY_PROFILE) {
|
|
59
|
-
const every = () =>
|
|
59
|
+
const every = () => {
|
|
60
|
+
const here = messenger.resolveSettings(self.optsWithGlobals(), { env, needsProfile: false, readOnly: true });
|
|
61
|
+
return here.accountProfiles ?? here.profilesHere;
|
|
62
|
+
};
|
|
60
63
|
return { set: { option: EVERY_PROFILE, profiles: every, key: ALL_PROFILES } };
|
|
61
64
|
}
|
|
62
65
|
if (names.length === 0 || names.includes(EVERY_PROFILE))
|
|
@@ -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;
|
|
7
6
|
path: string;
|
|
8
7
|
exists: boolean;
|
|
8
|
+
error?: undefined;
|
|
9
9
|
} | {
|
|
10
10
|
path: string;
|
|
11
11
|
exists: boolean;
|
package/dist/cli/program.js
CHANGED
|
@@ -21,7 +21,7 @@ import { commandPathOf } from "./command-contract.js";
|
|
|
21
21
|
import { entityRows } from "./compact.js";
|
|
22
22
|
import { outputFor, provide } from "./context.js";
|
|
23
23
|
import { refuseByCommandPermissions, refuseWriteBeforeConnecting } from "./early-refusal.js";
|
|
24
|
-
import { changesAuthorityAt, changesAuthorityNow, isLogin, isStoreChange, judgedEffects, judgedTargets, storeChangeRefusal, } from "./effects.js";
|
|
24
|
+
import { admittedUnderLock, changesAuthorityAt, changesAuthorityNow, isLogin, isStoreChange, judgedEffects, judgedTargets, storeChangeRefusal, } from "./effects.js";
|
|
25
25
|
import { itemOf, renderAnswer, snakeKeys } from "./envelope.js";
|
|
26
26
|
import { byteCount, DEFAULT_AGENT_COOPERATIVE_MS, DEFAULT_COMMAND_MS, DEFAULT_COOPERATIVE_MS, DEFAULT_OUTPUT_BYTES, DEFAULT_SYNC_MS, execution, } from "./execution.js";
|
|
27
27
|
import { setupAtStart } from "./first-run.js";
|
|
@@ -334,7 +334,7 @@ const runOnce = async (argv, definition, options) => {
|
|
|
334
334
|
if (options.relayed && isLogin(action))
|
|
335
335
|
throw new CliError("permission_error", `\`${command} ${path.join(" ")}\` logs the account in or out, which serve never runs for a client — stop ` +
|
|
336
336
|
`serve first (\`${command} ${options.relayed.profile} server stop\`), or update ${command}, which then runs it here`, { sent: false });
|
|
337
|
-
if (lock && changesAuthorityNow(action) && !flags.dryRun)
|
|
337
|
+
if (lock && changesAuthorityNow(action) && !admittedUnderLock(action) && !flags.dryRun)
|
|
338
338
|
throw new CliError("permission_error", `this process is read-only (${lock}) — \`${command} ${path.join(" ")}\` changes what a later run may do; ` +
|
|
339
339
|
`run it without ${lock}`);
|
|
340
340
|
if (options.agent && options.agent.authority !== true && changesAuthorityNow(action))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import { type AgentCli } from "../../background/schedules.js";
|
|
2
3
|
import { type AppIdentity } from "../../ports/app.js";
|
|
3
4
|
import { type AnswerOutput } from "../envelope.js";
|
|
4
5
|
export interface ScheduleHost {
|
|
@@ -14,10 +15,15 @@ export interface ScheduleHost {
|
|
|
14
15
|
* the agent then runs only after a sync stored mail it has not processed.
|
|
15
16
|
*/
|
|
16
17
|
newMail?: (command: Command, holder: string) => Promise<boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* Other CLIs the scheduled agent may run besides this one, each under its own `<PREFIX>_READ_ONLY`: memo's mail
|
|
20
|
+
* triage runs `mail`. Claude may run them; codex may write their state, config and cache folders.
|
|
21
|
+
*/
|
|
22
|
+
agentRuns?: readonly AgentCli[];
|
|
17
23
|
}
|
|
18
24
|
/**
|
|
19
25
|
* Runs the CLI's own commands, or an agent, later or on a timer — through systemd (Linux), launchd
|
|
20
26
|
* (macOS) or Task Scheduler (Windows). Every timer runs `schedule exec <file>`; the schedule file holds
|
|
21
27
|
* the rest, so what runs and with which variables is the same on all three.
|
|
22
28
|
*/
|
|
23
|
-
export declare const scheduleCommandFor: ({ app, context: contextOf, newMail }: ScheduleHost) => Command;
|
|
29
|
+
export declare const scheduleCommandFor: ({ app, context: contextOf, newMail, agentRuns }: ScheduleHost) => Command;
|
|
@@ -81,7 +81,8 @@ const promptExample = (app) => app.messenger === false
|
|
|
81
81
|
* (macOS) or Task Scheduler (Windows). Every timer runs `schedule exec <file>`; the schedule file holds
|
|
82
82
|
* the rest, so what runs and with which variables is the same on all three.
|
|
83
83
|
*/
|
|
84
|
-
export const scheduleCommandFor = ({ app, context: contextOf, newMail }) => {
|
|
84
|
+
export const scheduleCommandFor = ({ app, context: contextOf, newMail, agentRuns = [] }) => {
|
|
85
|
+
const agentClis = [app, ...agentRuns];
|
|
85
86
|
const command = new Command("schedule").description(`run a ${app.command} command or an agent later or on a timer (systemd, launchd, Task Scheduler); an agent cannot send`);
|
|
86
87
|
const prepare = (self, resolving = {}) => {
|
|
87
88
|
const context = contextOf(self, resolving);
|
|
@@ -154,7 +155,7 @@ export const scheduleCommandFor = ({ app, context: contextOf, newMail }) => {
|
|
|
154
155
|
`${app.command} personal schedule agent triage codex "list what needs an answer today" --every 2h --max-cost 1`,
|
|
155
156
|
],
|
|
156
157
|
}))
|
|
157
|
-
.description(`start an agent (claude or codex) with a prompt on a timer; it runs with ${
|
|
158
|
+
.description(`start an agent (claude or codex) with a prompt on a timer; it runs with ${agentClis.map((one) => `${one.envPrefix}_READ_ONLY=1`).join(" and ")}, so it reads and drafts and cannot send — its answer is kept and you are notified`)
|
|
158
159
|
.argument("<name>", "lowercase letters, digits and dashes")
|
|
159
160
|
.addArgument(new Argument("<harness>", "the agent to start").choices(HARNESSES))
|
|
160
161
|
.argument("<prompt>", `what to do, e.g. "${promptExample(app)}"`)
|
|
@@ -166,7 +167,10 @@ export const scheduleCommandFor = ({ app, context: contextOf, newMail }) => {
|
|
|
166
167
|
const program = findProgram(harness, context.env, system.platform);
|
|
167
168
|
if (!program)
|
|
168
169
|
throw new CliError("not_found", `${harness} is not on PATH`);
|
|
169
|
-
const
|
|
170
|
+
const folders = agentClis.flatMap((one) => {
|
|
171
|
+
const paths = resolvePaths({ appName: one.appName, prefix: one.envPrefix, env: context.env });
|
|
172
|
+
return [paths.state, paths.config, paths.cache];
|
|
173
|
+
});
|
|
170
174
|
const argv = harness === "claude"
|
|
171
175
|
? [
|
|
172
176
|
program,
|
|
@@ -176,7 +180,7 @@ export const scheduleCommandFor = ({ app, context: contextOf, newMail }) => {
|
|
|
176
180
|
"--permission-mode",
|
|
177
181
|
"dontAsk",
|
|
178
182
|
"--allowedTools",
|
|
179
|
-
`Bash(${
|
|
183
|
+
...agentClis.map((one) => `Bash(${one.command}:*)`),
|
|
180
184
|
"--no-session-persistence",
|
|
181
185
|
...(options.maxCost ? ["--max-budget-usd", String(options.maxCost)] : []),
|
|
182
186
|
]
|
|
@@ -187,13 +191,10 @@ export const scheduleCommandFor = ({ app, context: contextOf, newMail }) => {
|
|
|
187
191
|
"--ephemeral",
|
|
188
192
|
"--sandbox",
|
|
189
193
|
"workspace-write",
|
|
190
|
-
// The
|
|
194
|
+
// The CLIs reach their provider and write the store and their own state, and nothing else.
|
|
191
195
|
"-c",
|
|
192
196
|
"sandbox_workspace_write.network_access=true",
|
|
193
|
-
...[dirname(storePath(context.env)),
|
|
194
|
-
"--add-dir",
|
|
195
|
-
dir,
|
|
196
|
-
]),
|
|
197
|
+
...[dirname(storePath(context.env)), ...folders].flatMap((dir) => ["--add-dir", dir]),
|
|
197
198
|
prompt,
|
|
198
199
|
];
|
|
199
200
|
await save({
|
|
@@ -263,6 +264,7 @@ export const scheduleCommandFor = ({ app, context: contextOf, newMail }) => {
|
|
|
263
264
|
return say({ name, skipped: true, holder }, [`${name}: skipped — no mail stored past ${holder}'s mark`]);
|
|
264
265
|
const last = await untilChildEnds(this, (signal) => runSchedule(app, system, context.env, scheduleFile(state, scheduleName(name)), {
|
|
265
266
|
timer: timer(),
|
|
267
|
+
agentRuns,
|
|
266
268
|
...(signal ? { signal } : {}),
|
|
267
269
|
}));
|
|
268
270
|
say(snakeKeys({ name, ...last }), [`${name}: exit ${last.exitCode} — ${last.output}`]);
|
|
@@ -297,6 +299,7 @@ export const scheduleCommandFor = ({ app, context: contextOf, newMail }) => {
|
|
|
297
299
|
return;
|
|
298
300
|
const last = await untilChildEnds(this, (signal) => runSchedule(app, system, context.env, file, {
|
|
299
301
|
timer: timer(),
|
|
302
|
+
agentRuns,
|
|
300
303
|
untilUnlocked: true,
|
|
301
304
|
...(signal ? { signal } : {}),
|
|
302
305
|
}));
|
package/dist/cli/settings.d.ts
CHANGED
|
@@ -156,8 +156,16 @@ export interface Settings extends AISettings {
|
|
|
156
156
|
configFound: boolean;
|
|
157
157
|
/** Profiles the configuration file names, whether or not anyone has logged in to them. */
|
|
158
158
|
configuredProfiles: string[];
|
|
159
|
-
/** Every profile on this computer — what
|
|
159
|
+
/** Every profile on this computer, bots included — what a missing profile's error lists. */
|
|
160
160
|
profilesHere: string[];
|
|
161
|
+
/**
|
|
162
|
+
* The profiles here that are an account, not only a bot — what `account list` shows. A name only the file's bot
|
|
163
|
+
* section names, with no personal section, recorded login or messenger profile of its own, is a bot. Absent for an
|
|
164
|
+
* app that builds `Settings` itself; `profilesHere` stands in.
|
|
165
|
+
*/
|
|
166
|
+
accountProfiles?: string[];
|
|
167
|
+
/** Every name with a bot: the file's bot section, or a bot's own files. `account list` marks these. */
|
|
168
|
+
botProfiles?: string[];
|
|
161
169
|
/** Where each value came from — `config show` prints it. */
|
|
162
170
|
sources: Record<string, Source>;
|
|
163
171
|
/** This profile's entries and the shared defaults, for an adapter resolving its own fields with `first`. */
|
package/dist/cli/settings.js
CHANGED
|
@@ -10,9 +10,10 @@ import { assertOverridesTighten, CEILING_KEY, COMMAND_PATH_KEY, LEVELS, layerPer
|
|
|
10
10
|
import { DEFAULT_HISTORY_LIMITS, HISTORY_MAX_DAYS } from "../services/history-limits.js";
|
|
11
11
|
import { storePath } from "../store/path.js";
|
|
12
12
|
import { sharedConfigPath } from "../store/shared-config.js";
|
|
13
|
+
import { registryProfiles } from "./bot/registry.js";
|
|
13
14
|
import { checkConfigText, issueMessage, readConfigFile, readConfigText, savedPermissionKeys, schemaIssues, settingPathOf, withoutPath, } from "./config-issues.js";
|
|
14
15
|
import { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
|
|
15
|
-
import { profilesHere } from "./messenger/accounts.js";
|
|
16
|
+
import { profilesHere, profilesWithAccounts } from "./messenger/accounts.js";
|
|
16
17
|
import { usableProfileName } from "./profile.js";
|
|
17
18
|
import { withSettingDefaults } from "./setting-defaults.js";
|
|
18
19
|
import { permissionCeiling } from "./setup-answers.js";
|
|
@@ -298,6 +299,16 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
298
299
|
throw new CliError("validation_error", `name a profile: \`${app.command} <profile> …\` — profiles here: ${here.join(", ")} (or set ${PROFILE})`, { profiles: here });
|
|
299
300
|
};
|
|
300
301
|
const profilesOf = (config, env) => profilesHere(app, [...namedProfiles(config), ...(extension.profiles?.(env) ?? [])], env);
|
|
302
|
+
const botsOf = (config, env) => [...new Set([...Object.keys(config.bot?.profiles ?? {}), ...registryProfiles(app, env)])].sort();
|
|
303
|
+
// The shared `profiles` section is no sign of an account: a bot's owner may set something there for both sides.
|
|
304
|
+
const accountsOf = (config, env, bots) => {
|
|
305
|
+
const personal = new Set([
|
|
306
|
+
...Object.keys(config.personal?.profiles ?? {}),
|
|
307
|
+
...profilesWithAccounts(app, env),
|
|
308
|
+
...(extension.profiles?.(env) ?? []),
|
|
309
|
+
]);
|
|
310
|
+
return profilesOf(config, env).filter((name) => !bots.includes(name) || personal.has(name));
|
|
311
|
+
};
|
|
301
312
|
const resolveSettings = (flags = {}, options = {}) => {
|
|
302
313
|
const env = options.env ?? process.env;
|
|
303
314
|
const configPath = configPathFor(options);
|
|
@@ -322,6 +333,7 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
322
333
|
? unnamed(config, env, options.needsProfile !== false)
|
|
323
334
|
: { value: named.value, from: named.from }, lock);
|
|
324
335
|
const kind = options.kind ?? "personal";
|
|
336
|
+
const bots = botsOf(config, env);
|
|
325
337
|
const name = usableProfileName(profile.value);
|
|
326
338
|
const configured = config.profiles[name] ?? {};
|
|
327
339
|
const shared = config.defaults ?? {};
|
|
@@ -429,6 +441,8 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
429
441
|
configFound: existsSync(configPath),
|
|
430
442
|
configuredProfiles: namedProfiles(config),
|
|
431
443
|
profilesHere: profilesOf(config, env),
|
|
444
|
+
accountProfiles: accountsOf(config, env, bots),
|
|
445
|
+
botProfiles: bots,
|
|
432
446
|
sources: {
|
|
433
447
|
...ai.sources,
|
|
434
448
|
profile: profile.from,
|
|
@@ -527,10 +541,22 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
527
541
|
if (kind !== "bot" && botOnlyKeys.includes(setting)) {
|
|
528
542
|
throw new CliError("validation_error", `${setting} is a bot's setting — add --bot`);
|
|
529
543
|
}
|
|
530
|
-
const changed = structuredClone(raw);
|
|
531
|
-
setAt(changed, at, level === undefined ? parseValue(value) : value.trim());
|
|
532
544
|
const had = new Set(before.map((issue) => issue.at.join(".")));
|
|
533
|
-
const
|
|
545
|
+
const tried = (typed) => {
|
|
546
|
+
const changed = structuredClone(raw);
|
|
547
|
+
setAt(changed, at, typed);
|
|
548
|
+
const added = schemaIssues(changed, readSchema, context(path)).find((issue) => !had.has(issue.at.join(".")) || issue.at.join(".") === at.join("."));
|
|
549
|
+
return { changed, added };
|
|
550
|
+
};
|
|
551
|
+
const parsed = level === undefined ? parseValue(value) : value.trim();
|
|
552
|
+
let { changed, added } = tried(parsed);
|
|
553
|
+
// `phone_number_id 100000000000001` parses as a number; a setting that takes text keeps the digits as typed,
|
|
554
|
+
// never through a number, which rounds any id past 2^53.
|
|
555
|
+
if (added && typeof parsed !== "string") {
|
|
556
|
+
const asText = tried(value.trim());
|
|
557
|
+
if (!asText.added)
|
|
558
|
+
({ changed, added } = asText);
|
|
559
|
+
}
|
|
534
560
|
if (added)
|
|
535
561
|
throw new CliError("validation_error", `${setting} cannot be "${value}": ${added.what}`);
|
|
536
562
|
const checked = v.safeParse(readSchema, changed);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
import type { AppSource, OAuthClient } from "../oauth/provider.js";
|
|
3
|
+
/** cli-core's layer, read after cli-base's and before the tool's. */
|
|
4
|
+
export declare const CORE_CONSTANTS: import("url").URL;
|
|
5
|
+
/** What cli-base's and cli-core's layers hold; a tool extends it with `v.object({ ...constantsSchema.entries, … })`. */
|
|
6
|
+
export declare const constantsSchema: v.ObjectSchema<{
|
|
7
|
+
readonly google: v.ObjectSchema<{
|
|
8
|
+
readonly app: v.ObjectSchema<{
|
|
9
|
+
readonly client_id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.NonEmptyAction<string, undefined>, v.TransformAction<string, string>]>;
|
|
10
|
+
readonly client_secret: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TransformAction<string, string>]>, undefined>;
|
|
11
|
+
}, undefined>;
|
|
12
|
+
}, undefined>;
|
|
13
|
+
readonly microsoft: v.ObjectSchema<{
|
|
14
|
+
readonly app: v.ObjectSchema<{
|
|
15
|
+
readonly client_id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.NonEmptyAction<string, undefined>]>;
|
|
16
|
+
}, undefined>;
|
|
17
|
+
}, undefined>;
|
|
18
|
+
}, undefined>;
|
|
19
|
+
export type Constants = v.InferOutput<typeof constantsSchema>;
|
|
20
|
+
/**
|
|
21
|
+
* The packaged constants, base → core → the tool's `constants.json` when given, read once per process for each tool
|
|
22
|
+
* layer. A tool with constants of its own passes its layer and a schema built on `constantsSchema`; the first call for
|
|
23
|
+
* a layer decides its schema.
|
|
24
|
+
*/
|
|
25
|
+
export declare const constants: <TSchema extends v.GenericSchema<unknown, Constants> = typeof constantsSchema>(toolLayer?: URL, schema?: TSchema) => v.InferOutput<TSchema>;
|
|
26
|
+
export interface AppOptions {
|
|
27
|
+
env: NodeJS.ProcessEnv;
|
|
28
|
+
/** The tool's environment prefix: `GCAL` with provider `google` reads `GCAL_GOOGLE_CLIENT_ID` and `_SECRET`. */
|
|
29
|
+
prefix: string;
|
|
30
|
+
/** `--client-id`. A secret for it comes from `<PREFIX>_<PROVIDER>_CLIENT_SECRET`. */
|
|
31
|
+
clientId?: string | undefined;
|
|
32
|
+
/** The account's config key `<provider>.client_id`. A config file never holds the secret: it comes from the env. */
|
|
33
|
+
configClientId?: string | undefined;
|
|
34
|
+
}
|
|
35
|
+
export interface OAuthAppOptions extends AppOptions {
|
|
36
|
+
/** `google`, `microsoft`, `zoom`: names the env variables, upper-cased. */
|
|
37
|
+
provider: string;
|
|
38
|
+
/** WireCat's app from the constants; an empty or missing id means WireCat has none for this provider. */
|
|
39
|
+
wirecat?: {
|
|
40
|
+
client_id?: string | undefined;
|
|
41
|
+
client_secret?: string | undefined;
|
|
42
|
+
} | undefined;
|
|
43
|
+
}
|
|
44
|
+
export interface OAuthApp {
|
|
45
|
+
client: OAuthClient & {
|
|
46
|
+
source: AppSource;
|
|
47
|
+
};
|
|
48
|
+
source: AppSource;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The app a sign-in uses, first match wins (WC-SECR-25): `--client-id`, `<PREFIX>_<PROVIDER>_CLIENT_ID`, the account's
|
|
52
|
+
* `<provider>.client_id`, WireCat's app. A secret without any of the first three ids is a `configuration_error`.
|
|
53
|
+
* Undefined when nothing names an app and WireCat has none.
|
|
54
|
+
*/
|
|
55
|
+
export declare const oauthApp: (options: OAuthAppOptions) => OAuthApp | undefined;
|
|
56
|
+
export interface GoogleAppOptions extends AppOptions {
|
|
57
|
+
/** The owner's Desktop client JSON, `--client-file`; it must be private to the owner. */
|
|
58
|
+
clientFile?: string | undefined;
|
|
59
|
+
platform?: NodeJS.Platform;
|
|
60
|
+
/** The tool's own constants, when its layer overrides the app. */
|
|
61
|
+
constants?: Constants;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The Google Desktop app a sign-in uses: `--client-file`, then `oauthApp`'s order. Google treats a Desktop app's
|
|
65
|
+
* secret as public (https://developers.google.com/identity/protocols/oauth2/native-app), so WireCat's ships, obscured,
|
|
66
|
+
* in cli-core's constants.
|
|
67
|
+
*/
|
|
68
|
+
export declare const googleApp: (options: GoogleAppOptions) => OAuthApp;
|
|
69
|
+
/** WireCat's Microsoft app is a public client: it has an id and no secret. */
|
|
70
|
+
export declare const microsoftApp: (options: AppOptions & {
|
|
71
|
+
constants?: Constants;
|
|
72
|
+
}) => OAuthApp;
|
|
73
|
+
export interface OAuthClientReport {
|
|
74
|
+
client: "wirecat" | "own" | null;
|
|
75
|
+
source: AppSource | null;
|
|
76
|
+
/** Why there is no app, when `client` is null. */
|
|
77
|
+
reason?: string;
|
|
78
|
+
}
|
|
79
|
+
/** `doctor`'s `oauth_client` for one provider: the app a new sign-in would use; doctor reads no keyring. */
|
|
80
|
+
export declare const oauthClientReport: (app: {
|
|
81
|
+
source?: AppSource | undefined;
|
|
82
|
+
} | undefined, reason?: string) => OAuthClientReport;
|
|
83
|
+
export type { AppSource };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { BASE_CONSTANTS, baseConstantsSchema, CliError, loadConstants } from "@wirecat/cli-base";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
import { readClientFile } from "../oauth/client-file.js";
|
|
4
|
+
import { reveal } from "./obscure.js";
|
|
5
|
+
/** cli-core's layer, read after cli-base's and before the tool's. */
|
|
6
|
+
export const CORE_CONSTANTS = new URL("../../constants.json", import.meta.url);
|
|
7
|
+
/** What cli-base's and cli-core's layers hold; a tool extends it with `v.object({ ...constantsSchema.entries, … })`. */
|
|
8
|
+
export const constantsSchema = v.object({
|
|
9
|
+
...baseConstantsSchema.entries,
|
|
10
|
+
google: v.object({
|
|
11
|
+
app: v.object({
|
|
12
|
+
client_id: v.pipe(v.string(), v.nonEmpty(), v.transform(reveal)),
|
|
13
|
+
client_secret: v.optional(v.pipe(v.string(), v.transform(reveal))),
|
|
14
|
+
}),
|
|
15
|
+
}),
|
|
16
|
+
microsoft: v.object({ app: v.object({ client_id: v.pipe(v.string(), v.nonEmpty()) }) }),
|
|
17
|
+
});
|
|
18
|
+
const loaded = new Map();
|
|
19
|
+
/**
|
|
20
|
+
* The packaged constants, base → core → the tool's `constants.json` when given, read once per process for each tool
|
|
21
|
+
* layer. A tool with constants of its own passes its layer and a schema built on `constantsSchema`; the first call for
|
|
22
|
+
* a layer decides its schema.
|
|
23
|
+
*/
|
|
24
|
+
export const constants = (toolLayer, schema) => {
|
|
25
|
+
const key = toolLayer?.href ?? "";
|
|
26
|
+
if (!loaded.has(key)) {
|
|
27
|
+
const layers = toolLayer ? [BASE_CONSTANTS, CORE_CONSTANTS, toolLayer] : [BASE_CONSTANTS, CORE_CONSTANTS];
|
|
28
|
+
loaded.set(key, loadConstants(layers, schema ?? constantsSchema));
|
|
29
|
+
}
|
|
30
|
+
return loaded.get(key);
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The app a sign-in uses, first match wins (WC-SECR-25): `--client-id`, `<PREFIX>_<PROVIDER>_CLIENT_ID`, the account's
|
|
34
|
+
* `<provider>.client_id`, WireCat's app. A secret without any of the first three ids is a `configuration_error`.
|
|
35
|
+
* Undefined when nothing names an app and WireCat has none.
|
|
36
|
+
*/
|
|
37
|
+
export const oauthApp = (options) => {
|
|
38
|
+
const { env, prefix, provider } = options;
|
|
39
|
+
const name = `${prefix}_${provider.toUpperCase()}_CLIENT`;
|
|
40
|
+
const clientSecret = env[`${name}_SECRET`];
|
|
41
|
+
const own = [
|
|
42
|
+
[options.clientId, "flag"],
|
|
43
|
+
[env[`${name}_ID`], "env"],
|
|
44
|
+
[options.configClientId, "config"],
|
|
45
|
+
];
|
|
46
|
+
const [clientId, source] = own.find(([id]) => id) ?? [];
|
|
47
|
+
if (clientId && source)
|
|
48
|
+
return app({ clientId, ...(clientSecret ? { clientSecret } : {}) }, source);
|
|
49
|
+
if (clientSecret)
|
|
50
|
+
throw new CliError("configuration_error", `${name}_SECRET is set without a client id; set ${name}_ID too, or unset it to use WireCat's app`, { reason: "sign-in.secret-without-id" });
|
|
51
|
+
const wirecat = options.wirecat;
|
|
52
|
+
if (!wirecat?.client_id)
|
|
53
|
+
return undefined;
|
|
54
|
+
return app({ clientId: wirecat.client_id, ...(wirecat.client_secret ? { clientSecret: wirecat.client_secret } : {}) }, "constants");
|
|
55
|
+
};
|
|
56
|
+
const app = (client, source) => ({ client: { ...client, source }, source });
|
|
57
|
+
/**
|
|
58
|
+
* The Google Desktop app a sign-in uses: `--client-file`, then `oauthApp`'s order. Google treats a Desktop app's
|
|
59
|
+
* secret as public (https://developers.google.com/identity/protocols/oauth2/native-app), so WireCat's ships, obscured,
|
|
60
|
+
* in cli-core's constants.
|
|
61
|
+
*/
|
|
62
|
+
export const googleApp = (options) => {
|
|
63
|
+
const { clientFile, platform } = options;
|
|
64
|
+
if (clientFile)
|
|
65
|
+
return app(readClientFile(clientFile, platform ? { platform } : {}), "flag");
|
|
66
|
+
const wirecat = (options.constants ?? constants()).google.app;
|
|
67
|
+
return oauthApp({ ...options, provider: "google", wirecat });
|
|
68
|
+
};
|
|
69
|
+
/** WireCat's Microsoft app is a public client: it has an id and no secret. */
|
|
70
|
+
export const microsoftApp = (options) => {
|
|
71
|
+
const wirecat = (options.constants ?? constants()).microsoft.app;
|
|
72
|
+
return oauthApp({ ...options, provider: "microsoft", wirecat });
|
|
73
|
+
};
|
|
74
|
+
/** `doctor`'s `oauth_client` for one provider: the app a new sign-in would use; doctor reads no keyring. */
|
|
75
|
+
export const oauthClientReport = (app, reason) => app?.source
|
|
76
|
+
? { client: app.source === "constants" ? "wirecat" : "own", source: app.source }
|
|
77
|
+
: { client: null, source: null, ...(reason ? { reason } : {}) };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const reveal: (obscured: string) => string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createDecipheriv } from "node:crypto";
|
|
2
|
+
// Not protection: anyone holding the package can reveal these. It keeps secret scanners and a casual read from
|
|
3
|
+
// treating WireCat's public Desktop app as a leaked credential, as rclone does for its own app.
|
|
4
|
+
const KEY = Buffer.from("d43012779aa2dded3e98092a3dbcf5d8980c564172f5ce0a5fd0574e5f740339", "hex");
|
|
5
|
+
export const reveal = (obscured) => {
|
|
6
|
+
const bytes = Buffer.from(obscured, "base64url");
|
|
7
|
+
const decipher = createDecipheriv("aes-256-ctr", KEY, bytes.subarray(0, 16));
|
|
8
|
+
return Buffer.concat([decipher.update(bytes.subarray(16)), decipher.final()]).toString("utf8");
|
|
9
|
+
};
|
|
@@ -6,4 +6,6 @@ export declare const accountProfile: v.StrictObjectSchema<{
|
|
|
6
6
|
readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
7
7
|
/** False for a profile with no session on this computer; absent when the messenger does not say. */
|
|
8
8
|
readonly signed_in: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
9
|
+
/** True when the same name is a bot profile too (`<tool> <profile> bot …`); absent otherwise. */
|
|
10
|
+
readonly bot: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
|
|
9
11
|
}, undefined>;
|
|
@@ -7,4 +7,6 @@ export const accountProfile = v.strictObject({
|
|
|
7
7
|
name: v.nullable(text),
|
|
8
8
|
/** False for a profile with no session on this computer; absent when the messenger does not say. */
|
|
9
9
|
signed_in: v.optional(v.boolean()),
|
|
10
|
+
/** True when the same name is a bot profile too (`<tool> <profile> bot …`); absent otherwise. */
|
|
11
|
+
bot: v.optional(v.literal(true)),
|
|
10
12
|
});
|
package/dist/entities/index.d.ts
CHANGED
|
@@ -3885,6 +3885,7 @@ export declare const ENTITIES: {
|
|
|
3885
3885
|
readonly account: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
|
|
3886
3886
|
readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
3887
3887
|
readonly signed_in: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
3888
|
+
readonly bot: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
|
|
3888
3889
|
}, undefined>;
|
|
3889
3890
|
readonly organization: v.StrictObjectSchema<{
|
|
3890
3891
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
package/dist/errors/reasons.js
CHANGED
|
@@ -33,6 +33,7 @@ const SIGN_IN = [
|
|
|
33
33
|
["sign-in.client-file-invalid", "configuration_error", "the file is not a Desktop app OAuth client JSON"],
|
|
34
34
|
["oauth.scope-missing", "permission_error", "the sign-in lacks a permission this command needs"],
|
|
35
35
|
["sign-in.client-rejected", "configuration_error", "the provider rejected the OAuth client"],
|
|
36
|
+
["sign-in.secret-without-id", "configuration_error", "an OAuth client secret is set without its client id"],
|
|
36
37
|
];
|
|
37
38
|
/** Reasons every CLI over a provider's API raises alike: gcal and drive today. */
|
|
38
39
|
const PROVIDER_API = [
|
|
@@ -234,7 +235,7 @@ const OTHERS = [
|
|
|
234
235
|
validation("task.category-unknown", "the category is not in the task category list"),
|
|
235
236
|
validation("task.claimed", "someone else holds the task"),
|
|
236
237
|
validation("task.closed", "the task is already closed"),
|
|
237
|
-
["task.stale", "
|
|
238
|
+
["task.stale", "conflict", "the task changed since it was read; read it again"],
|
|
238
239
|
validation("task.column", "the task beside which it moves is in another column than the status asked for"),
|
|
239
240
|
validation("task.dependency-cycle", "the task would depend on itself"),
|
|
240
241
|
validation("task.is-subtask", "a subtask moves to another project alone only when it is detached from its parent in the same write"),
|
package/dist/oauth/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { type Authorization, type AuthorizeOptions, authorize } from "./authorize.js";
|
|
2
2
|
export { type ClientFileOptions, readClientFile } from "./client-file.js";
|
|
3
3
|
export { type SignInOptions, type TokenKeeper, type TokenKeeperOptions, type TokensOptions, tokenKeeper, } from "./keeper.js";
|
|
4
|
-
export type { ExchangeContext, OAuthClient, OAuthProvider, RequestContext, TokenSet } from "./provider.js";
|
|
4
|
+
export type { AppSource, ExchangeContext, OAuthClient, OAuthProvider, RequestContext, TokenSet } from "./provider.js";
|
|
5
5
|
export { requestTokens, revokeToken, TOKEN_LOG_ALLOW, TOKEN_LOG_EVENT } from "./requests.js";
|
|
6
6
|
export { type CredentialStorageOptions, credentialStorage, driveSessionCodec, type EncodeInput, oauthAccount, type StoredTokens, standardCodec, type TokenCodec, type TokenStorage, zoomTokenCodec, } from "./storage.js";
|
package/dist/oauth/provider.d.ts
CHANGED
|
@@ -3,7 +3,12 @@ import type { Logger } from "@wirecat/cli-base";
|
|
|
3
3
|
export interface OAuthClient {
|
|
4
4
|
clientId: string;
|
|
5
5
|
clientSecret?: string;
|
|
6
|
+
/** Where the sign-in found the app (WC-SECR-25); saved with the entry, so `doctor` reports the app it used. */
|
|
7
|
+
source?: AppSource;
|
|
6
8
|
}
|
|
9
|
+
/** `flag`: `--client-file` or `--client-id`; `env`: `<TOOL>_<PROVIDER>_CLIENT_ID`; `config`: the account's
|
|
10
|
+
* `<provider>.client_id`; `constants`: WireCat's app. */
|
|
11
|
+
export type AppSource = "flag" | "env" | "config" | "constants";
|
|
7
12
|
export interface TokenSet {
|
|
8
13
|
accessToken?: string;
|
|
9
14
|
refreshToken: string;
|
package/dist/oauth/storage.js
CHANGED
|
@@ -40,8 +40,9 @@ export const standardCodec = (providerId) => ({
|
|
|
40
40
|
throw invalid();
|
|
41
41
|
if (!Array.isArray(value.scopes) || !value.scopes.every((scope) => typeof scope === "string"))
|
|
42
42
|
throw invalid();
|
|
43
|
+
const source = text(value.clientSource);
|
|
43
44
|
return {
|
|
44
|
-
client: { clientId },
|
|
45
|
+
client: defined({ clientId, clientSecret: text(value.clientSecret), source }),
|
|
45
46
|
tokens: defined({
|
|
46
47
|
accessToken: text(value.accessToken),
|
|
47
48
|
refreshToken,
|
|
@@ -57,6 +58,10 @@ export const standardCodec = (providerId) => ({
|
|
|
57
58
|
version: 1,
|
|
58
59
|
provider: providerId,
|
|
59
60
|
clientId: client.clientId,
|
|
61
|
+
// Only WireCat's secret, which is public: a later release may ship another default app, and a refresh needs
|
|
62
|
+
// the secret of the app that signed in. An owner's own secret stays where the owner keeps it.
|
|
63
|
+
clientSecret: client.source === "constants" ? client.clientSecret : undefined,
|
|
64
|
+
clientSource: client.source,
|
|
60
65
|
accessToken: tokens.accessToken,
|
|
61
66
|
refreshToken: tokens.refreshToken,
|
|
62
67
|
expiresAt: tokens.expiresAt,
|
|
@@ -80,7 +85,11 @@ export const driveSessionCodec = {
|
|
|
80
85
|
if (!refreshToken || !clientId || typeof value.principal !== "string")
|
|
81
86
|
throw invalid();
|
|
82
87
|
return {
|
|
83
|
-
client: defined({
|
|
88
|
+
client: defined({
|
|
89
|
+
clientId,
|
|
90
|
+
clientSecret: text(value.clientSecret),
|
|
91
|
+
source: text(value.clientSource),
|
|
92
|
+
}),
|
|
84
93
|
tokens: defined({
|
|
85
94
|
accessToken: text(fields.access_token),
|
|
86
95
|
refreshToken,
|
|
@@ -98,6 +107,7 @@ export const driveSessionCodec = {
|
|
|
98
107
|
...extra,
|
|
99
108
|
clientId: client.clientId,
|
|
100
109
|
...(client.clientSecret ? { clientSecret: client.clientSecret } : {}),
|
|
110
|
+
...(client.source ? { clientSource: client.source } : {}),
|
|
101
111
|
tokens: defined({
|
|
102
112
|
...oldTokens,
|
|
103
113
|
access_token: tokens.accessToken,
|
|
@@ -68,7 +68,7 @@ const timesAsIso = (changes) => Object.fromEntries(Object.entries(changes).map((
|
|
|
68
68
|
export const checkRevision = (row, expected) => {
|
|
69
69
|
if (expected === undefined || Number(row.revision) === expected)
|
|
70
70
|
return;
|
|
71
|
-
throw new CliError("
|
|
71
|
+
throw new CliError("conflict", `task ${String(row.key)} changed since revision ${expected}; it is at ${String(row.revision)}: read it again`, { reason: "task.stale", revision: Number(row.revision) });
|
|
72
72
|
};
|
|
73
73
|
export const workTaskOperationsOver = (context) => {
|
|
74
74
|
const { database, now } = context;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.567.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",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"packageManager": "pnpm@11.28.8",
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
22
|
+
"constants.json",
|
|
22
23
|
"parity.json",
|
|
23
24
|
"skills",
|
|
24
25
|
"README.md",
|
|
@@ -166,6 +167,10 @@
|
|
|
166
167
|
"./oauth": {
|
|
167
168
|
"types": "./dist/oauth/index.d.ts",
|
|
168
169
|
"default": "./dist/oauth/index.js"
|
|
170
|
+
},
|
|
171
|
+
"./constants": {
|
|
172
|
+
"types": "./dist/constants/index.d.ts",
|
|
173
|
+
"default": "./dist/constants/index.js"
|
|
169
174
|
}
|
|
170
175
|
},
|
|
171
176
|
"scripts": {
|
|
@@ -273,7 +278,7 @@
|
|
|
273
278
|
},
|
|
274
279
|
"peerDependencies": {
|
|
275
280
|
"@napi-rs/canvas": ">=0.1.69 <2",
|
|
276
|
-
"@wirecat/cli-base": ">=0.
|
|
281
|
+
"@wirecat/cli-base": ">=0.76.0 <0.78.0",
|
|
277
282
|
"commander": ">=15"
|
|
278
283
|
},
|
|
279
284
|
"bin": {
|