@wirecat/cli-core 0.410.0 → 0.411.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 +26 -0
- package/dist/cli/bot/command.js +1 -0
- package/dist/cli/bot/port.d.ts +1 -1
- package/dist/cli/bot/setup.js +11 -5
- package/dist/cli/commands-command.js +18 -12
- package/dist/cli/contract-evidence.d.ts +26 -0
- package/dist/cli/contract-evidence.js +58 -0
- package/dist/cli/contract-templates.d.ts +4 -0
- package/dist/cli/contract-templates.js +83 -0
- package/dist/cli/discovery-descriptions.d.ts +4 -0
- package/dist/cli/discovery-descriptions.js +4 -0
- package/dist/cli/index.d.ts +21 -55
- package/dist/cli/index.js +18 -55
- package/dist/cli/messenger/chat-answers.d.ts +1 -1
- package/dist/cli/messenger/chat-answers.js +1 -1
- package/dist/cli/messenger/chat-lists.d.ts +1 -1
- package/dist/cli/messenger/chats-command.js +3 -0
- package/dist/cli/messenger/contacts-command.js +23 -15
- package/dist/cli/messenger/guard-commands.d.ts +5 -1
- package/dist/cli/messenger/guard-commands.js +5 -3
- package/dist/cli/messenger/messages-press-command.js +4 -1
- package/dist/cli/messenger/models-command.js +3 -11
- package/dist/cli/messenger/records-command.js +1 -1
- package/dist/cli/messenger/tasks-command.js +14 -4
- package/dist/cli/model-catalogue-command.d.ts +15 -0
- package/dist/cli/model-catalogue-command.js +110 -0
- package/dist/cli/program.js +2 -0
- package/dist/cli/runs/search-command.js +2 -3
- package/dist/cli/runs/search-query.d.ts +1 -1
- package/dist/cli/runs/search-query.js +18 -2
- package/dist/cli/runs/search.d.ts +2 -1
- package/dist/cli/runs/search.js +4 -3
- package/dist/cli/shared-command-exports.d.ts +644 -0
- package/dist/cli/shared-command-exports.js +114 -0
- package/dist/cli/shared-contracts.d.ts +1 -0
- package/dist/cli/shared-contracts.js +1 -0
- package/dist/entities/chat.d.ts +3 -3
- package/dist/entities/chat.js +2 -2
- package/dist/entities/contact-context.d.ts +8 -8
- package/dist/entities/contact.d.ts +4 -4
- package/dist/entities/index.d.ts +11 -12
- package/dist/entities/list-results.d.ts +1 -1
- package/dist/entities/list-results.js +1 -1
- package/dist/entities/send-attempt.d.ts +0 -1
- package/dist/entities/send-attempt.js +0 -1
- package/dist/errors/reasons.d.ts +1 -1
- package/dist/errors/reasons.js +7 -0
- package/dist/meetings/cli/index.d.ts +1 -1
- package/dist/meetings/cli/index.js +25 -9
- package/dist/meetings/cli/passage.d.ts +0 -1
- package/dist/meetings/cli/passage.js +5 -3
- package/dist/meetings/cli/project-evidence.js +3 -2
- package/dist/meetings/cli/search.d.ts +2 -2
- package/dist/meetings/cli/search.js +9 -7
- package/dist/meetings/event-query.d.ts +1 -0
- package/dist/meetings/event-query.js +15 -0
- package/dist/meetings/index.d.ts +1 -0
- package/dist/meetings/index.js +1 -0
- package/dist/meetings/store.d.ts +2 -0
- package/dist/meetings/testing/memory.js +5 -0
- package/dist/services/tasks.d.ts +2 -0
- package/dist/services/tasks.js +6 -4
- package/dist/store/sqlite/events.d.ts +2 -1
- package/dist/store/sqlite/events.js +16 -1
- package/dist/store/sqlite/meetings.js +3 -0
- package/package.json +3 -3
- package/parity.json +61 -76
|
@@ -101,7 +101,7 @@ export declare const chatShown: (account: AccountKey, card: Parameters<typeof ch
|
|
|
101
101
|
check?: {
|
|
102
102
|
[x: string]: unknown;
|
|
103
103
|
} | undefined;
|
|
104
|
-
}[] |
|
|
104
|
+
}[] | undefined;
|
|
105
105
|
}>;
|
|
106
106
|
/** The lowest score `score>=n` asks for; the audit's own floor when the query names none. */
|
|
107
107
|
export declare const scoreFloor: (ast: QueryAst | undefined) => number;
|
|
@@ -38,7 +38,7 @@ export const chatAnswer = (account, chat) => defined({
|
|
|
38
38
|
description: chat.description,
|
|
39
39
|
invite_link: chat.link,
|
|
40
40
|
settings: chat.settings ? settingsOf(chat.settings) : undefined,
|
|
41
|
-
members: chat.members
|
|
41
|
+
members: chat.members?.map(memberAnswer),
|
|
42
42
|
});
|
|
43
43
|
export const memberAnswer = (member) => {
|
|
44
44
|
const group = member;
|
|
@@ -61,7 +61,7 @@ export declare const chatsList: (services: Services, query: QueryWords, paging:
|
|
|
61
61
|
check?: {
|
|
62
62
|
[x: string]: unknown;
|
|
63
63
|
} | undefined;
|
|
64
|
-
}[] |
|
|
64
|
+
}[] | undefined;
|
|
65
65
|
}>>;
|
|
66
66
|
export declare const chatEvents: (services: Services, chat: string, query: QueryWords) => Promise<import("../envelope.js").ListBody<{
|
|
67
67
|
chat_id: string;
|
|
@@ -109,6 +109,8 @@ export const chatsCommand = (messenger) => {
|
|
|
109
109
|
"admins add": `"Book club" "Alex Example"${messenger.pickAdminRights === false ? "" : ` --can ${messenger.adminRights?.[0] ?? "info"}`}`,
|
|
110
110
|
"admins remove": '"Book club" "Alex Example"',
|
|
111
111
|
create: '"Book club" "Alex Example"',
|
|
112
|
+
app: '"Example Bot"',
|
|
113
|
+
start: '"Example Bot"',
|
|
112
114
|
"documents list": '"Project room" --limit 10',
|
|
113
115
|
events: '"Book club"',
|
|
114
116
|
"folders create": '"Reading" --chat "Book club"',
|
|
@@ -125,6 +127,7 @@ export const chatsCommand = (messenger) => {
|
|
|
125
127
|
"links list": '"Book club"',
|
|
126
128
|
"links reset": '"Book club"',
|
|
127
129
|
"links remove": '"Book club" https://example.com/invite/book-club',
|
|
130
|
+
"links update": '"Book club" https://example.com/invite/book-club --max-uses 25',
|
|
128
131
|
"links show": '"Book club"',
|
|
129
132
|
list: "--limit 10",
|
|
130
133
|
"mark-read": '"Book club" --until-id 101',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
3
|
+
import { defineQuery } from "@wirecat/cli-base/query";
|
|
3
4
|
import { Command } from "commander";
|
|
4
5
|
import { REGISTRIES, registriesCover } from "../../botcheck/registries.js";
|
|
5
6
|
import { levelFor } from "../../sends/permissions.js";
|
|
@@ -12,7 +13,7 @@ import { readSecret } from "../../terminal/prompt.js";
|
|
|
12
13
|
import { secretInputOf } from "../context.js";
|
|
13
14
|
import { FETCH, LOCAL_WRITE, REMOTE_READ } from "../effects.js";
|
|
14
15
|
import { itemOf, listOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
15
|
-
import { listQuery, nativeTerms, typedQuery, withQuery } from "../list-query.js";
|
|
16
|
+
import { listQuery, nativeField, nativeTerms, termField, timeBounds, typedQuery, withQuery } from "../list-query.js";
|
|
16
17
|
import { positiveCount, window, withPaging } from "../paging.js";
|
|
17
18
|
import { casKey } from "../registry-keys.js";
|
|
18
19
|
import { commandAt, nextStep } from "../steps.js";
|
|
@@ -23,6 +24,19 @@ import { hideNever } from "./lacks.js";
|
|
|
23
24
|
import { CONTACTS_QUERY, notesAllowed } from "./people-queries.js";
|
|
24
25
|
import { privatePeopleCommands } from "./private-people-command.js";
|
|
25
26
|
import { syncsFirst } from "./search-sync-options.js";
|
|
27
|
+
const CONTACT_ACTIVITY_QUERY = defineQuery({
|
|
28
|
+
entity: "contact_activity",
|
|
29
|
+
backend: "native",
|
|
30
|
+
fields: [
|
|
31
|
+
termField("scope", "enum", "scope:work", { values: SCOPES }),
|
|
32
|
+
nativeField({
|
|
33
|
+
name: "at",
|
|
34
|
+
type: "time",
|
|
35
|
+
operators: ["term", "phrase", "range"],
|
|
36
|
+
example: "at:[2026-10-01 TO 2026-10-31]",
|
|
37
|
+
}),
|
|
38
|
+
],
|
|
39
|
+
});
|
|
26
40
|
/** People this account has a one-to-one chat with, as the people service counts them. */
|
|
27
41
|
const collect = (value, previous = []) => [...previous, value];
|
|
28
42
|
export const contactsCommand = (messenger) => {
|
|
@@ -173,28 +187,24 @@ export const contactsCommand = (messenger) => {
|
|
|
173
187
|
meta: { ...window.meta, ...(hasMore ? { truncated: true } : {}) },
|
|
174
188
|
}));
|
|
175
189
|
});
|
|
176
|
-
annotate(contacts.command("events"), {
|
|
190
|
+
withQuery(annotate(contacts.command("events"), {
|
|
177
191
|
effects: REMOTE_READ,
|
|
178
192
|
output: { entity: "contact_activity" },
|
|
179
193
|
})
|
|
180
194
|
.summary("one person's activity across their linked identities")
|
|
181
195
|
.description("everything one person took part in, in every messenger linked to them — messages they wrote or were " +
|
|
182
196
|
"mentioned in, chats, mail, meetings, tasks — newest first, from the store; never connects")
|
|
183
|
-
.argument("<person>", "their id, @username, or part of their name")
|
|
184
|
-
.option("--scope <scope>", "only what belongs to personal or to work accounts: personal or work")
|
|
185
|
-
.option("--since-time <time>", "nothing older than this ISO 8601 time, or 2h / 1d ago")
|
|
186
|
-
.option("--until-time <time>", "through this ISO 8601 time, or 2h / 1d ago")
|
|
197
|
+
.argument("<person>", "their id, @username, or part of their name"), CONTACT_ACTIVITY_QUERY)
|
|
187
198
|
.option("--limit <n>", `at most this many; ${TIMELINE_ITEMS} if not given`, positiveCount("--limit"))
|
|
188
199
|
.option("--cursor <cursor>", "go on from where the last page stopped; page.next gives it, with the same filters")
|
|
189
|
-
.action(async function (person) {
|
|
190
|
-
const
|
|
200
|
+
.action(async function (person, words) {
|
|
201
|
+
const terms = nativeTerms(listQuery(typedQuery(words), CONTACT_ACTIVITY_QUERY, "contacts events"), "contacts events");
|
|
202
|
+
const scope = terms.get("scope")?.value.toLowerCase();
|
|
203
|
+
const { limit, cursor } = this.opts();
|
|
191
204
|
if (scope !== undefined && !SCOPES.includes(scope))
|
|
192
205
|
throw new CliError("validation_error", `--scope is personal or work, not "${scope}"`);
|
|
193
206
|
const context = messengerContext(this, messenger);
|
|
194
|
-
const window =
|
|
195
|
-
...(sinceTime === undefined ? {} : { sinceTime }),
|
|
196
|
-
...(untilTime === undefined ? {} : { untilTime }),
|
|
197
|
-
});
|
|
207
|
+
const window = timeBounds(terms.get("at"));
|
|
198
208
|
const found = await context.withServices((services) => services.people.timeline(person, {
|
|
199
209
|
...(scope === undefined ? {} : { scope: scope }),
|
|
200
210
|
...(window.since === undefined ? {} : { since: window.since }),
|
|
@@ -211,9 +221,7 @@ export const contactsCommand = (messenger) => {
|
|
|
211
221
|
...(found.cursor === undefined ? {} : { cursor: found.cursor }),
|
|
212
222
|
args: {
|
|
213
223
|
person,
|
|
214
|
-
...(
|
|
215
|
-
...(sinceTime === undefined ? {} : { since_time: sinceTime }),
|
|
216
|
-
...(untilTime === undefined ? {} : { until_time: untilTime }),
|
|
224
|
+
...(words.length ? { query: typedQuery(words) } : {}),
|
|
217
225
|
},
|
|
218
226
|
meta: { ...window.meta, ...(pending > 0 ? { truncated: true } : {}) },
|
|
219
227
|
}));
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import { type SendEntry } from "../../sends/journal.js";
|
|
2
3
|
import { type Messenger } from "./context.js";
|
|
3
4
|
/**
|
|
4
5
|
* The chats this profile may send to. Off until the first `add`; `clear` deletes it, which turns it off again. It stops
|
|
5
6
|
* a model that a message talked into writing somewhere, not an agent set on getting around it.
|
|
6
7
|
*/
|
|
7
|
-
export declare const recipientsCommand: (messenger: Messenger
|
|
8
|
+
export declare const recipientsCommand: (messenger: Messenger, options?: {
|
|
9
|
+
resolve?: "chat-card";
|
|
10
|
+
}) => Command;
|
|
8
11
|
/** Every attempt to send from this profile, kept whatever `--record` says. Never the text. */
|
|
9
12
|
export declare const sendsCommand: (messenger: Messenger) => Command;
|
|
13
|
+
export declare const sendAttemptItem: ({ sendId, operationId, ...entry }: SendEntry) => unknown;
|
|
@@ -15,7 +15,7 @@ import { SENDS_QUERY, sendFilter } from "./sends-query.js";
|
|
|
15
15
|
* The chats this profile may send to. Off until the first `add`; `clear` deletes it, which turns it off again. It stops
|
|
16
16
|
* a model that a message talked into writing somewhere, not an agent set on getting around it.
|
|
17
17
|
*/
|
|
18
|
-
export const recipientsCommand = (messenger) => {
|
|
18
|
+
export const recipientsCommand = (messenger, options = {}) => {
|
|
19
19
|
const tool = `${messenger.app.command} personal`;
|
|
20
20
|
const listFor = (profile, env) => new RecipientList(recipientsPathFor(messenger.app, profile, env), messenger.app.command);
|
|
21
21
|
const command = new Command("recipients").description("the chats this profile may send to, when the list is on");
|
|
@@ -43,7 +43,8 @@ export const recipientsCommand = (messenger) => {
|
|
|
43
43
|
.description("allow sending to this chat; the first add turns the list on")
|
|
44
44
|
.action(async function (chat) {
|
|
45
45
|
const context = messengerContext(this, messenger);
|
|
46
|
-
|
|
46
|
+
// Some transports resolve an id without reading its title or dialog partner.
|
|
47
|
+
const found = await context.withMessenger((connection) => options.resolve === "chat-card" ? connection.chat(chat) : connection.resolve(chat));
|
|
47
48
|
const partnerId = messenger.partnerOf?.(found);
|
|
48
49
|
const added = await listFor(context.settings.profile, context.env).addAsync({
|
|
49
50
|
id: found.id,
|
|
@@ -93,10 +94,11 @@ export const sendsCommand = (messenger) => new Command("sends").description("eve
|
|
|
93
94
|
const from = settings.all ? 0 : (settings.page - 1) * settings.limit;
|
|
94
95
|
const shown = settings.all ? entries : entries.slice(from, from + settings.limit);
|
|
95
96
|
renderAnswer(this, context, pagedList("sends list", settings, {
|
|
96
|
-
items: shown.map(
|
|
97
|
+
items: shown.map(sendAttemptItem),
|
|
97
98
|
args: words.length ? { query: typedQuery(words) } : {},
|
|
98
99
|
has_more: from + shown.length < entries.length,
|
|
99
100
|
}));
|
|
100
101
|
if (entries.length === 0)
|
|
101
102
|
renderer.note(filter ? "no send attempts match the query" : `profile ${settings.profile} has not tried to send anything`);
|
|
102
103
|
}));
|
|
104
|
+
export const sendAttemptItem = ({ sendId, operationId, ...entry }) => snakeKeys({ ...entry, ...((operationId ?? sendId) === undefined ? {} : { operationId: operationId ?? sendId }) });
|
|
@@ -5,7 +5,10 @@ import { REMOTE_WRITE } from "../effects.js";
|
|
|
5
5
|
import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
6
6
|
import { messengerContext } from "./context.js";
|
|
7
7
|
/** `messages press` — one of a bot's callback buttons; the bot sees who pressed it. */
|
|
8
|
-
export const pressCommand = (messenger) => annotate(new Command("press"), {
|
|
8
|
+
export const pressCommand = (messenger) => annotate(new Command("press"), {
|
|
9
|
+
effects: REMOTE_WRITE,
|
|
10
|
+
examples: [`${messenger.app.command} personal messages press "Example Bot" 101 1`],
|
|
11
|
+
})
|
|
9
12
|
.description("press a bot's button under a message; the bot sees that you pressed it")
|
|
10
13
|
.argument("<chat>", messenger.chatArgument)
|
|
11
14
|
.argument("<message>", "the id of the message with the buttons")
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { CliError } from "@wirecat/cli-base";
|
|
3
3
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
4
|
-
import { defineQuery } from "@wirecat/cli-base/query";
|
|
5
4
|
import { Command } from "commander";
|
|
6
5
|
import { isTextModelInstalled, textModelsDirectory } from "../../embeddings/embed.js";
|
|
7
6
|
import { embeddingKeys } from "../../embeddings/keys.js";
|
|
@@ -16,19 +15,12 @@ import { readSecret } from "../../terminal/prompt.js";
|
|
|
16
15
|
import { baseContext, environmentOf } from "../context.js";
|
|
17
16
|
import { FETCH, grantsAuthority, LOCAL_READ, LOCAL_WRITE, reachesOtherService } from "../effects.js";
|
|
18
17
|
import { itemOf, listOf, renderAnswer } from "../envelope.js";
|
|
19
|
-
import { listQuery, nativeTerms,
|
|
18
|
+
import { listQuery, nativeTerms, typedQuery, unfilteredQuery, withQuery } from "../list-query.js";
|
|
19
|
+
import { MODEL_CATALOGUE_QUERY } from "../model-catalogue-command.js";
|
|
20
20
|
import { nextStep } from "../steps.js";
|
|
21
21
|
/** A model list is the catalog this version ships. */
|
|
22
22
|
const MODELS_QUERY = unfilteredQuery("model", modelEntity);
|
|
23
|
-
const ROOT_MODELS_QUERY =
|
|
24
|
-
entity: "model",
|
|
25
|
-
backend: "native",
|
|
26
|
-
fields: [
|
|
27
|
-
termField("kind", "enum", "kind:audio", { values: ["audio", "text"] }),
|
|
28
|
-
termField("id", "kw", "id:e5-small"),
|
|
29
|
-
termField("downloaded", "bool", "downloaded:true"),
|
|
30
|
-
],
|
|
31
|
-
});
|
|
23
|
+
const ROOT_MODELS_QUERY = MODEL_CATALOGUE_QUERY;
|
|
32
24
|
/** Every model this version knows: a short, fixed list, never paged. */
|
|
33
25
|
const everyModel = (command, items, next = []) => listOf(command, { items, limit: Math.max(1, items.length), has_more: false, next });
|
|
34
26
|
const audioItem = (model, directory, chosen) => ({
|
|
@@ -22,7 +22,7 @@ const kindsOf = (typed) => {
|
|
|
22
22
|
export const mediaCommand = (messenger) => annotate(new Command("media"), { effects: REMOTE_READ })
|
|
23
23
|
.description("a chat's photos, videos, files, audio and links, from the messenger's server; reading marks nothing")
|
|
24
24
|
.argument("<chat>", messenger.chatArgument)
|
|
25
|
-
.option("--type <
|
|
25
|
+
.option("--type <type>", `only these kinds, comma-separated: ${MEDIA_KINDS.join(", ")}`)
|
|
26
26
|
.option("--limit <n>", "how many to show", positiveCount("--limit"))
|
|
27
27
|
.option("--cursor <cursor>", "go on from where page.next says the last page stopped")
|
|
28
28
|
.action(async function (chat) {
|
|
@@ -108,6 +108,10 @@ const holderOf = (command) => holderIn(environmentOf(command).env ?? process.env
|
|
|
108
108
|
const HOLDER_HELP = "who you are for claims: a session id or a chat id; default WIRECAT_HOLDER, else Claude Code's session, else the agent's name";
|
|
109
109
|
const planFrom = (options) => {
|
|
110
110
|
const plan = {};
|
|
111
|
+
if (options.priority !== undefined)
|
|
112
|
+
plan.priority = options.priority;
|
|
113
|
+
if (options.parent !== undefined)
|
|
114
|
+
plan.parent = options.parent;
|
|
111
115
|
if (options.category !== undefined)
|
|
112
116
|
plan.category = options.category;
|
|
113
117
|
if (options.title !== undefined)
|
|
@@ -190,15 +194,21 @@ export const tasksCommand = (messenger) => {
|
|
|
190
194
|
effects: LOCAL_WRITE,
|
|
191
195
|
examples: [`${tool} personal tasks update DOC-12 --title "Review draft"`],
|
|
192
196
|
}))
|
|
193
|
-
.summary("
|
|
194
|
-
.description("
|
|
197
|
+
.summary("update a task's text, schedule, category, assignees, priority or parent")
|
|
198
|
+
.description("update a task's text, schedule, category, assignees, priority or parent; nothing reaches the messenger")
|
|
195
199
|
.argument("<task>", "the task's id or key, as tasks list shows it")
|
|
196
|
-
.option("--category <name>", `${categoryHelp}; a change is kept in the task's history`)
|
|
200
|
+
.option("--category <name>", `${categoryHelp}; a change is kept in the task's history`)
|
|
201
|
+
.option("--priority <n>", "priority from 0 (urgent) through 4 (low)", (value) => {
|
|
202
|
+
if (!/^[0-4]$/.test(value))
|
|
203
|
+
throw new CliError("validation_error", "--priority takes 0 through 4");
|
|
204
|
+
return Number(value);
|
|
205
|
+
})
|
|
206
|
+
.option("--parent <id>", "parent task in the same project; none clears it", (value) => value === "none" ? null : value), "send Alex the API token").action(async function (id) {
|
|
197
207
|
const context = messengerContext(this, messenger);
|
|
198
208
|
refuseLocalWrite(context, messenger.app.command, "tasks.update");
|
|
199
209
|
const plan = planFrom(this.opts());
|
|
200
210
|
if (Object.keys(plan).length === 0)
|
|
201
|
-
throw new CliError("validation_error", "give at least one of --title, --description, --due, --remind-time, --category");
|
|
211
|
+
throw new CliError("validation_error", "give at least one of --title, --description, --due, --remind-time, --category, --priority, --parent");
|
|
202
212
|
const planned = await context.withServices((services) => services.tasks.plan(id, plan, writerOf(this)));
|
|
203
213
|
renderAnswer(this, context, itemOf(snakeKeys(planned)), () => `${card(planned)}\n`);
|
|
204
214
|
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import type { AppIdentity } from "../ports/app.js";
|
|
3
|
+
import { type Body } from "./envelope.js";
|
|
4
|
+
export declare const MODEL_CATALOGUE_QUERY: import("@wirecat/cli-base/query").QueryRegistry;
|
|
5
|
+
export interface ModelCatalogueOptions {
|
|
6
|
+
kinds: readonly ("audio" | "text")[];
|
|
7
|
+
env: () => NodeJS.ProcessEnv;
|
|
8
|
+
output: (answer: Body, command: Command) => void;
|
|
9
|
+
diagnostic: (line: string) => void;
|
|
10
|
+
jsonOption?: boolean;
|
|
11
|
+
offline?: () => boolean;
|
|
12
|
+
defaultAudio?: () => string;
|
|
13
|
+
}
|
|
14
|
+
/** Provider composition supplies I/O and supported kinds; parsing, receipts and download policy stay shared. */
|
|
15
|
+
export declare const modelCatalogueCommand: (app: AppIdentity, options: ModelCatalogueOptions) => Command;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { CliError } from "@wirecat/cli-base";
|
|
3
|
+
import { annotate } from "@wirecat/cli-base/commands";
|
|
4
|
+
import { defineQuery } from "@wirecat/cli-base/query";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { isTextModelInstalled, textModelsDirectory } from "../embeddings/embed.js";
|
|
7
|
+
import { DEFAULT_TEXT_MODEL, TEXT_MODELS } from "../embeddings/models.js";
|
|
8
|
+
import { downloadTextModel } from "../services/model-downloads.js";
|
|
9
|
+
import { install, installedBytes, isInstalled, modelsDirectory } from "../speech/install.js";
|
|
10
|
+
import { DEFAULT_ORDER, orderedModels, speechModel } from "../speech/models.js";
|
|
11
|
+
import { FETCH, LOCAL_READ, NO_EFFECTS } from "./effects.js";
|
|
12
|
+
import { itemOf, listOf } from "./envelope.js";
|
|
13
|
+
import { listQuery, nativeTerms, termField, typedQuery, withQuery } from "./list-query.js";
|
|
14
|
+
export const MODEL_CATALOGUE_QUERY = defineQuery({
|
|
15
|
+
entity: "model",
|
|
16
|
+
backend: "native",
|
|
17
|
+
fields: [
|
|
18
|
+
termField("kind", "enum", "kind:audio", { values: ["audio", "text"] }),
|
|
19
|
+
termField("id", "kw", "id:e5-small"),
|
|
20
|
+
termField("downloaded", "bool", "downloaded:true"),
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
/** Provider composition supplies I/O and supported kinds; parsing, receipts and download policy stay shared. */
|
|
24
|
+
export const modelCatalogueCommand = (app, options) => {
|
|
25
|
+
if (options.kinds.length === 0)
|
|
26
|
+
throw new CliError("configuration_error", "declare at least one supported model kind");
|
|
27
|
+
const audio = (id, env) => {
|
|
28
|
+
const model = speechModel(id);
|
|
29
|
+
const directory = modelsDirectory(env);
|
|
30
|
+
return {
|
|
31
|
+
id,
|
|
32
|
+
kind: "audio",
|
|
33
|
+
title: model.title,
|
|
34
|
+
languages: model.languages,
|
|
35
|
+
size_bytes: installedBytes(model),
|
|
36
|
+
downloaded: isInstalled(model, directory),
|
|
37
|
+
default: id === (options.defaultAudio?.() ?? DEFAULT_ORDER[0]),
|
|
38
|
+
path: join(directory, id),
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
const text = (id, env) => {
|
|
42
|
+
const model = TEXT_MODELS.find((one) => one.id === id);
|
|
43
|
+
if (!model)
|
|
44
|
+
throw new CliError("validation_error", `unknown text model ${id}`);
|
|
45
|
+
const directory = textModelsDirectory(env);
|
|
46
|
+
return {
|
|
47
|
+
id,
|
|
48
|
+
kind: "text",
|
|
49
|
+
title: model.title,
|
|
50
|
+
languages: model.languages,
|
|
51
|
+
licence: model.licence,
|
|
52
|
+
size_bytes: model.files.reduce((sum, file) => sum + file.bytes, 0),
|
|
53
|
+
downloaded: isTextModelInstalled(model, directory),
|
|
54
|
+
default: id === DEFAULT_TEXT_MODEL,
|
|
55
|
+
path: join(directory, id),
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
const models = annotate(new Command("models"), { effects: NO_EFFECTS }).description("models that run on this machine");
|
|
59
|
+
withQuery(annotate(models.command("list"), {
|
|
60
|
+
effects: LOCAL_READ,
|
|
61
|
+
output: { entity: "model" },
|
|
62
|
+
examples: [`${app.command} models list`, `${app.command} models list kind:${options.kinds[0]}`],
|
|
63
|
+
}), MODEL_CATALOGUE_QUERY)
|
|
64
|
+
.description("speech and embedding models, their download status and the default of each kind")
|
|
65
|
+
.action(function (words) {
|
|
66
|
+
const terms = nativeTerms(listQuery(typedQuery(words), MODEL_CATALOGUE_QUERY, "models list"), "models list");
|
|
67
|
+
const env = options.env();
|
|
68
|
+
const items = [
|
|
69
|
+
...(options.kinds.includes("audio") ? orderedModels().map((model) => audio(model.id, env)) : []),
|
|
70
|
+
...(options.kinds.includes("text") ? TEXT_MODELS.map((model) => text(model.id, env)) : []),
|
|
71
|
+
].filter((item) => (terms.get("kind") === undefined || item.kind === terms.get("kind")?.value) &&
|
|
72
|
+
(terms.get("id") === undefined || item.id === terms.get("id")?.value) &&
|
|
73
|
+
(terms.get("downloaded") === undefined || item.downloaded === (terms.get("downloaded")?.value === "true")));
|
|
74
|
+
options.output(listOf("models list", { items, limit: Math.max(1, items.length), has_more: false }), this);
|
|
75
|
+
});
|
|
76
|
+
annotate(models.command("download"), {
|
|
77
|
+
effects: FETCH,
|
|
78
|
+
output: { entity: "model" },
|
|
79
|
+
examples: [
|
|
80
|
+
`${app.command} models download ${options.kinds.includes("text") ? DEFAULT_TEXT_MODEL : DEFAULT_ORDER[0]}`,
|
|
81
|
+
],
|
|
82
|
+
})
|
|
83
|
+
.description("download and verify one speech or embedding model")
|
|
84
|
+
.argument("<model>", "a model id from models list")
|
|
85
|
+
.option("--offline", "refuse a download that needs the network")
|
|
86
|
+
.option("--accept-terms", "accept the model's licence terms, for a model that has its own")
|
|
87
|
+
.action(async function (id, given) {
|
|
88
|
+
if (options.offline?.() || this.optsWithGlobals().offline)
|
|
89
|
+
throw new CliError("permission_error", "downloading a model needs the network; --offline was requested", {
|
|
90
|
+
reason: "model.offline",
|
|
91
|
+
});
|
|
92
|
+
const env = options.env();
|
|
93
|
+
if (TEXT_MODELS.some((model) => model.id === id)) {
|
|
94
|
+
if (!options.kinds.includes("text"))
|
|
95
|
+
throw new CliError("validation_error", "this tool does not use text models");
|
|
96
|
+
await downloadTextModel(id, { env, acceptTerms: given.acceptTerms === true, progress: options.diagnostic });
|
|
97
|
+
options.output(itemOf(text(id, env)), this);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
if (!options.kinds.includes("audio"))
|
|
101
|
+
throw new CliError("validation_error", "this tool does not use audio models");
|
|
102
|
+
await install(speechModel(id), modelsDirectory(env), { verifyExisting: true, progress: options.diagnostic });
|
|
103
|
+
options.output(itemOf(audio(id, env)), this);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
if (options.jsonOption)
|
|
107
|
+
for (const command of models.commands)
|
|
108
|
+
command.option("--json", "print JSON");
|
|
109
|
+
return models;
|
|
110
|
+
};
|
package/dist/cli/program.js
CHANGED
|
@@ -30,6 +30,7 @@ import { PreviewComplete, preview } from "./preview.js";
|
|
|
30
30
|
import { commandWords, liftProfile, PROFILE_FREE, valuedOptions } from "./profile.js";
|
|
31
31
|
import { absolutePaths, relayTarget } from "./relay-route.js";
|
|
32
32
|
import { settingsFor } from "./settings.js";
|
|
33
|
+
import { assertSharedContracts } from "./shared-contracts.js";
|
|
33
34
|
import { commandAt, lineOf, withLines } from "./steps.js";
|
|
34
35
|
/**
|
|
35
36
|
* The command tree, built fresh on each call. Commander is global state by default —
|
|
@@ -66,6 +67,7 @@ export const createProgram = ({ app, commands, configure, firstScreen }, { out,
|
|
|
66
67
|
for (const command of commands())
|
|
67
68
|
program.addCommand(command);
|
|
68
69
|
configure?.(program);
|
|
70
|
+
assertSharedContracts(program);
|
|
69
71
|
installHelp(program, { pitch: app.description, ...firstScreen });
|
|
70
72
|
// Commander builds a usage line from the command tree, where the profile is not a word.
|
|
71
73
|
for (const top of program.commands) {
|
|
@@ -12,7 +12,6 @@ import { RUN_SEARCH_QUERY, runSearchFilter } from "./search-query.js";
|
|
|
12
12
|
export const runsSearchCommand = (app) => annotate(withQuery(new Command("runs")
|
|
13
13
|
.summary("search recorded diagnostic metadata and events")
|
|
14
14
|
.description("search recorded diagnostic metadata and events; literal words and field:value terms joined by AND; no message contents")
|
|
15
|
-
.option("--since-time <time>", "runs starting on or after an ISO time or 2h / 1d ago")
|
|
16
15
|
.option("--limit <n>", "matches per page, at most 100", positiveCount("--limit"), 20)
|
|
17
16
|
.option("--page <n>", "result page", positiveCount("--page"), 1), RUN_SEARCH_QUERY), {
|
|
18
17
|
effects: LOCAL_READ,
|
|
@@ -22,6 +21,6 @@ export const runsSearchCommand = (app) => annotate(withQuery(new Command("runs")
|
|
|
22
21
|
const filter = runSearchFilter(words);
|
|
23
22
|
const options = this.opts();
|
|
24
23
|
const dir = runsDirFor(app, environmentOf(this).env ?? process.env);
|
|
25
|
-
const found = searchRuns(dir, { ...filter, ...options
|
|
26
|
-
renderAnswer(this, outputFor(this), runsFound(found, { query: typedQuery(words)
|
|
24
|
+
const found = searchRuns(dir, { ...filter, ...options });
|
|
25
|
+
renderAnswer(this, outputFor(this), runsFound(found, { query: typedQuery(words) }));
|
|
27
26
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { type QueryWords } from "../list-query.js";
|
|
2
2
|
import type { RunSearch } from "./search.js";
|
|
3
3
|
export declare const RUN_SEARCH_QUERY: import("@wirecat/cli-base/query").QueryRegistry;
|
|
4
|
-
export declare const runSearchFilter: (words: QueryWords) => Pick<RunSearch, "terms" | "status" | "errorCode" | "operation" | "profile">;
|
|
4
|
+
export declare const runSearchFilter: (words: QueryWords) => Pick<RunSearch, "terms" | "status" | "errorCode" | "operation" | "profile" | "since" | "until">;
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { defineQuery, parseLucene, queryError, validateQuery } from "@wirecat/cli-base/query";
|
|
2
|
-
import { LIST_QUERY_GUIDE, termField, typedQuery } from "../list-query.js";
|
|
1
|
+
import { defineQuery, parseLucene, queryError, validateQuery, } from "@wirecat/cli-base/query";
|
|
2
|
+
import { LIST_QUERY_GUIDE, nativeField, termField, timeBounds, typedQuery } from "../list-query.js";
|
|
3
3
|
export const RUN_SEARCH_QUERY = defineQuery({
|
|
4
4
|
entity: "run",
|
|
5
5
|
backend: "native",
|
|
6
6
|
fields: [
|
|
7
|
+
nativeField({
|
|
8
|
+
name: "started_at",
|
|
9
|
+
type: "time",
|
|
10
|
+
operators: ["term", "phrase", "range"],
|
|
11
|
+
example: "started_at>=7d",
|
|
12
|
+
}),
|
|
7
13
|
termField("text", "kw", "text:timeout"),
|
|
8
14
|
termField("status", "enum", "status:failed", { values: ["success", "failed", "partial", "running"] }),
|
|
9
15
|
termField("error_code", "kw", "error_code:rate_limited"),
|
|
@@ -20,6 +26,7 @@ export const runSearchFilter = (words) => {
|
|
|
20
26
|
validateQuery(ast, RUN_SEARCH_QUERY, context);
|
|
21
27
|
const terms = [];
|
|
22
28
|
const fields = new Map();
|
|
29
|
+
let started;
|
|
23
30
|
const add = (node) => {
|
|
24
31
|
if (node.kind === "boolean") {
|
|
25
32
|
for (const clause of node.clauses) {
|
|
@@ -29,6 +36,12 @@ export const runSearchFilter = (words) => {
|
|
|
29
36
|
}
|
|
30
37
|
return;
|
|
31
38
|
}
|
|
39
|
+
if (node.field === "started_at") {
|
|
40
|
+
if (started)
|
|
41
|
+
queryError("query.unsupported-placement", node.span, "started_at only once here; use a range", context);
|
|
42
|
+
started = node;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
32
45
|
if (node.operator !== "term" && node.operator !== "phrase")
|
|
33
46
|
queryError("query.unsupported-placement", node.span, "run search takes literal words and field:value terms", context);
|
|
34
47
|
if (node.field === "text")
|
|
@@ -40,7 +53,10 @@ export const runSearchFilter = (words) => {
|
|
|
40
53
|
}
|
|
41
54
|
};
|
|
42
55
|
add(ast.root);
|
|
56
|
+
const window = timeBounds(started);
|
|
43
57
|
return {
|
|
58
|
+
...(window.since === undefined ? {} : { since: new Date(window.since).toISOString() }),
|
|
59
|
+
...(window.until === undefined ? {} : { until: new Date(window.until).toISOString() }),
|
|
44
60
|
...(terms.length ? { terms } : {}),
|
|
45
61
|
...(fields.has("status") ? { status: fields.get("status")?.toLowerCase() } : {}),
|
|
46
62
|
...(fields.has("error_code") ? { errorCode: fields.get("error_code") } : {}),
|
|
@@ -6,10 +6,11 @@ export interface RunSearch {
|
|
|
6
6
|
operation?: string;
|
|
7
7
|
profile?: string;
|
|
8
8
|
since?: string;
|
|
9
|
+
until?: string;
|
|
9
10
|
limit?: number;
|
|
10
11
|
page?: number;
|
|
11
12
|
}
|
|
12
|
-
export declare const searchRuns: (dir: string, { query, terms, status, errorCode, operation, profile, since, limit, page }?: RunSearch) => {
|
|
13
|
+
export declare const searchRuns: (dir: string, { query, terms, status, errorCode, operation, profile, since, until, limit, page }?: RunSearch) => {
|
|
13
14
|
items: {
|
|
14
15
|
runId: string;
|
|
15
16
|
command: string;
|
package/dist/cli/runs/search.js
CHANGED
|
@@ -32,10 +32,10 @@ const safeEvent = (event) => Object.fromEntries(FIELDS.flatMap((name) => {
|
|
|
32
32
|
}
|
|
33
33
|
return typeof value === "string" || typeof value === "number" ? [[name, value]] : [];
|
|
34
34
|
}));
|
|
35
|
-
export const searchRuns = (dir, { query = "", terms = [], status, errorCode, operation, profile, since, limit = 20, page = 1 } = {}) => {
|
|
35
|
+
export const searchRuns = (dir, { query = "", terms = [], status, errorCode, operation, profile, since, until, limit = 20, page = 1 } = {}) => {
|
|
36
36
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !Number.isSafeInteger(page) || page < 1)
|
|
37
37
|
throw new CliError("validation_error", "run search uses a limit from 1 to 100 and a positive page");
|
|
38
|
-
const window = dateWindow(
|
|
38
|
+
const window = dateWindow({ sinceTime: since, untilTime: until });
|
|
39
39
|
const from = window.since;
|
|
40
40
|
if (from !== undefined && !Number.isFinite(from))
|
|
41
41
|
throw new CliError("validation_error", "--since-time must be a valid date or ISO timestamp");
|
|
@@ -45,7 +45,8 @@ export const searchRuns = (dir, { query = "", terms = [], status, errorCode, ope
|
|
|
45
45
|
for (const run of listRuns(dir, from === undefined ? {} : { since: from })) {
|
|
46
46
|
if ((profile !== undefined && run.profile !== profile) ||
|
|
47
47
|
(status !== undefined && run.status !== status) ||
|
|
48
|
-
(from !== undefined && Date.parse(run.startedAt) < from)
|
|
48
|
+
(from !== undefined && Date.parse(run.startedAt) < from) ||
|
|
49
|
+
(window.until !== undefined && Date.parse(run.startedAt) > window.until))
|
|
49
50
|
continue;
|
|
50
51
|
const metadataMatch = (!errorCode ||
|
|
51
52
|
run.errorCode === errorCode ||
|