@wirecat/cli-core 0.530.0 → 0.531.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 +33 -0
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +1 -1
- package/dist/cli/list-query.d.ts +0 -4
- package/dist/cli/list-query.js +19 -1
- package/dist/cli/messenger/schedule-command.d.ts +2 -19
- package/dist/cli/messenger/schedule-command.js +2 -231
- package/dist/cli/messenger/topics-command.js +22 -5
- package/dist/cli/schedule/schedule-command.d.ts +19 -0
- package/dist/cli/schedule/schedule-command.js +261 -0
- package/dist/cli/shared-command-exports.d.ts +1 -1
- package/dist/cli/shared-command-exports.js +2 -1
- package/dist/domain/models.d.ts +2 -0
- package/dist/entities/index.d.ts +41 -2
- package/dist/entities/index.js +3 -2
- package/dist/entities/list-results.d.ts +3 -0
- package/dist/entities/list-results.js +1 -0
- package/dist/entities/task-results.d.ts +30 -0
- package/dist/entities/task-results.js +1 -0
- package/dist/entities/task.d.ts +7 -0
- package/dist/entities/task.js +2 -0
- package/dist/errors/reasons.js +1 -0
- package/dist/mail/account-key.d.ts +2 -0
- package/dist/mail/account-key.js +1 -0
- package/dist/mail/cli/index.js +10 -10
- package/dist/mail/index.d.ts +1 -0
- package/dist/mail/index.js +1 -0
- package/dist/mail/retry.js +2 -1
- package/dist/mail/rule-stored.js +2 -1
- package/dist/mail/sender-scores.js +2 -1
- package/dist/mail/sync.js +3 -2
- package/dist/ports/messenger.d.ts +3 -0
- package/dist/services/tasks.d.ts +4 -2
- package/dist/services/tasks.js +2 -2
- package/dist/services/topics.d.ts +3 -0
- package/dist/services/topics.js +20 -6
- package/dist/store/index.d.ts +1 -0
- package/dist/store/sqlite/task-board.js +3 -1
- package/dist/store/sqlite/task-comments.d.ts +23 -0
- package/dist/store/sqlite/task-comments.js +41 -0
- package/dist/store/sqlite/things.js +8 -1
- package/dist/store/sqlite/work-tasks.d.ts +13 -2
- package/dist/store/sqlite/work-tasks.js +52 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,39 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.531.0 — 01.11.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`topics create --emoji` and `topics edit --emoji`: a forum topic's icon.** The value is an emoji from the
|
|
15
|
+
messenger's topic icon set, which `topics icons list` shows; `topics edit --emoji none` removes the icon. An emoji
|
|
16
|
+
change is journaled as `topic-edit`. Adapters take it as `createTopic(…, { icon })` and `TopicChange.icon`
|
|
17
|
+
(`null` removes it), and list the set with the optional `TopicEditing.topicIcons()`; the `topic_icon` entity is the
|
|
18
|
+
list's item.
|
|
19
|
+
- **`tasks.edit(id, { project })` moves a project task to another of the owner's projects.** It takes the next key
|
|
20
|
+
there; `id`, `ref` and `key` change, and the old key still finds the task through a `project` event in its
|
|
21
|
+
history. The source project's numbers are never reused. A subtask leaves its parent; a
|
|
22
|
+
task with subtasks is refused (`task.has-subtasks`, with their keys). The revision goes up.
|
|
23
|
+
- **`tasks.edit(id, { type })` changes a project task's kind** among bug, feature and chore, as a `type` event, and
|
|
24
|
+
bumps the revision.
|
|
25
|
+
- **A task names who added it**: `TaskDetails.createdBy` (`created_by` in the `task` and `task_card` entities),
|
|
26
|
+
`{ type, id, name, owner }`. `tasksService.add` takes an `author`, so a task found in a message by a named agent is
|
|
27
|
+
that agent's, not `agent`'s.
|
|
28
|
+
- **Task comments**: `tasks.comment(id, text, by)` keeps a note about the task in its author's name; `tasks.comments`
|
|
29
|
+
lists them oldest first with `author`, `text` and `at`. A comment leaves the revision as it is.
|
|
30
|
+
- **`tomorrow` in any time field of a list query** (`due:tomorrow`, `due:[today TO tomorrow]`), as the owner's next
|
|
31
|
+
calendar day: `listQuery` reads it before validating. `today` and `yesterday` already worked.
|
|
32
|
+
- **`mailAccountKey(address)`** from `./mail`: the store key of a mail account, `{ provider: "email", account }` with
|
|
33
|
+
the address in lower case. **`positiveCount(flag)`** from `./cli`: the parser of a whole-number flag from 1 up.
|
|
34
|
+
- **The shared `schedule` command lives in `src/cli/schedule/`**, not under the messenger commands. `scheduleCommandFor`
|
|
35
|
+
and `ScheduleHost` come from `./cli` as before; the old module path re-exports them. Each `schedule` command now
|
|
36
|
+
has examples and described arguments, and the example profile of the shared mail read commands is `personal`.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- **`schedule command` and `schedule agent` refuse a name that another profile already uses**, rather than replacing
|
|
41
|
+
its schedule and timer. Schedules of the same profile are still replaced. Existing timers keep their names.
|
|
42
|
+
|
|
10
43
|
## 0.530.0 — 01.11.2026
|
|
11
44
|
|
|
12
45
|
### Added
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -97,7 +97,6 @@ export { httpTokenFile, type McpEnvironment, type McpHost, serverEntry } from ".
|
|
|
97
97
|
export { type MessageItem, messageItem, type ReadMessage } from "./messenger/message-items.js";
|
|
98
98
|
export { type ModeProblem, type PrivateFiles, privateFiles, withSqliteSidecars } from "./messenger/private-files.js";
|
|
99
99
|
export { buildReport as buildDoctorReport, type Report as DoctorReport, type ReportContext as DoctorReportContext, reportFileName as doctorReportFileName, } from "./messenger/report.js";
|
|
100
|
-
export type { ScheduleHost } from "./messenger/schedule-command.js";
|
|
101
100
|
export { allAnswer, allHitItem, coverageGaps, type HitItem, type MessageHitItem, messageHitItem, type NoteHitItem, noteHitItem, shownOf, } from "./messenger/search-answers.js";
|
|
102
101
|
export { SEARCHES_QUERY } from "./messenger/searches-command.js";
|
|
103
102
|
export { servingProfiles } from "./messenger/serve-command.js";
|
|
@@ -108,12 +107,13 @@ export { storeSummary } from "./messenger/store-maintenance-command.js";
|
|
|
108
107
|
export { type Saving, stored } from "./messenger/stored.js";
|
|
109
108
|
export { dateOrNone } from "./messenger/tasks-command.js";
|
|
110
109
|
export { MODEL_CATALOGUE_QUERY, type ModelCatalogueOptions } from "./model-catalogue-command.js";
|
|
111
|
-
export { window, withPaging } from "./paging.js";
|
|
110
|
+
export { positiveCount, window, withPaging } from "./paging.js";
|
|
112
111
|
export { type CommandPermissions, commandPathKey, commandPermissions, knownBeside, knownPermissionKeys, type PermissionKeyOf, type PermissionKeySource, refuseUnknownPermissionKeys, unknownPermissionIssues, unknownPermissionKeys, } from "./permission-keys.js";
|
|
113
112
|
export { asFirstWord, commandWords, liftProfile, profileFree, refuseCommandName, rootOf, takesProfile, usableProfileName, } from "./profile.js";
|
|
114
113
|
export { createProgram, type ProgramDefinition, type ProgramOptions, type RunOptions, run } from "./program.js";
|
|
115
114
|
export { type ProjectTextHints, projectAnswerParts, projectCutoffTime, projectEvidenceText, projectWindowOf, withSyncCommands, } from "./project-evidence.js";
|
|
116
115
|
export { fieldsOf, projectFields } from "./result-fields.js";
|
|
116
|
+
export type { ScheduleHost } from "./schedule/schedule-command.js";
|
|
117
117
|
export { type Config, type Configuration, count, first, flag, fromFile, type GlobalFlags, NONE_NAMED, namedProfile, type ProfileKind, plain, type ResolveOptions, type Settings, type SettingsExtension, type SettingsExtensionContext, type Source, settingsFor, } from "./settings.js";
|
|
118
118
|
export { type PermissionAnswer, readSetupAnswers, type SetupAnswers, setupPermissionCeiling } from "./setup-answers.js";
|
|
119
119
|
export { AGENTS, type Agent, agentFor, screenFor } from "./setup-screen.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -97,7 +97,7 @@ export { storeSummary } from "./messenger/store-maintenance-command.js";
|
|
|
97
97
|
export { stored } from "./messenger/stored.js";
|
|
98
98
|
export { dateOrNone } from "./messenger/tasks-command.js";
|
|
99
99
|
export { MODEL_CATALOGUE_QUERY } from "./model-catalogue-command.js";
|
|
100
|
-
export { window, withPaging } from "./paging.js";
|
|
100
|
+
export { positiveCount, window, withPaging } from "./paging.js";
|
|
101
101
|
export { commandPathKey, commandPermissions, knownBeside, knownPermissionKeys, refuseUnknownPermissionKeys, unknownPermissionIssues, unknownPermissionKeys, } from "./permission-keys.js";
|
|
102
102
|
export { asFirstWord, commandWords, liftProfile, profileFree, refuseCommandName, rootOf, takesProfile, usableProfileName, } from "./profile.js";
|
|
103
103
|
export { createProgram, run } from "./program.js";
|
package/dist/cli/list-query.d.ts
CHANGED
|
@@ -16,10 +16,6 @@ export declare const withQuery: (command: Command, registry: QueryRegistry) => C
|
|
|
16
16
|
export declare const unfilteredQuery: (entity: string, schema: Parameters<typeof queryFieldsOf>[0]) => QueryRegistry;
|
|
17
17
|
/** `tasks list`, as a query error names the command. */
|
|
18
18
|
export declare const commandPath: (command: Command) => string;
|
|
19
|
-
/**
|
|
20
|
-
* The query as typed, validated against the list's fields, with each default whose field it does not name;
|
|
21
|
-
* `undefined` when nothing narrows the list. A bare word is refused unless the list names a `defaultField` for it. CLI and MCP read a list's query through this one function.
|
|
22
|
-
*/
|
|
23
19
|
export declare const listQuery: (text: string, registry: QueryRegistry, command: string, { defaultField }?: {
|
|
24
20
|
defaultField?: string;
|
|
25
21
|
}) => QueryAst | undefined;
|
package/dist/cli/list-query.js
CHANGED
|
@@ -39,9 +39,27 @@ export const commandPath = (command) => {
|
|
|
39
39
|
* The query as typed, validated against the list's fields, with each default whose field it does not name;
|
|
40
40
|
* `undefined` when nothing narrows the list. A bare word is refused unless the list names a `defaultField` for it. CLI and MCP read a list's query through this one function.
|
|
41
41
|
*/
|
|
42
|
+
/**
|
|
43
|
+
* `tomorrow` in a time field as the owner's next calendar date: the query language knows `today` and `yesterday`
|
|
44
|
+
* only, and a due date is more often ahead than behind.
|
|
45
|
+
*/
|
|
46
|
+
const withTomorrow = (query, registry, zone = timezoneOf(), now = Date.now()) => {
|
|
47
|
+
const times = new Set(registry.fields.filter(({ type }) => type === "time").map(({ name }) => name));
|
|
48
|
+
const date = new Date(`${new Intl.DateTimeFormat("en-CA", { timeZone: zone }).format(now)}T00:00:00Z`);
|
|
49
|
+
date.setUTCDate(date.getUTCDate() + 1);
|
|
50
|
+
const tomorrow = date.toISOString().slice(0, 10);
|
|
51
|
+
const day = (value) => (value.toLowerCase() === "tomorrow" ? tomorrow : value);
|
|
52
|
+
const map = (node) => node.kind === "predicate"
|
|
53
|
+
? times.has(node.field)
|
|
54
|
+
? { ...node, value: day(node.value), ...(node.upper === undefined ? {} : { upper: day(node.upper) }) }
|
|
55
|
+
: node
|
|
56
|
+
: { ...node, clauses: node.clauses.map((clause) => ({ ...clause, node: map(clause.node) })) };
|
|
57
|
+
return { ...query, root: map(query.root) };
|
|
58
|
+
};
|
|
42
59
|
export const listQuery = (text, registry, command, { defaultField = "none" } = {}) => {
|
|
43
60
|
const context = { command, guide: LIST_QUERY_GUIDE };
|
|
44
|
-
const
|
|
61
|
+
const parsed = text.trim() === "" ? undefined : parseLucene(text, { defaultField, ...context });
|
|
62
|
+
const typed = parsed && withTomorrow(parsed, registry);
|
|
45
63
|
if (typed)
|
|
46
64
|
validateQuery(typed, registry, context);
|
|
47
65
|
const named = typed ? fieldsNamed(typed) : new Set();
|
|
@@ -1,21 +1,4 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import { type AppIdentity } from "../../ports/app.js";
|
|
3
|
-
import { type AnswerOutput } from "../envelope.js";
|
|
1
|
+
import type { Command } from "commander";
|
|
4
2
|
import { type Messenger } from "./context.js";
|
|
5
|
-
|
|
6
|
-
export interface ScheduleHost {
|
|
7
|
-
app: AppIdentity;
|
|
8
|
-
context(command: Command, resolving?: {
|
|
9
|
-
needsProfile?: boolean;
|
|
10
|
-
}): AnswerOutput & {
|
|
11
|
-
env: NodeJS.ProcessEnv;
|
|
12
|
-
profile: string;
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Runs the CLI's own commands, or an agent, later or on a timer — through systemd (Linux), launchd
|
|
17
|
-
* (macOS) or Task Scheduler (Windows). Every timer runs `schedule exec <file>`; the schedule file holds
|
|
18
|
-
* the rest, so what runs and with which variables is the same on all three.
|
|
19
|
-
*/
|
|
20
|
-
export declare const scheduleCommandFor: ({ app, context: contextOf }: ScheduleHost) => Command;
|
|
3
|
+
export { type ScheduleHost, scheduleCommandFor } from "../schedule/schedule-command.js";
|
|
21
4
|
export declare const scheduleCommand: (messenger: Messenger) => Command;
|
|
@@ -1,235 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { CliError, resolvePaths, writeSecurely } from "@wirecat/cli-base";
|
|
4
|
-
import { annotate } from "@wirecat/cli-base/commands";
|
|
5
|
-
import { Argument, Command, Option } from "commander";
|
|
6
|
-
import { describeWhen, findProgram, lastRun, listSchedules, runSchedule, scheduleFile, scheduleName, timerFor, } from "../../background/schedules.js";
|
|
7
|
-
import { thisMachine } from "../../background/system.js";
|
|
8
|
-
import { locationVariables } from "../../background/units.js";
|
|
9
|
-
import { parseDuration } from "../../domain/duration.js";
|
|
10
|
-
import { readOnlyLockOf } from "../../ports/app.js";
|
|
11
|
-
import { aheadOf } from "../../services/moment.js";
|
|
12
|
-
import { storePath } from "../../store/path.js";
|
|
13
|
-
import { environmentOf, setExitCode } from "../context.js";
|
|
14
|
-
import { grantsAuthority, LOCAL_READ, LOCAL_WRITE, REMOTE_WRITE } from "../effects.js";
|
|
15
|
-
import { itemOf, listOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
16
|
-
import { withQuery } from "../list-query.js";
|
|
17
|
-
import { lineOf } from "../steps.js";
|
|
1
|
+
import { scheduleCommandFor } from "../schedule/schedule-command.js";
|
|
18
2
|
import { messengerContext } from "./context.js";
|
|
19
|
-
|
|
20
|
-
/** Each run of an agent is a paid model call that reads chats; keeping the store current is `serve`'s job. */
|
|
21
|
-
const AGENT_MINIMUM_MINUTES = 60;
|
|
22
|
-
const COMMAND_MINIMUM_MINUTES = 1;
|
|
23
|
-
const HARNESSES = ["claude", "codex"];
|
|
24
|
-
const whenOf = ({ every, at, atTime }, minimum) => {
|
|
25
|
-
if ([every ?? at, atTime].filter(Boolean).length !== 1) {
|
|
26
|
-
throw new CliError("validation_error", "say when: --every <30m|2h|1d>, --at HH:MM (daily), or --at-time <30m|2d|ISO time>");
|
|
27
|
-
}
|
|
28
|
-
if (atTime) {
|
|
29
|
-
const time = aheadOf(atTime, "--at-time");
|
|
30
|
-
if (time <= Date.now())
|
|
31
|
-
throw new CliError("validation_error", "--at-time is in the past");
|
|
32
|
-
return { once: new Date(time).toISOString() };
|
|
33
|
-
}
|
|
34
|
-
if (at) {
|
|
35
|
-
if (!/^([01]\d|2[0-3]):[0-5]\d$/.test(at))
|
|
36
|
-
throw new CliError("validation_error", `--at takes HH:MM, not "${at}"`);
|
|
37
|
-
if (every && parseDuration(every, "--every") !== 86_400_000) {
|
|
38
|
-
throw new CliError("validation_error", "--at runs once a day; leave out --every, or use --every 1d");
|
|
39
|
-
}
|
|
40
|
-
return { daily: at };
|
|
41
|
-
}
|
|
42
|
-
const minutes = parseDuration(every ?? "", "--every") / 60_000;
|
|
43
|
-
if (!Number.isInteger(minutes) || minutes < minimum) {
|
|
44
|
-
throw new CliError("validation_error", `--every is at least ${minimum} minute${minimum === 1 ? "" : "s"} here, in whole minutes`);
|
|
45
|
-
}
|
|
46
|
-
return { every: minutes };
|
|
47
|
-
};
|
|
48
|
-
/** A schedule as answered: snake_case, but its environment keeps the variables' own names. */
|
|
49
|
-
const scheduleAnswer = ({ environment, createdAt, ...schedule }, extra) => ({
|
|
50
|
-
...snakeKeys(schedule),
|
|
51
|
-
environment,
|
|
52
|
-
created_at: createdAt,
|
|
53
|
-
...snakeKeys(extra),
|
|
54
|
-
});
|
|
55
|
-
const withWhen = (command) => command
|
|
56
|
-
.option("--every <duration>", "repeat: 30m, 2h, 1d")
|
|
57
|
-
.option("--at <HH:MM>", "every day at this local time")
|
|
58
|
-
.option("--at-time <time>", "one run: 30m, 2d from now, or an ISO time; the schedule removes itself after it");
|
|
59
|
-
/**
|
|
60
|
-
* Runs the CLI's own commands, or an agent, later or on a timer — through systemd (Linux), launchd
|
|
61
|
-
* (macOS) or Task Scheduler (Windows). Every timer runs `schedule exec <file>`; the schedule file holds
|
|
62
|
-
* the rest, so what runs and with which variables is the same on all three.
|
|
63
|
-
*/
|
|
64
|
-
export const scheduleCommandFor = ({ app, context: contextOf }) => {
|
|
65
|
-
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`);
|
|
66
|
-
const prepare = (self, resolving = {}) => {
|
|
67
|
-
const context = contextOf(self, resolving);
|
|
68
|
-
const system = environmentOf(self).system ?? thisMachine();
|
|
69
|
-
const state = resolvePaths({ appName: app.appName, prefix: app.envPrefix, env: context.env }).state;
|
|
70
|
-
const say = (value, lines) => renderAnswer(self, context, itemOf(value), () => lines.join("\n"));
|
|
71
|
-
const timer = () => timerFor(app, system, context.env);
|
|
72
|
-
// The command line refuses a schedule under the lock; a caller that gets here still passes the lock on.
|
|
73
|
-
const readOnly = readOnlyLockOf(app, context.env);
|
|
74
|
-
const environment = () => ({
|
|
75
|
-
[`${app.envPrefix}_PROFILE`]: context.profile,
|
|
76
|
-
...locationVariables(app, context.env),
|
|
77
|
-
// The timer starts with almost no PATH; an agent runs `max` and its own tools by name.
|
|
78
|
-
...(context.env.PATH ? { PATH: context.env.PATH } : {}),
|
|
79
|
-
...(readOnly ? { [readOnly]: "1" } : {}),
|
|
80
|
-
});
|
|
81
|
-
const save = async (schedule) => {
|
|
82
|
-
const file = scheduleFile(state, schedule.name);
|
|
83
|
-
const replaced = existsSync(file);
|
|
84
|
-
writeSecurely(file, `${JSON.stringify(schedule, null, 2)}\n`, 0o600);
|
|
85
|
-
await timer().install(schedule, [...system.entry, "schedule", "exec", file]);
|
|
86
|
-
say(scheduleAnswer(schedule, { file, timer: timer().where(schedule.name), replaced }), [
|
|
87
|
-
`${replaced ? "Replaced" : "Added"} ${schedule.name}: ${describeWhen(schedule.when)}.`,
|
|
88
|
-
`It runs: ${schedule.kind === "agent" ? schedule.argv.join(" ") : `${app.command} ${schedule.argv.join(" ")}`}`,
|
|
89
|
-
`Output: ${join(dirname(file), "runs")} · now: ${lineOf(self, `schedule run ${schedule.name}`)} · stop: ${lineOf(self, `schedule remove ${schedule.name}`)}`,
|
|
90
|
-
]);
|
|
91
|
-
};
|
|
92
|
-
return { context, system, state, say, timer, environment, save };
|
|
93
|
-
};
|
|
94
|
-
withWhen(grantsAuthority(annotate(command.command("command"), { effects: LOCAL_WRITE }))
|
|
95
|
-
.description(`run ${app.command} <args…> on a timer — reads save what they read, so this is how a chat's history fills the store`)
|
|
96
|
-
.argument("<name>", "lowercase letters, digits and dashes")
|
|
97
|
-
.argument("<args...>", `the ${app.command} command after its name, e.g. -- messages list <chat> --limit 50`)).action(async function (name, args) {
|
|
98
|
-
const { environment, save } = prepare(this);
|
|
99
|
-
if (args[0] === "schedule")
|
|
100
|
-
throw new CliError("validation_error", "a schedule cannot run schedule");
|
|
101
|
-
await save({
|
|
102
|
-
name: scheduleName(name),
|
|
103
|
-
kind: "command",
|
|
104
|
-
argv: args,
|
|
105
|
-
when: whenOf(this.opts(), COMMAND_MINIMUM_MINUTES),
|
|
106
|
-
environment: environment(),
|
|
107
|
-
createdAt: new Date().toISOString(),
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
withWhen(grantsAuthority(annotate(command.command("agent"), { effects: LOCAL_WRITE }))
|
|
111
|
-
.description(`start an agent (claude or codex) with a prompt on a timer; it runs with ${app.envPrefix}_READ_ONLY=1, so it reads and drafts and cannot send — its answer is kept and you are notified`)
|
|
112
|
-
.argument("<name>", "lowercase letters, digits and dashes")
|
|
113
|
-
.addArgument(new Argument("<harness>", "the agent to start").choices(HARNESSES))
|
|
114
|
-
.argument("<prompt>", `what to do, e.g. "run the daily follow-up review: ${app.command} skill show follow-ups"`)
|
|
115
|
-
.addOption(new Option("--max-cost <usd>", "claude only: stop a run that costs more than this, in US dollars").argParser(Number))).action(async function (name, harness, prompt) {
|
|
116
|
-
const { context, system, environment, save } = prepare(this);
|
|
117
|
-
const options = this.opts();
|
|
118
|
-
const program = findProgram(harness, context.env, system.platform);
|
|
119
|
-
if (!program)
|
|
120
|
-
throw new CliError("not_found", `${harness} is not on PATH`);
|
|
121
|
-
const paths = resolvePaths({ appName: app.appName, prefix: app.envPrefix, env: context.env });
|
|
122
|
-
const argv = harness === "claude"
|
|
123
|
-
? [
|
|
124
|
-
program,
|
|
125
|
-
"-p",
|
|
126
|
-
prompt,
|
|
127
|
-
// The owner's own settings may let every tool run (bypassPermissions); here only the CLI may.
|
|
128
|
-
"--permission-mode",
|
|
129
|
-
"dontAsk",
|
|
130
|
-
"--allowedTools",
|
|
131
|
-
`Bash(${app.command}:*)`,
|
|
132
|
-
"--no-session-persistence",
|
|
133
|
-
...(options.maxCost ? ["--max-budget-usd", String(options.maxCost)] : []),
|
|
134
|
-
]
|
|
135
|
-
: [
|
|
136
|
-
program,
|
|
137
|
-
"exec",
|
|
138
|
-
"--skip-git-repo-check",
|
|
139
|
-
"--ephemeral",
|
|
140
|
-
"--sandbox",
|
|
141
|
-
"workspace-write",
|
|
142
|
-
// The CLI reaches the messenger and writes its store and state, and nothing else.
|
|
143
|
-
"-c",
|
|
144
|
-
"sandbox_workspace_write.network_access=true",
|
|
145
|
-
...[dirname(storePath(context.env)), paths.state, paths.config, paths.cache].flatMap((dir) => [
|
|
146
|
-
"--add-dir",
|
|
147
|
-
dir,
|
|
148
|
-
]),
|
|
149
|
-
prompt,
|
|
150
|
-
];
|
|
151
|
-
await save({
|
|
152
|
-
name: scheduleName(name),
|
|
153
|
-
kind: "agent",
|
|
154
|
-
argv,
|
|
155
|
-
when: whenOf(options, AGENT_MINIMUM_MINUTES),
|
|
156
|
-
environment: environment(),
|
|
157
|
-
createdAt: new Date().toISOString(),
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
withQuery(annotate(command.command("list"), { effects: LOCAL_READ }), SCHEDULES_QUERY)
|
|
161
|
-
.description("the schedules, when they run and how their last run went")
|
|
162
|
-
.action(function (words) {
|
|
163
|
-
const filter = scheduleFilter(words);
|
|
164
|
-
const { context, state } = prepare(this);
|
|
165
|
-
const rows = listSchedules(state, filter).map((schedule) => ({
|
|
166
|
-
name: schedule.name,
|
|
167
|
-
kind: schedule.kind,
|
|
168
|
-
when: describeWhen(schedule.when),
|
|
169
|
-
last: lastRun(scheduleFile(state, schedule.name)) ?? null,
|
|
170
|
-
}));
|
|
171
|
-
const answer = listOf("schedule list", {
|
|
172
|
-
items: rows.map((row) => snakeKeys(row)),
|
|
173
|
-
limit: Math.max(1, rows.length),
|
|
174
|
-
has_more: false,
|
|
175
|
-
});
|
|
176
|
-
renderAnswer(this, context, answer, () => {
|
|
177
|
-
if (rows.length === 0) {
|
|
178
|
-
context.renderer.note(`no schedules — \`${lineOf(this, "schedule command|agent")}\` adds one`);
|
|
179
|
-
return "";
|
|
180
|
-
}
|
|
181
|
-
return rows
|
|
182
|
-
.map(({ name, kind, when, last }) => [
|
|
183
|
-
name,
|
|
184
|
-
kind,
|
|
185
|
-
when,
|
|
186
|
-
last
|
|
187
|
-
? `last ${last.at.slice(0, 16).replace("T", " ")} exit ${last.exitCode}` +
|
|
188
|
-
(last.failures
|
|
189
|
-
? ` — failing since ${(last.failingSince ?? last.at).slice(0, 16).replace("T", " ")}, ` +
|
|
190
|
-
`${last.failures} in a row${last.reason ? `: ${last.reason}` : ""} — ${last.output}`
|
|
191
|
-
: "")
|
|
192
|
-
: "not run yet",
|
|
193
|
-
].join(" "))
|
|
194
|
-
.join("\n");
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
annotate(command.command("run"), { effects: REMOTE_WRITE })
|
|
198
|
-
.description("run a schedule now, as its timer would, and wait for it")
|
|
199
|
-
.argument("<name>")
|
|
200
|
-
.action(async function (name) {
|
|
201
|
-
const { context, system, state, say, timer } = prepare(this);
|
|
202
|
-
const last = await runSchedule(app, system, context.env, scheduleFile(state, scheduleName(name)), {
|
|
203
|
-
timer: timer(),
|
|
204
|
-
});
|
|
205
|
-
say(snakeKeys({ name, ...last }), [`${name}: exit ${last.exitCode} — ${last.output}`]);
|
|
206
|
-
if (last.exitCode !== 0)
|
|
207
|
-
setExitCode(this, 1);
|
|
208
|
-
});
|
|
209
|
-
grantsAuthority(annotate(command.command("remove"), { effects: LOCAL_WRITE }))
|
|
210
|
-
.description("stop a schedule and remove its timer; its past output stays")
|
|
211
|
-
.argument("<name>")
|
|
212
|
-
.action(async function (name) {
|
|
213
|
-
const { state, say, timer } = prepare(this);
|
|
214
|
-
const file = scheduleFile(state, scheduleName(name));
|
|
215
|
-
if (!existsSync(file))
|
|
216
|
-
throw new CliError("not_found", `no schedule ${name} — \`${lineOf(this, "schedule list")}\``);
|
|
217
|
-
await timer().remove(name);
|
|
218
|
-
rmSync(file, { force: true });
|
|
219
|
-
say({ name, removed: true }, [`Removed ${name}.`]);
|
|
220
|
-
});
|
|
221
|
-
annotate(command.command("exec"), { effects: REMOTE_WRITE })
|
|
222
|
-
.description("what a timer runs, given the schedule's file; to run one by hand, use run")
|
|
223
|
-
.argument("<file>")
|
|
224
|
-
.action(async function (file) {
|
|
225
|
-
// A timer starts it with no profile named; the schedule's own environment carries the profile.
|
|
226
|
-
const { context, system, timer } = prepare(this, { needsProfile: false });
|
|
227
|
-
const last = await runSchedule(app, system, context.env, file, { timer: timer() });
|
|
228
|
-
if (last.exitCode !== 0)
|
|
229
|
-
setExitCode(this, 1);
|
|
230
|
-
});
|
|
231
|
-
return command;
|
|
232
|
-
};
|
|
3
|
+
export { scheduleCommandFor } from "../schedule/schedule-command.js";
|
|
233
4
|
export const scheduleCommand = (messenger) => scheduleCommandFor({
|
|
234
5
|
app: messenger.app,
|
|
235
6
|
context: (command, resolving) => messengerContext(command, messenger, resolving),
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
3
3
|
import { Command } from "commander";
|
|
4
|
+
import { topicIcon } from "../../entities/index.js";
|
|
4
5
|
import { capability } from "../../ports/messenger.js";
|
|
5
6
|
import { ownerOnlyOption, REMOTE_READ, REMOTE_WRITE } from "../effects.js";
|
|
6
7
|
import { itemOf, listOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
7
|
-
import { typedQuery, withQuery } from "../list-query.js";
|
|
8
|
+
import { listQuery, typedQuery, unfilteredQuery, withQuery } from "../list-query.js";
|
|
8
9
|
import { window, withPaging } from "../paging.js";
|
|
9
|
-
import { lines, pageOf, topicAnswer } from "./chat-answers.js";
|
|
10
|
+
import { lines, pageOf, topicAnswer, wholeList } from "./chat-answers.js";
|
|
10
11
|
import { messengerContext } from "./context.js";
|
|
11
12
|
import { TOPICS_QUERY, topicTitle } from "./list-result-queries.js";
|
|
12
13
|
const listed = (messenger) => async function (chat, value) {
|
|
@@ -47,6 +48,17 @@ export const topicsCommand = (messenger) => {
|
|
|
47
48
|
const shown = await context.withServices((services) => services.topics.show(chat, topic));
|
|
48
49
|
renderAnswer(this, context, itemOf(topicAnswer(shown)));
|
|
49
50
|
});
|
|
51
|
+
const icons = new Command("icons").description("the emojis a forum topic can take as its icon");
|
|
52
|
+
topics.addCommand(icons);
|
|
53
|
+
icons.addCommand(withQuery(annotate(new Command("list"), {
|
|
54
|
+
effects: REMOTE_READ,
|
|
55
|
+
examples: [`${messenger.app.command} personal topics icons list`],
|
|
56
|
+
}).description("the emojis `--emoji` takes in topics create and topics edit"), TOPIC_ICONS_QUERY).action(async function (words) {
|
|
57
|
+
const context = messengerContext(this, messenger);
|
|
58
|
+
listQuery(typedQuery(words), TOPIC_ICONS_QUERY, "topics icons list");
|
|
59
|
+
const icons = await context.withServices((services) => services.topics.icons());
|
|
60
|
+
renderAnswer(this, context, wholeList("topics icons list", icons.map((emoji) => ({ emoji }))), lines(({ emoji }) => emoji));
|
|
61
|
+
}));
|
|
50
62
|
annotate(topics.command("enable"), { effects: REMOTE_WRITE })
|
|
51
63
|
.description("enable forum topics; only the owner, with an explicit upgrade for a basic group")
|
|
52
64
|
.argument("<chat>", messenger.chatArgument)
|
|
@@ -61,22 +73,25 @@ export const topicsCommand = (messenger) => {
|
|
|
61
73
|
.argument("<chat>", messenger.chatArgument)
|
|
62
74
|
.argument("<title>", "the topic title, at most 128 UTF-8 bytes")
|
|
63
75
|
.option("--operation-id <id>", "identify this creation attempt; an already sent or unknown id is refused")
|
|
76
|
+
.option("--emoji <emoji>", "the topic's icon, one of `topics icons list`")
|
|
64
77
|
.action(async function (chat, title) {
|
|
65
78
|
const context = messengerContext(this, messenger);
|
|
66
|
-
const
|
|
79
|
+
const { operationId, emoji } = this.opts();
|
|
80
|
+
const created = await context.withServices((services) => services.topics.create(chat, title, { sendId: operationId, ...(emoji === undefined ? {} : { icon: emoji }) }));
|
|
67
81
|
renderAnswer(this, context, itemOf(snakeKeys(created)));
|
|
68
82
|
});
|
|
69
83
|
annotate(topics.command("edit"), { effects: REMOTE_WRITE })
|
|
70
|
-
.description("rename, close or
|
|
84
|
+
.description("rename, close, reopen or give an icon to a forum topic")
|
|
71
85
|
.argument("<chat>", messenger.chatArgument)
|
|
72
86
|
.argument("<topic>", "the topic id, from `topics list`")
|
|
73
87
|
.option("--title <title>", "the new title, at most 128 UTF-8 bytes")
|
|
74
88
|
.option("--closed <on|off>", "on closes the topic to new messages, off reopens it")
|
|
75
89
|
.option("--pinned <on|off>", "on pins the topic at the top of the list, off unpins it")
|
|
76
90
|
.option("--hidden <on|off>", "on hides the General topic from the topic list, off shows it")
|
|
91
|
+
.option("--emoji <emoji>", "the topic's icon, one of `topics icons list`; none removes it")
|
|
77
92
|
.action(async function (chat, topic) {
|
|
78
93
|
const context = messengerContext(this, messenger);
|
|
79
|
-
const { title, closed, pinned, hidden } = this.opts();
|
|
94
|
+
const { title, closed, pinned, hidden, emoji } = this.opts();
|
|
80
95
|
const closes = onOff("--closed", closed);
|
|
81
96
|
const pins = onOff("--pinned", pinned);
|
|
82
97
|
const hides = onOff("--hidden", hidden);
|
|
@@ -85,6 +100,7 @@ export const topicsCommand = (messenger) => {
|
|
|
85
100
|
...(closes === undefined ? {} : { closed: closes }),
|
|
86
101
|
...(pins === undefined ? {} : { pinned: pins }),
|
|
87
102
|
...(hides === undefined ? {} : { hidden: hides }),
|
|
103
|
+
...(emoji === undefined ? {} : { icon: emoji === "none" ? null : emoji }),
|
|
88
104
|
}));
|
|
89
105
|
renderAnswer(this, context, itemOf(snakeKeys(done)));
|
|
90
106
|
});
|
|
@@ -107,6 +123,7 @@ export const topicsCommand = (messenger) => {
|
|
|
107
123
|
});
|
|
108
124
|
return topics;
|
|
109
125
|
};
|
|
126
|
+
const TOPIC_ICONS_QUERY = unfilteredQuery("topic_icon", topicIcon);
|
|
110
127
|
const onOff = (flag, value) => {
|
|
111
128
|
if (value === undefined)
|
|
112
129
|
return undefined;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { type AppIdentity } from "../../ports/app.js";
|
|
3
|
+
import { type AnswerOutput } from "../envelope.js";
|
|
4
|
+
/** What the schedule command needs of a CLI: who it is and, per command, where it prints and which profile runs. */
|
|
5
|
+
export interface ScheduleHost {
|
|
6
|
+
app: AppIdentity;
|
|
7
|
+
context(command: Command, resolving?: {
|
|
8
|
+
needsProfile?: boolean;
|
|
9
|
+
}): AnswerOutput & {
|
|
10
|
+
env: NodeJS.ProcessEnv;
|
|
11
|
+
profile: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Runs the CLI's own commands, or an agent, later or on a timer — through systemd (Linux), launchd
|
|
16
|
+
* (macOS) or Task Scheduler (Windows). Every timer runs `schedule exec <file>`; the schedule file holds
|
|
17
|
+
* the rest, so what runs and with which variables is the same on all three.
|
|
18
|
+
*/
|
|
19
|
+
export declare const scheduleCommandFor: ({ app, context: contextOf }: ScheduleHost) => Command;
|