@wirecat/cli-core 0.564.0 → 0.566.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 +76 -0
- package/constants.json +13 -0
- package/dist/calendar/busy.d.ts +10 -10
- package/dist/calendar/busy.js +9 -9
- package/dist/calendar/cli/answers.d.ts +7 -8
- package/dist/calendar/cli/answers.js +4 -5
- package/dist/calendar/cli/bodies.d.ts +33 -0
- package/dist/calendar/cli/bodies.js +52 -0
- package/dist/calendar/cli/files.d.ts +8 -0
- package/dist/calendar/cli/files.js +41 -0
- package/dist/calendar/cli/index.d.ts +58 -51
- package/dist/calendar/cli/index.js +311 -284
- package/dist/calendar/cli/remind.d.ts +2 -11
- package/dist/calendar/cli/remind.js +81 -81
- package/dist/calendar/cli/search-all.d.ts +3 -8
- package/dist/calendar/cli/search-all.js +47 -42
- package/dist/calendar/mcp/index.d.ts +13 -13
- package/dist/calendar/prepare.d.ts +1 -1
- package/dist/calendar/testing/answers.d.ts +27 -5
- package/dist/calendar/testing/answers.js +88 -17
- package/dist/calendar/testing/writer.d.ts +1 -0
- package/dist/calendar/testing/writer.js +6 -0
- package/dist/calendar/writer.d.ts +6 -1
- package/dist/calendar/writes.d.ts +3 -1
- package/dist/calendar/writes.js +42 -20
- package/dist/cli/app-doctor-command.d.ts +7 -13
- package/dist/cli/app-doctor-command.js +1 -1
- package/dist/cli/config-command.js +1 -1
- package/dist/cli/config-issues.d.ts +3 -1
- package/dist/cli/config-issues.js +2 -2
- package/dist/cli/contract-evidence.d.ts +1 -1
- package/dist/cli/early-refusal.d.ts +2 -1
- package/dist/cli/early-refusal.js +4 -2
- package/dist/cli/entry.d.ts +6 -2
- package/dist/cli/entry.js +4 -1
- package/dist/cli/envelope.d.ts +2 -2
- 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/permission-keys.d.ts +3 -1
- package/dist/cli/permission-keys.js +2 -1
- package/dist/cli/preview.d.ts +12 -0
- package/dist/cli/preview.js +8 -1
- package/dist/cli/settings.d.ts +2 -0
- package/dist/cli/settings.js +8 -1
- 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/errors/reasons.js +15 -1
- package/dist/mcp/meeting-preparation.d.ts +1 -1
- package/dist/mcp/meeting-project-candidates.d.ts +1 -1
- package/dist/mcp/meeting-series-inventory.d.ts +1 -1
- package/dist/meetings/mcp/passage.d.ts +1 -1
- package/dist/meetings/mcp/project-evidence.d.ts +1 -1
- package/dist/meetings/mcp/search.d.ts +1 -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/services/metadata.d.ts +1 -1
- package/package.json +8 -3
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
1
2
|
import * as v from "valibot";
|
|
2
3
|
import { answerEntitySchema } from "../../cli/answer-entity.js";
|
|
3
|
-
import {
|
|
4
|
+
import { provide } from "../../cli/context.js";
|
|
5
|
+
import { addCalendarCommands } from "../cli/index.js";
|
|
4
6
|
const commandAt = (root, argv) => {
|
|
5
7
|
let at = root;
|
|
6
8
|
for (const word of argv) {
|
|
@@ -11,28 +13,97 @@ const commandAt = (root, argv) => {
|
|
|
11
13
|
}
|
|
12
14
|
return at;
|
|
13
15
|
};
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
* snake_case, a list's items one by one, a search's hits each with the search's `untrusted`, and no `next`. A `meta`
|
|
17
|
-
* goes into the host's envelope, never into the item.
|
|
18
|
-
*/
|
|
19
|
-
export const checkCalendarAnswer = (root, argv, value) => {
|
|
16
|
+
/** Each item of a printed calendar answer, parsed against the command's declared entity. */
|
|
17
|
+
export const checkCalendarAnswer = (root, argv, envelope) => {
|
|
20
18
|
const command = commandAt(root, argv);
|
|
21
19
|
const schema = answerEntitySchema(command);
|
|
22
20
|
if (!schema)
|
|
23
21
|
throw new Error(`${argv.join(" ")} declares no answer entity`);
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
?.filter((field) => field.startsWith("hits."))
|
|
27
|
-
.map((field) => field.slice("hits.".length));
|
|
28
|
-
const items = Array.isArray(body.items)
|
|
29
|
-
? body.items
|
|
30
|
-
: Array.isArray(body.hits)
|
|
31
|
-
? body.hits.map((hit) => ({ ...hit, untrusted: marker }))
|
|
32
|
-
: [body];
|
|
33
|
-
for (const item of items) {
|
|
22
|
+
const body = envelope;
|
|
23
|
+
for (const item of body.items ?? [body.item]) {
|
|
34
24
|
const parsed = v.safeParse(schema, item);
|
|
35
25
|
if (!parsed.success)
|
|
36
26
|
throw new Error(`${argv.join(" ")}: ${parsed.issues.map((issue) => `${v.getDotPath(issue) ?? ""} ${issue.message}`).join("; ")}`);
|
|
37
27
|
}
|
|
38
28
|
};
|
|
29
|
+
export const CALENDAR_TEST_APP = {
|
|
30
|
+
command: "gcal",
|
|
31
|
+
appName: "gcal-cli",
|
|
32
|
+
envPrefix: "GCAL",
|
|
33
|
+
description: "synthetic",
|
|
34
|
+
version: "1.0.0",
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The shared calendar commands on a bare program over `run`, as an app mounts them, printing to captured streams. A
|
|
38
|
+
* test of the commands themselves; an app's own tests go through its `run()`.
|
|
39
|
+
*/
|
|
40
|
+
export const calendarTestProgram = (run, options = {}) => {
|
|
41
|
+
const stderr = [];
|
|
42
|
+
const opened = [];
|
|
43
|
+
let data = "";
|
|
44
|
+
const build = (tty = false) => {
|
|
45
|
+
const root = new Command("gcal")
|
|
46
|
+
.exitOverride()
|
|
47
|
+
.configureOutput({ writeErr: () => undefined, writeOut: () => undefined })
|
|
48
|
+
.option("--json")
|
|
49
|
+
.option("--jsonl")
|
|
50
|
+
.option("--offline")
|
|
51
|
+
.option("--full")
|
|
52
|
+
.option("--timeout <duration>")
|
|
53
|
+
.option("--yes")
|
|
54
|
+
.option("--dry-run");
|
|
55
|
+
addCalendarCommands(root, {
|
|
56
|
+
app: CALENDAR_TEST_APP,
|
|
57
|
+
open: async (_command, access) => {
|
|
58
|
+
opened.push(access);
|
|
59
|
+
return run;
|
|
60
|
+
},
|
|
61
|
+
now: () => Date.now(),
|
|
62
|
+
timezone: "UTC",
|
|
63
|
+
// A help example names a file; no test writes into the working folder.
|
|
64
|
+
readFile: async () => "",
|
|
65
|
+
writeFile: async () => undefined,
|
|
66
|
+
...options,
|
|
67
|
+
});
|
|
68
|
+
provide(root, {
|
|
69
|
+
streams: {
|
|
70
|
+
data: (text) => {
|
|
71
|
+
data += text;
|
|
72
|
+
},
|
|
73
|
+
diagnostic: (text) => {
|
|
74
|
+
stderr.push(text);
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
tty,
|
|
78
|
+
});
|
|
79
|
+
return root;
|
|
80
|
+
};
|
|
81
|
+
const root = build();
|
|
82
|
+
const cli = async (...args) => {
|
|
83
|
+
const fresh = build();
|
|
84
|
+
data = "";
|
|
85
|
+
const dashes = args.indexOf("--");
|
|
86
|
+
const argv = args.includes("--json")
|
|
87
|
+
? args
|
|
88
|
+
: dashes < 0
|
|
89
|
+
? [...args, "--json"]
|
|
90
|
+
: [...args.slice(0, dashes), "--json", ...args.slice(dashes)];
|
|
91
|
+
await fresh.parseAsync(argv, { from: "user" });
|
|
92
|
+
if (!data.trim())
|
|
93
|
+
throw new Error(`${args.join(" ")} printed nothing`);
|
|
94
|
+
const envelope = JSON.parse(data);
|
|
95
|
+
// A --dry-run preview answers what would happen, not the command's entity.
|
|
96
|
+
if (!args.includes("--dry-run"))
|
|
97
|
+
checkCalendarAnswer(fresh, args, envelope);
|
|
98
|
+
// The run's own facts differ every run; a test compares the rest.
|
|
99
|
+
const { item, meta, ...rest } = envelope;
|
|
100
|
+
const { run_id: _run, duration_ms: _duration, ...facts } = meta;
|
|
101
|
+
return { ...(item ?? {}), ...rest, meta: facts };
|
|
102
|
+
};
|
|
103
|
+
const text = async (...args) => {
|
|
104
|
+
data = "";
|
|
105
|
+
await build(true).parseAsync(args, { from: "user" });
|
|
106
|
+
return data;
|
|
107
|
+
};
|
|
108
|
+
return { root, cli, text, stderr, opened };
|
|
109
|
+
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { calendarConflict } from "../writer.js";
|
|
3
3
|
export const fakeCalendarWriter = (options = {}) => {
|
|
4
|
+
const now = options.now ?? Date.now;
|
|
4
5
|
const calendars = new Map();
|
|
6
|
+
const made = new Map();
|
|
5
7
|
const calls = [];
|
|
6
8
|
let version = 0;
|
|
7
9
|
let lose = false;
|
|
@@ -89,8 +91,12 @@ export const fakeCalendarWriter = (options = {}) => {
|
|
|
89
91
|
participants: [],
|
|
90
92
|
});
|
|
91
93
|
of(calendar.externalId).set(id, entry);
|
|
94
|
+
made.set(id, now());
|
|
92
95
|
return answer(entry);
|
|
93
96
|
},
|
|
97
|
+
async quickAdded(calendar, since) {
|
|
98
|
+
return [...of(calendar.externalId).values()].filter((one) => (made.get(one.externalId) ?? -1) >= since);
|
|
99
|
+
},
|
|
94
100
|
/** Keeps one event per UID on a calendar: a UID sent again replaces the event under the same id. */
|
|
95
101
|
async importEvent(calendar, event) {
|
|
96
102
|
const held = [...of(calendar.externalId).values()].find((one) => one.icalUid === event.uid);
|
|
@@ -67,11 +67,16 @@ export interface CalendarWriter {
|
|
|
67
67
|
}): Promise<EntryInput>;
|
|
68
68
|
/**
|
|
69
69
|
* Optional: the provider makes an event from one line of text, reading its date, time and place its own way. The
|
|
70
|
-
* provider picks the id, so a retry after `outcome_unknown`
|
|
70
|
+
* provider picks the id, so a retry after `outcome_unknown` first asks `quickAdded`.
|
|
71
71
|
*/
|
|
72
72
|
quickAdd?(calendar: StoredCalendar, text: string, options: {
|
|
73
73
|
notify: boolean;
|
|
74
74
|
}): Promise<EntryInput>;
|
|
75
|
+
/**
|
|
76
|
+
* With `quickAdd`: the events this account made on `calendar` at or after `since` (epoch ms), so a retry of a quick
|
|
77
|
+
* add whose answer was lost finds the event instead of making a second one.
|
|
78
|
+
*/
|
|
79
|
+
quickAdded?(calendar: StoredCalendar, since: number): Promise<EntryInput[]>;
|
|
75
80
|
/**
|
|
76
81
|
* Optional: adds a private copy of an event read from an iCalendar file, under its own UID, as Google's
|
|
77
82
|
* `events.import`. The provider mails nobody. Sending the same UID again goes to the provider again; what it does
|
|
@@ -86,10 +86,12 @@ export declare const createEvent: (context: WriteContext, calendarReference: str
|
|
|
86
86
|
}>;
|
|
87
87
|
/**
|
|
88
88
|
* An event the provider makes from one line of text, as Google's quickAdd. Nobody is mailed unless `notify` says so:
|
|
89
|
-
* the attendees are known only once the provider has read the text.
|
|
89
|
+
* the attendees are known only once the provider has read the text. The provider picks the event's id, so a retry
|
|
90
|
+
* with the `id` a lost attempt named first looks for the event that attempt made.
|
|
90
91
|
*/
|
|
91
92
|
export declare const createEventFromText: (context: WriteContext, calendarReference: string, text: string, options?: {
|
|
92
93
|
notify?: boolean;
|
|
94
|
+
id?: string;
|
|
93
95
|
}) => Promise<{
|
|
94
96
|
operation_id: string;
|
|
95
97
|
event: string | null;
|
package/dist/calendar/writes.js
CHANGED
|
@@ -165,9 +165,38 @@ export const createEvent = async (context, calendarReference, fields, options =
|
|
|
165
165
|
};
|
|
166
166
|
/** The longest line `createEventFromText` sends; a quick add is one sentence, not a description. */
|
|
167
167
|
const TEXT_MAX = 1000;
|
|
168
|
+
/** How far the provider's clock may run behind this computer's when a retry looks for what a lost quick add made. */
|
|
169
|
+
const CLOCK_SKEW_MS = 120_000;
|
|
170
|
+
const TIME_DIGITS = 9;
|
|
171
|
+
/**
|
|
172
|
+
* A quick add's id: `q`, which no hex create id has, then when it was sent, so a retry knows from when to look for the
|
|
173
|
+
* event it may have made.
|
|
174
|
+
*/
|
|
175
|
+
const quickId = (at) => `q${at.toString(32).padStart(TIME_DIGITS, "0")}${newEventId()}`;
|
|
176
|
+
const QUICK_ID = new RegExp(`^q[a-v0-9]{${TIME_DIGITS}}[a-f0-9]{32}$`, "u");
|
|
177
|
+
const sentAt = (id) => Number.parseInt(id.slice(1, 1 + TIME_DIGITS), 32);
|
|
178
|
+
const wordsOf = (text) => text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
179
|
+
/**
|
|
180
|
+
* What a lost quick add made: an event this account made on the calendar since the attempt, whose title's every word
|
|
181
|
+
* is in the line. The provider builds the title from the line's own words.
|
|
182
|
+
*/
|
|
183
|
+
const lostQuickAdd = async (context, calendar, line, id) => {
|
|
184
|
+
const quickAdded = context.writes.writer.quickAdded;
|
|
185
|
+
if (!quickAdded)
|
|
186
|
+
throw new CliError("validation_error", `this provider cannot find a lost quick add; leave out ${context.names.id}`, {
|
|
187
|
+
param: context.names.id,
|
|
188
|
+
});
|
|
189
|
+
const words = new Set(wordsOf(line));
|
|
190
|
+
const found = (await quickAdded.call(context.writes.writer, calendar, sentAt(id) - CLOCK_SKEW_MS)).filter((entry) => entry.status !== "cancelled" && wordsOf(entry.title ?? "").every((word) => words.has(word)));
|
|
191
|
+
if (found.length > 1)
|
|
192
|
+
throw new CliError("validation_error", `${found.length} events made since that attempt could be the one it added; look with events list ` +
|
|
193
|
+
"changed_at>=today and add it again only if none is", { reason: "event.quick-add-ambiguous", operation_id: id, retryable: false });
|
|
194
|
+
return found[0];
|
|
195
|
+
};
|
|
168
196
|
/**
|
|
169
197
|
* An event the provider makes from one line of text, as Google's quickAdd. Nobody is mailed unless `notify` says so:
|
|
170
|
-
* the attendees are known only once the provider has read the text.
|
|
198
|
+
* the attendees are known only once the provider has read the text. The provider picks the event's id, so a retry
|
|
199
|
+
* with the `id` a lost attempt named first looks for the event that attempt made.
|
|
171
200
|
*/
|
|
172
201
|
export const createEventFromText = async (context, calendarReference, text, options = {}) => {
|
|
173
202
|
const quickAdd = context.writes.writer.quickAdd;
|
|
@@ -178,29 +207,22 @@ export const createEventFromText = async (context, calendarReference, text, opti
|
|
|
178
207
|
throw new CliError("validation_error", "the text is empty: say what, when and where");
|
|
179
208
|
if (line.length > TEXT_MAX || /[\r\n]/u.test(line))
|
|
180
209
|
throw new CliError("validation_error", `the text is one line of at most ${TEXT_MAX} characters`);
|
|
210
|
+
if (options.id !== undefined && !QUICK_ID.test(options.id))
|
|
211
|
+
throw new CliError("validation_error", `${context.names.id} is the id a quick add's outcome_unknown named`, {
|
|
212
|
+
param: context.names.id,
|
|
213
|
+
});
|
|
181
214
|
const calendar = writable(await calendarIn(context.store, context.accountId, calendarReference));
|
|
215
|
+
const lost = options.id === undefined ? undefined : await lostQuickAdd(context, calendar, line, options.id);
|
|
182
216
|
const notify = options.notify ?? false;
|
|
183
|
-
const
|
|
184
|
-
const id = newEventId();
|
|
217
|
+
const id = lost ? options.id : quickId(context.now);
|
|
185
218
|
let entry;
|
|
186
|
-
|
|
187
|
-
entry =
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
throw new CliError("outcome_unknown", `${error.message} — the provider may have made the event, and a create from text cannot be retried safely: ` +
|
|
192
|
-
"sync, then look with events list changed_at>=today before adding it again", { ...error.details, operation_id: id, retryable: false });
|
|
193
|
-
throw error;
|
|
194
|
-
}
|
|
195
|
-
let saved;
|
|
196
|
-
try {
|
|
197
|
-
;
|
|
198
|
-
[saved] = await context.store.saveEntries(calendar.id, [entry], context.now);
|
|
199
|
-
}
|
|
200
|
-
catch (error) {
|
|
201
|
-
throw new CliError(isCliError(error) ? error.code : "invalid_response", `the provider made event ${entry.externalId}, but the local store did not save it ` +
|
|
202
|
-
`(${error instanceof Error ? error.message : String(error)}); sync to store it — do not create it again`, { ...(isCliError(error) ? error.details : {}), operation_id: id, id: entry.externalId, retryable: false });
|
|
219
|
+
if (lost)
|
|
220
|
+
entry = lost;
|
|
221
|
+
else {
|
|
222
|
+
const guard = writeGuard(context, keysFor(context, "quick-add", notify));
|
|
223
|
+
entry = await retryable(context, id, () => guardedWrite(guard, { chatId: null, kind: "edit", key: COMMAND_KEY["quick-add"], operationId: id, sendId: id, notify }, () => quickAdd.call(context.writes.writer, calendar, line, { notify })));
|
|
203
224
|
}
|
|
225
|
+
const [saved] = await savedAfterCreate(context, id, () => context.store.saveEntries(calendar.id, [entry], context.now));
|
|
204
226
|
return answerOf(id, saved, (entry.recurrence?.length ?? 0) > 0);
|
|
205
227
|
};
|
|
206
228
|
/** The most events one import sends: a file of more is refused before anything goes. */
|
|
@@ -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,
|
|
@@ -56,7 +56,7 @@ export const configCommand = (app, config, { permissionKey = config.permissions,
|
|
|
56
56
|
...(declared ? { default: declared.default, description: declared.description } : {}),
|
|
57
57
|
};
|
|
58
58
|
}),
|
|
59
|
-
store_settings: await storeSettings(env),
|
|
59
|
+
store_settings: (await storeSettings(env)).filter(({ setting }) => offersStoreSetting(app, setting)),
|
|
60
60
|
}));
|
|
61
61
|
for (const issue of [
|
|
62
62
|
...(settings.configIssues ?? []),
|
|
@@ -76,11 +76,13 @@ export declare const issueMessage: (issues: readonly ConfigIssue[], doctor?: str
|
|
|
76
76
|
* `knownPermissions` (from `knownPermissionKeys`), each saved permission key outside it is an issue too, in every
|
|
77
77
|
* section.
|
|
78
78
|
*/
|
|
79
|
-
export declare const checkConfigText: <T extends v.GenericSchema>(text: string, { file, tool, schema, knownPermissions, }: {
|
|
79
|
+
export declare const checkConfigText: <T extends v.GenericSchema>(text: string, { file, tool, schema, knownPermissions, repair, }: {
|
|
80
80
|
file: string;
|
|
81
81
|
tool: string;
|
|
82
82
|
schema: T;
|
|
83
83
|
knownPermissions?: ReadonlySet<string>;
|
|
84
|
+
/** `false` for a CLI with its own `config` that has no `repair`: no fix then names one. */
|
|
85
|
+
repair?: boolean;
|
|
84
86
|
}) => {
|
|
85
87
|
issues: ConfigIssue[];
|
|
86
88
|
config: v.InferOutput<T> | undefined;
|
|
@@ -317,12 +317,12 @@ export const issueMessage = (issues, doctor = "doctor") => {
|
|
|
317
317
|
* `knownPermissions` (from `knownPermissionKeys`), each saved permission key outside it is an issue too, in every
|
|
318
318
|
* section.
|
|
319
319
|
*/
|
|
320
|
-
export const checkConfigText = (text, { file, tool, schema, knownPermissions, }) => {
|
|
320
|
+
export const checkConfigText = (text, { file, tool, schema, knownPermissions, repair = true, }) => {
|
|
321
321
|
const parsed = parseConfigText(text, file);
|
|
322
322
|
if ("issue" in parsed) {
|
|
323
323
|
const issue = publicIssue(parsed.issue);
|
|
324
324
|
// Offered only where it would work, so the desktop never shows a Repair button that fails.
|
|
325
|
-
if (repairConfigText(text).state === "repaired")
|
|
325
|
+
if (repair && repairConfigText(text).state === "repaired")
|
|
326
326
|
issue.fix = `${tool} config repair`;
|
|
327
327
|
return { issues: [issue], config: undefined };
|
|
328
328
|
}
|
|
@@ -15,7 +15,7 @@ export declare const contractEvidence: (root: Command, { fields, errors }?: Cont
|
|
|
15
15
|
field_only: string[];
|
|
16
16
|
reasons: readonly {
|
|
17
17
|
reason: string;
|
|
18
|
-
code: "authentication_error" | "cancelled" | "configuration_error" | "confirmation_required" | "internal_error" | "invalid_response" | "network_error" | "not_found" | "outcome_unknown" | "permission_error" | "provider_error" | "provider_unavailable" | "rate_limited" | "timeout" | "validation_error";
|
|
18
|
+
code: "authentication_error" | "cancelled" | "configuration_error" | "confirmation_required" | "conflict" | "internal_error" | "invalid_response" | "network_error" | "not_found" | "outcome_unknown" | "permission_error" | "provider_error" | "provider_unavailable" | "rate_limited" | "timeout" | "validation_error";
|
|
19
19
|
message: string;
|
|
20
20
|
details: string[];
|
|
21
21
|
step?: string | undefined;
|
|
@@ -22,7 +22,8 @@ export declare const refuseWriteBeforeConnecting: (action: Command, { app, env,
|
|
|
22
22
|
* A command of a CLI that declares its own keys (`commandPermissions`), refused by its key before anything connects:
|
|
23
23
|
* `deny` stops it, `readonly` stops a write, and the read-only lock stops a write to the service. `ask` is left to
|
|
24
24
|
* the write guard, which can put the question. A saved or `--permission` key that names no command stops every keyed
|
|
25
|
-
* command; housekeeping still runs, so `config unset` can remove it.
|
|
25
|
+
* command; housekeeping still runs, so `config unset` can remove it. Under the lock a non-messenger CLI still previews
|
|
26
|
+
* a `--dry-run`, which sends nothing (WC-PERM-12); a messenger's preview stays refused (SEC-65).
|
|
26
27
|
*/
|
|
27
28
|
export declare const refuseByCommandPermissions: (action: Command, permissions: CommandPermissions, { app, env, dryRun, resolve }: {
|
|
28
29
|
app: AppIdentity;
|
|
@@ -79,7 +79,8 @@ export const refuseWriteBeforeConnecting = (action, { app, env, dryRun, resolve
|
|
|
79
79
|
* A command of a CLI that declares its own keys (`commandPermissions`), refused by its key before anything connects:
|
|
80
80
|
* `deny` stops it, `readonly` stops a write, and the read-only lock stops a write to the service. `ask` is left to
|
|
81
81
|
* the write guard, which can put the question. A saved or `--permission` key that names no command stops every keyed
|
|
82
|
-
* command; housekeeping still runs, so `config unset` can remove it.
|
|
82
|
+
* command; housekeeping still runs, so `config unset` can remove it. Under the lock a non-messenger CLI still previews
|
|
83
|
+
* a `--dry-run`, which sends nothing (WC-PERM-12); a messenger's preview stays refused (SEC-65).
|
|
83
84
|
*/
|
|
84
85
|
export const refuseByCommandPermissions = (action, permissions, { app, env, dryRun, resolve }) => {
|
|
85
86
|
const key = permissions.keyOf(commandPathOf(action));
|
|
@@ -87,7 +88,8 @@ export const refuseByCommandPermissions = (action, permissions, { app, env, dryR
|
|
|
87
88
|
return;
|
|
88
89
|
const { effects } = metaOf(action);
|
|
89
90
|
const lock = readOnlyLockOf(app, env);
|
|
90
|
-
|
|
91
|
+
const preview = dryRun && app.messenger === false;
|
|
92
|
+
if (lock && effects?.remote === "write" && !preview)
|
|
91
93
|
throw readOnlyRefusal(lock, `run ${commandPathOf(action).join(" ")}`);
|
|
92
94
|
const flags = action.optsWithGlobals();
|
|
93
95
|
const overrides = environmentOf(action).agent?.overrides ?? [];
|
package/dist/cli/entry.d.ts
CHANGED
|
@@ -16,8 +16,12 @@ export interface EntryContext {
|
|
|
16
16
|
atStart: (command: Command) => Promise<void>;
|
|
17
17
|
/** A yes/no question on stderr; false without asking when there is no terminal, under CI, `--json`, `--jsonl` or `--no-input`. */
|
|
18
18
|
ask: (question: string, command?: Command) => Promise<boolean>;
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Opens the URL in the system browser, or in `browser` when given; never waits for the browser to close. Does nothing
|
|
21
|
+
* where nobody can use a browser (WC-CMD-15): under CI, `--json`, `--jsonl`, `--no-input`, or with no terminal.
|
|
22
|
+
* The caller prints the URL either way.
|
|
23
|
+
*/
|
|
24
|
+
openBrowser: (url: string, browser?: string, command?: Command) => Promise<void>;
|
|
21
25
|
/** True once a reader closed the output (`| head`): nothing more reaches the terminal. */
|
|
22
26
|
outputClosed: () => boolean;
|
|
23
27
|
}
|
package/dist/cli/entry.js
CHANGED
|
@@ -88,7 +88,10 @@ export const startCli = async (entry) => {
|
|
|
88
88
|
});
|
|
89
89
|
return /^(y|yes)$/i.test(answer.trim());
|
|
90
90
|
},
|
|
91
|
-
openBrowser: (url, browser) =>
|
|
91
|
+
openBrowser: async (url, browser, command) => {
|
|
92
|
+
if (!headless(command))
|
|
93
|
+
await openBrowser(url, browser, io);
|
|
94
|
+
},
|
|
92
95
|
outputClosed: () => closed,
|
|
93
96
|
};
|
|
94
97
|
let code;
|
package/dist/cli/envelope.d.ts
CHANGED
|
@@ -89,7 +89,7 @@ export declare const answerSchema: (item?: v.GenericSchema) => v.UnionSchema<[v.
|
|
|
89
89
|
readonly accounts: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>, undefined>;
|
|
90
90
|
readonly complete: v.BooleanSchema<undefined>;
|
|
91
91
|
readonly issues: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
92
|
-
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled"], undefined>;
|
|
92
|
+
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled"], undefined>;
|
|
93
93
|
readonly reason: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>, undefined>;
|
|
94
94
|
readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
|
|
95
95
|
}, undefined>, undefined>, undefined>;
|
|
@@ -144,7 +144,7 @@ export declare const answerSchema: (item?: v.GenericSchema) => v.UnionSchema<[v.
|
|
|
144
144
|
readonly accounts: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>, undefined>;
|
|
145
145
|
readonly complete: v.BooleanSchema<undefined>;
|
|
146
146
|
readonly issues: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
147
|
-
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled"], undefined>;
|
|
147
|
+
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled"], undefined>;
|
|
148
148
|
readonly reason: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>, undefined>;
|
|
149
149
|
readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
|
|
150
150
|
}, undefined>, undefined>, undefined>;
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ export { type AgentPolicy, type AgentTask, type EvaluationResult, evaluateAgent,
|
|
|
45
45
|
export { answerEntity } from "./answer-entity.js";
|
|
46
46
|
export { type AppDoctorOptions, appDoctorReport } from "./app-doctor-command.js";
|
|
47
47
|
export type { AppMcpOptions } from "./app-mcp-command.js";
|
|
48
|
+
export { actorOf } from "./author.js";
|
|
48
49
|
export type { ApiCommandInput, ApiCommands } from "./bot/api.js";
|
|
49
50
|
export { apiFlagOf, apiJson, apiOptionKey, apiPlainJson, checkApiBody, checkApiParameter, parseApiJson, readApiBody, } from "./bot/api-input.js";
|
|
50
51
|
export { prepareRpcApiBody, type RpcApiBody } from "./bot/api-rpc.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -39,6 +39,7 @@ export { liftOptionsAfterDashes } from "./after-dashes.js";
|
|
|
39
39
|
export { evaluateAgent, } from "./agent-evaluation.js";
|
|
40
40
|
export { answerEntity } from "./answer-entity.js";
|
|
41
41
|
export { appDoctorReport } from "./app-doctor-command.js";
|
|
42
|
+
export { actorOf } from "./author.js";
|
|
42
43
|
export { apiFlagOf, apiJson, apiOptionKey, apiPlainJson, checkApiBody, checkApiParameter, parseApiJson, readApiBody, } from "./bot/api-input.js";
|
|
43
44
|
export { prepareRpcApiBody } from "./bot/api-rpc.js";
|
|
44
45
|
export { botContext, botWords } from "./bot/context.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;
|
|
@@ -14,12 +14,14 @@ export interface CommandPermissions {
|
|
|
14
14
|
extra: readonly PermissionKey[];
|
|
15
15
|
/** cli-core's built-in levels and the CLI's own; they only ever tighten. */
|
|
16
16
|
defaults: Readonly<Record<PermissionKey, Level>>;
|
|
17
|
+
/** The other keys a command's real run checks, so its `--dry-run` preview shows each: gcal's `api.calendar.*`. */
|
|
18
|
+
alsoChecks?: (path: readonly string[]) => readonly PermissionKey[];
|
|
17
19
|
}
|
|
18
20
|
/** Either way of naming a program's keys: a messenger's per-command mapping, or a declared CLI's own. */
|
|
19
21
|
export type PermissionKeySource = PermissionKeyOf | CommandPermissions;
|
|
20
22
|
/** A command's own path is its key; the commands that look after the tool have none. */
|
|
21
23
|
export declare const commandPathKey: (path: readonly string[]) => PermissionKey | null;
|
|
22
|
-
export declare const commandPermissions: ({ keyOf, extra, defaults, }?: Partial<CommandPermissions>) => CommandPermissions;
|
|
24
|
+
export declare const commandPermissions: ({ keyOf, extra, defaults, alsoChecks, }?: Partial<CommandPermissions>) => CommandPermissions;
|
|
23
25
|
/**
|
|
24
26
|
* The keys `permissions` may name: every command of the program, every write key, and each of their
|
|
25
27
|
* parents. A key outside it is a typo that would otherwise do nothing (BUG-137).
|
|
@@ -16,10 +16,11 @@ const HOUSEKEEPING = new Set([
|
|
|
16
16
|
]);
|
|
17
17
|
/** A command's own path is its key; the commands that look after the tool have none. */
|
|
18
18
|
export const commandPathKey = (path) => path[0] === undefined || HOUSEKEEPING.has(path[0]) ? null : path.join(".");
|
|
19
|
-
export const commandPermissions = ({ keyOf = commandPathKey, extra = [], defaults = {}, } = {}) => ({
|
|
19
|
+
export const commandPermissions = ({ keyOf = commandPathKey, extra = [], defaults = {}, alsoChecks, } = {}) => ({
|
|
20
20
|
keyOf,
|
|
21
21
|
extra: [AUTHORITY_KEY, ...extra],
|
|
22
22
|
defaults: { ...DEFAULT_PERMISSIONS, ...defaults },
|
|
23
|
+
...(alsoChecks ? { alsoChecks } : {}),
|
|
23
24
|
});
|
|
24
25
|
const pathOf = (command) => {
|
|
25
26
|
const words = [];
|
package/dist/cli/preview.d.ts
CHANGED
|
@@ -10,6 +10,10 @@ export declare const preview: (command: Command, settings: Pick<Settings, "permi
|
|
|
10
10
|
permission?: {
|
|
11
11
|
key: PermissionKey | null;
|
|
12
12
|
level: string;
|
|
13
|
+
also?: {
|
|
14
|
+
key: PermissionKey;
|
|
15
|
+
level: string;
|
|
16
|
+
}[];
|
|
13
17
|
} | undefined;
|
|
14
18
|
validation: {
|
|
15
19
|
arguments: string;
|
|
@@ -42,12 +46,20 @@ export declare const preview: (command: Command, settings: Pick<Settings, "permi
|
|
|
42
46
|
export declare const previewBody: (command: Command, permission: {
|
|
43
47
|
key: PermissionKey | null;
|
|
44
48
|
level: string;
|
|
49
|
+
also?: {
|
|
50
|
+
key: PermissionKey;
|
|
51
|
+
level: string;
|
|
52
|
+
}[];
|
|
45
53
|
} | undefined, messenger: boolean) => {
|
|
46
54
|
preview: boolean;
|
|
47
55
|
path: string[];
|
|
48
56
|
permission?: {
|
|
49
57
|
key: PermissionKey | null;
|
|
50
58
|
level: string;
|
|
59
|
+
also?: {
|
|
60
|
+
key: PermissionKey;
|
|
61
|
+
level: string;
|
|
62
|
+
}[];
|
|
51
63
|
} | undefined;
|
|
52
64
|
validation: {
|
|
53
65
|
arguments: string;
|
package/dist/cli/preview.js
CHANGED
|
@@ -23,7 +23,14 @@ export const preview = (command, settings, tool, keys, messenger = true) => {
|
|
|
23
23
|
(!declared && key !== null && key !== undefined && WRITE_KEYS.includes(key));
|
|
24
24
|
if (level === "deny" || (writes && level === "readonly"))
|
|
25
25
|
throw new CliError("permission_error", `this profile does not permit ${path.join(" ")}`, { permission: key });
|
|
26
|
-
|
|
26
|
+
const also = (declared?.alsoChecks?.(path) ?? []).map((one) => ({
|
|
27
|
+
key: one,
|
|
28
|
+
level: levelFor(permissions, one, declared?.defaults).level,
|
|
29
|
+
}));
|
|
30
|
+
const stopped = also.find((one) => one.level === "deny" || (writes && one.level === "readonly"));
|
|
31
|
+
if (stopped)
|
|
32
|
+
throw new CliError("permission_error", `this profile does not permit ${stopped.key}`, { permission: stopped.key });
|
|
33
|
+
return previewBody(command, { key: key ?? null, level, ...(also.length ? { also } : {}) }, messenger);
|
|
27
34
|
};
|
|
28
35
|
const TARGETS = new Set(["chat", "message", "person", "profile"]);
|
|
29
36
|
/**
|
package/dist/cli/settings.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ export interface SettingsExtension<T extends object = Record<never, never>> {
|
|
|
42
42
|
* `--permission` then take any command path, which the program checks against its commands.
|
|
43
43
|
*/
|
|
44
44
|
permissions?: CommandPermissions;
|
|
45
|
+
/** `false` for a CLI with its own `config` that has no `repair`: no config issue then names one. */
|
|
46
|
+
configRepair?: boolean;
|
|
45
47
|
}
|
|
46
48
|
export interface SettingsExtensionContext {
|
|
47
49
|
flags: GlobalFlags;
|
package/dist/cli/settings.js
CHANGED
|
@@ -102,6 +102,8 @@ export const MESSENGER_ONLY_SETTINGS = [
|
|
|
102
102
|
"history_group_days",
|
|
103
103
|
"history_group_members_max",
|
|
104
104
|
"read_other_bots",
|
|
105
|
+
"speech_model",
|
|
106
|
+
"picture_search",
|
|
105
107
|
];
|
|
106
108
|
/** One program, one version: whether to look for a newer one is not a per-profile matter. */
|
|
107
109
|
const SHARED_DEFAULTS_ONLY = {
|
|
@@ -259,7 +261,12 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
259
261
|
return { config: { profiles: {} }, issues: [read.issue] };
|
|
260
262
|
if (read.text === undefined)
|
|
261
263
|
return { config: { profiles: {} }, issues: [] };
|
|
262
|
-
const { config, issues } = checkConfigText(read.text, {
|
|
264
|
+
const { config, issues } = checkConfigText(read.text, {
|
|
265
|
+
file: path,
|
|
266
|
+
tool: app.command,
|
|
267
|
+
schema: readSchema,
|
|
268
|
+
repair: extension.configRepair !== false,
|
|
269
|
+
});
|
|
263
270
|
return { config: (config ?? { profiles: {} }), issues };
|
|
264
271
|
};
|
|
265
272
|
/**
|