@wirecat/cli-core 0.343.0 → 0.345.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 +93 -0
- package/dist/cli/bot/command.js +3 -1
- package/dist/cli/bot/context.js +8 -4
- package/dist/cli/command-contract.d.ts +13 -0
- package/dist/cli/command-contract.js +1 -1
- package/dist/cli/commands-command.js +9 -12
- package/dist/cli/context.d.ts +2 -0
- package/dist/cli/guide.d.ts +52 -0
- package/dist/cli/guide.js +307 -0
- package/dist/cli/help/command-help.d.ts +30 -0
- package/dist/cli/help/command-help.js +218 -0
- package/dist/cli/help/commands-text.d.ts +9 -0
- package/dist/cli/help/commands-text.js +43 -0
- package/dist/cli/help/first-screen.d.ts +37 -0
- package/dist/cli/help/first-screen.js +109 -0
- package/dist/cli/help/sections.d.ts +19 -0
- package/dist/cli/help/sections.js +77 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/list-query.js +2 -4
- package/dist/cli/messenger/complete-command.js +2 -0
- package/dist/cli/messenger/context.js +12 -11
- package/dist/cli/messenger/conversations-command.js +4 -1
- package/dist/cli/messenger/inbox.js +1 -1
- package/dist/cli/messenger/mcp-command.js +1 -1
- package/dist/cli/messenger/messages-search-command.js +2 -2
- package/dist/cli/messenger/observed.js +15 -9
- package/dist/cli/messenger/private-people-command.js +10 -1
- package/dist/cli/messenger/review.js +1 -1
- package/dist/cli/messenger/search-command.js +3 -3
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/messenger/store-maintenance-command.js +8 -5
- package/dist/cli/moved.js +1 -1
- package/dist/cli/profile.d.ts +5 -1
- package/dist/cli/profile.js +13 -3
- package/dist/cli/program.d.ts +4 -1
- package/dist/cli/program.js +23 -8
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +1 -0
- package/dist/meetings/cli/index.d.ts +1 -0
- package/dist/meetings/cli/index.js +3 -4
- package/dist/meetings/cli/project-evidence.js +3 -3
- package/dist/meetings/cli/query-options.d.ts +6 -0
- package/dist/meetings/cli/query-options.js +21 -0
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +1 -0
- package/dist/services/project-gathering-request.d.ts +120 -0
- package/dist/services/project-gathering-request.js +217 -0
- package/dist/services/project-gathering.d.ts +89 -0
- package/dist/services/project-gathering.js +202 -0
- package/package.json +5 -4
- package/parity.json +2 -2
|
@@ -46,8 +46,8 @@ export const messagesSearchCommand = (messenger) => {
|
|
|
46
46
|
coverage says what was searched and coverage.next what would improve it.`
|
|
47
47
|
: `Search reads the local archive: what \`${messenger.app.command} serve\` has kept since the account was linked.
|
|
48
48
|
Keep serve running so new messages are searchable.`} Notes are
|
|
49
|
-
\`${messenger.app.command} search notes\`, messages and notes at once \`${messenger.app.command} search all\`; mail
|
|
50
|
-
\`memo search mail\`.
|
|
49
|
+
\`${messenger.app.command} search notes\`, messages and notes at once \`${messenger.app.command} search all\`; for mail,
|
|
50
|
+
the separate memo CLI: \`memo search mail\`.
|
|
51
51
|
A bare word looks in message text, voice transcripts, file names and file text. text:, transcript:,
|
|
52
52
|
content: (the text of files) and filename: (their names) narrow it to one —
|
|
53
53
|
\`${messenger.app.command} search messages 'content:invoice'\`. chat: narrows it to one chat; has:voice, has:file and
|
|
@@ -32,8 +32,15 @@ export const observed = (messenger, events) => {
|
|
|
32
32
|
throw error;
|
|
33
33
|
}
|
|
34
34
|
};
|
|
35
|
+
const counted = async (operation, onEvent, work) => {
|
|
36
|
+
const counts = {};
|
|
37
|
+
await timed(operation, {}, () => work((event) => {
|
|
38
|
+
counts[event.event] = (counts[event.event] ?? 0) + 1;
|
|
39
|
+
onEvent(event);
|
|
40
|
+
}), () => ({ counts }));
|
|
41
|
+
};
|
|
35
42
|
const mint = messenger.newSendId;
|
|
36
|
-
const { chats, history, contact, around } = messenger;
|
|
43
|
+
const { chats, history, contact, around, watch, listen } = messenger;
|
|
37
44
|
// A method not listed here is timed under its own name, with no ids until a line here names them.
|
|
38
45
|
return throughWrapper(messenger, {
|
|
39
46
|
self: () => messenger.self(),
|
|
@@ -75,15 +82,14 @@ export const observed = (messenger, events) => {
|
|
|
75
82
|
...(options.threadId ? { thread: options.threadId } : {}),
|
|
76
83
|
},
|
|
77
84
|
}, () => messenger.send(chatId, text, options), (sent) => ({ ids: { message_id: sent.message.id } })),
|
|
78
|
-
...(
|
|
85
|
+
...(watch
|
|
86
|
+
? {
|
|
87
|
+
watch: (onEvent, signal, onReady) => counted("messages.watch", onEvent, (counting) => watch.call(messenger, counting, signal, onReady)),
|
|
88
|
+
}
|
|
89
|
+
: {}),
|
|
90
|
+
...(listen
|
|
79
91
|
? {
|
|
80
|
-
|
|
81
|
-
const counts = {};
|
|
82
|
-
await timed("messages.watch", {}, () => messenger.watch?.((event) => {
|
|
83
|
-
counts[event.event] = (counts[event.event] ?? 0) + 1;
|
|
84
|
-
onEvent(event);
|
|
85
|
-
}, signal, onReady) ?? Promise.resolve(), () => ({ counts }));
|
|
86
|
-
},
|
|
92
|
+
listen: (onEvent, signal) => counted("adapter.listen", onEvent, (counting) => listen.call(messenger, counting, signal)),
|
|
87
93
|
}
|
|
88
94
|
: {}),
|
|
89
95
|
logout: () => timed("session.logout", {}, () => messenger.logout()),
|
|
@@ -15,6 +15,7 @@ const everyNote = (command, items) => listOf(command, { items, limit: Math.max(1
|
|
|
15
15
|
export const privatePeopleCommands = (messenger) => {
|
|
16
16
|
const alias = new Command("alias").description("a private local display name in the selected account");
|
|
17
17
|
annotate(alias.command("set"), { effects: LOCAL_WRITE })
|
|
18
|
+
.description("your own name for a person in this account; kept on this machine, never sent")
|
|
18
19
|
.argument("<person>")
|
|
19
20
|
.argument("<alias>")
|
|
20
21
|
.action(async function (person, value) {
|
|
@@ -23,6 +24,7 @@ export const privatePeopleCommands = (messenger) => {
|
|
|
23
24
|
renderAnswer(this, context, itemOf(snakeKeys(await context.withServices((services) => services.privatePeople.alias(person, value)))));
|
|
24
25
|
});
|
|
25
26
|
annotate(alias.command("rm"), { effects: LOCAL_WRITE })
|
|
27
|
+
.description("drop your local name for a person; their own name shows again")
|
|
26
28
|
.argument("<person>")
|
|
27
29
|
.action(async function (person) {
|
|
28
30
|
const context = messengerContext(this, messenger);
|
|
@@ -30,13 +32,16 @@ export const privatePeopleCommands = (messenger) => {
|
|
|
30
32
|
renderAnswer(this, context, itemOf(snakeKeys(await context.withServices((services) => services.privatePeople.alias(person, null)))));
|
|
31
33
|
});
|
|
32
34
|
const notes = new Command("notes").description("your private notes on a stored contact, the same in every account that sees them");
|
|
33
|
-
withQuery(annotate(notes.command("list"), { effects: LOCAL_READ })
|
|
35
|
+
withQuery(annotate(notes.command("list"), { effects: LOCAL_READ })
|
|
36
|
+
.description("every private note you keep on a person, oldest first")
|
|
37
|
+
.argument("<person>"), NOTES_QUERY).action(async function (person, words) {
|
|
34
38
|
const context = messengerContext(this, messenger);
|
|
35
39
|
listQuery(typedQuery(words), NOTES_QUERY, "contacts notes list");
|
|
36
40
|
const items = await context.withServices((services) => services.privatePeople.notes(person));
|
|
37
41
|
renderAnswer(this, context, everyNote("contacts notes list", items));
|
|
38
42
|
});
|
|
39
43
|
annotate(notes.command("show"), { effects: LOCAL_READ, output: { entity: "contact_note" } })
|
|
44
|
+
.description("one private note on a person, with its revision for edit")
|
|
40
45
|
.argument("<person>")
|
|
41
46
|
.argument("<id>")
|
|
42
47
|
.action(async function (person, id) {
|
|
@@ -48,6 +53,9 @@ export const privatePeopleCommands = (messenger) => {
|
|
|
48
53
|
effects: LOCAL_WRITE,
|
|
49
54
|
output: { entity: "contact_note" },
|
|
50
55
|
}), { name: "text", description: "the note's text" })
|
|
56
|
+
.description(name === "add"
|
|
57
|
+
? "add a private note on a person from stdin or --file; never sent"
|
|
58
|
+
: "replace a note's text; --revision must match the one you read")
|
|
51
59
|
.argument("<person>")
|
|
52
60
|
.addOption(ownerOnlyOption("--file <path>", "read note text from a file; omitted or - reads stdin"));
|
|
53
61
|
if (name === "edit")
|
|
@@ -67,6 +75,7 @@ export const privatePeopleCommands = (messenger) => {
|
|
|
67
75
|
});
|
|
68
76
|
}
|
|
69
77
|
annotate(notes.command("remove"), { effects: LOCAL_WRITE })
|
|
78
|
+
.description("delete one private note on a person")
|
|
70
79
|
.argument("<person>")
|
|
71
80
|
.argument("<id>")
|
|
72
81
|
.action(async function (person, id) {
|
|
@@ -26,7 +26,7 @@ export const unansweredHours = (value, flag = "--unanswered") => value === true
|
|
|
26
26
|
*/
|
|
27
27
|
export const reviewCommand = (messenger) => annotate(new Command("review"), {
|
|
28
28
|
effects: REMOTE_READ,
|
|
29
|
-
output: { entity: "message", view: "
|
|
29
|
+
output: { entity: "message", view: "summary" },
|
|
30
30
|
query: REVIEW_QUERY,
|
|
31
31
|
})
|
|
32
32
|
.description("every message, yours too, in chats that changed since a point — for reviewing who owes what")
|
|
@@ -15,7 +15,7 @@ import { topicsSearchCommand } from "./topics-command.js";
|
|
|
15
15
|
const onlyOf = (value) => {
|
|
16
16
|
const chosen = value.split(",").map((one) => one.trim());
|
|
17
17
|
if (chosen.includes("mail"))
|
|
18
|
-
throw new CliError("validation_error", "mail is memo
|
|
18
|
+
throw new CliError("validation_error", "mail is searched by the separate memo CLI: install @wirecat/cli-memo, then `memo search mail …`");
|
|
19
19
|
const unknown = chosen.find((one) => !SOURCE_SEARCHED.includes(one));
|
|
20
20
|
if (unknown !== undefined || chosen.length === 0)
|
|
21
21
|
throw new CliError("validation_error", `--only takes ${SOURCE_SEARCHED.join(", ")}, separated by commas; meetings when --source names a meeting account`);
|
|
@@ -37,7 +37,7 @@ const allCommand = (messenger) => {
|
|
|
37
37
|
? "search messenger messages and notes in the local store, and the messenger's server for messages " +
|
|
38
38
|
"(--backend; notes are local only)"
|
|
39
39
|
: "search messenger messages and notes in the local store") +
|
|
40
|
-
", and a meeting account's transcripts when --source names one — best match first; not mail (memo search mail), " +
|
|
40
|
+
", and a meeting account's transcripts when --source names one — best match first; not mail (the separate memo CLI searches it: memo search mail), " +
|
|
41
41
|
"tasks, people, memories or projects. Use it when you do not know where something was written; search messages " +
|
|
42
42
|
"or notes reads one kind with all its fields")
|
|
43
43
|
.argument("<query...>", 'strict Lucene query: words, "phrases", AND/OR/NOT, field groups and date ranges')
|
|
@@ -149,7 +149,7 @@ const notesCommand = (messenger) => annotate(new Command("notes"), { effects: LO
|
|
|
149
149
|
* hierarchy"). `topics` only where the CLI has forum topics.
|
|
150
150
|
*/
|
|
151
151
|
export const searchCommand = (messenger, { topics = false } = {}) => {
|
|
152
|
-
const search = new Command("search").description("find things by text: one resource when you know it (messages, notes), or search all across messages and notes; mail
|
|
152
|
+
const search = new Command("search").description("find things by text: one resource when you know it (messages, notes), or search all across messages and notes; for mail, the separate memo CLI: memo search mail");
|
|
153
153
|
search
|
|
154
154
|
.addCommand(allCommand(messenger))
|
|
155
155
|
.addCommand(messagesSearchCommand(messenger))
|
|
@@ -2,9 +2,9 @@ import { Command } from "commander";
|
|
|
2
2
|
import type { Messenger } from "./context.js";
|
|
3
3
|
/** `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. */
|
|
4
4
|
export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
|
|
5
|
-
error?: undefined;
|
|
6
5
|
path: string;
|
|
7
6
|
exists: boolean;
|
|
7
|
+
error?: undefined;
|
|
8
8
|
} | {
|
|
9
9
|
path: string;
|
|
10
10
|
exists: boolean;
|
|
@@ -569,8 +569,10 @@ const restoreFrom = async (command, messenger, backup, given) => {
|
|
|
569
569
|
const schema = await backupSchema(backup);
|
|
570
570
|
const stamp = stampNow();
|
|
571
571
|
const kept = existsSync(path) ? `${path}.before-restore-${stamp}` : null;
|
|
572
|
-
if (kept)
|
|
573
|
-
|
|
572
|
+
if (kept) {
|
|
573
|
+
const waitMs = environmentOf(command).storeLockWaitMs;
|
|
574
|
+
await quiesce(path, messenger, "restore", (message) => renderer.warn(message), waitMs);
|
|
575
|
+
}
|
|
574
576
|
else
|
|
575
577
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
576
578
|
const incoming = `${path}.restoring-${stamp}`;
|
|
@@ -614,12 +616,13 @@ const backupSchema = async (backup) => {
|
|
|
614
616
|
}
|
|
615
617
|
return schema;
|
|
616
618
|
};
|
|
619
|
+
const STORE_LOCK_WAIT_MS = 1_000;
|
|
617
620
|
/**
|
|
618
621
|
* No other process has the file open, no write is under way, and the write-ahead log is folded in,
|
|
619
622
|
* so the file set aside is whole on its own. In WAL mode SQLite cannot say who has the file open —
|
|
620
623
|
* `EXCLUSIVE` behaves as `IMMEDIATE` — so that is asked of the system.
|
|
621
624
|
*/
|
|
622
|
-
const quiesce = async (path, messenger, then, warn) => {
|
|
625
|
+
const quiesce = async (path, messenger, then, warn, waitMs = STORE_LOCK_WAIT_MS) => {
|
|
623
626
|
const { command } = messenger.app;
|
|
624
627
|
const files = [path, `${path}-wal`, `${path}-shm`]
|
|
625
628
|
.filter((file) => existsSync(file))
|
|
@@ -634,7 +637,7 @@ const quiesce = async (path, messenger, then, warn) => {
|
|
|
634
637
|
}
|
|
635
638
|
const database = await openCache(path);
|
|
636
639
|
try {
|
|
637
|
-
database.exec(
|
|
640
|
+
database.exec(`PRAGMA busy_timeout = ${waitMs}`);
|
|
638
641
|
database.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
639
642
|
try {
|
|
640
643
|
database.exec("BEGIN IMMEDIATE");
|
|
@@ -673,7 +676,7 @@ const resetCommand = (messenger) => annotate(new Command("reset"), { effects: LO
|
|
|
673
676
|
if (!/^\s*y(es)?\s*$/i.test(answer))
|
|
674
677
|
throw new CliError("cancelled", "cancelled — the store is unchanged");
|
|
675
678
|
}
|
|
676
|
-
await quiesce(path, messenger, "reset", (message) => renderer.warn(message));
|
|
679
|
+
await quiesce(path, messenger, "reset", (message) => renderer.warn(message), environmentOf(this).storeLockWaitMs);
|
|
677
680
|
const backup = keep ? `${path}.backup-${stampNow()}` : null;
|
|
678
681
|
let backedUp = null;
|
|
679
682
|
if (backup) {
|
package/dist/cli/moved.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* "unknown command"; a path the program still mounts never reaches it.
|
|
5
5
|
*/
|
|
6
6
|
export const MOVED_TO_MEMO = {
|
|
7
|
-
"search mail": { what: "mail is memo
|
|
7
|
+
"search mail": { what: "mail is searched by the separate memo CLI", memo: "memo search mail …" },
|
|
8
8
|
};
|
|
9
9
|
export const movedPointer = (path) => {
|
|
10
10
|
const moved = MOVED_TO_MEMO[path];
|
package/dist/cli/profile.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { Command } from "commander";
|
|
|
2
2
|
/** How the command line names this profile: always the first word, since none is assumed. */
|
|
3
3
|
export declare const asFirstWord: (profile: string) => string;
|
|
4
4
|
export declare const usableProfileName: (profile: string) => string;
|
|
5
|
+
/** The top-level commands that never read a profile; every other one is `<tool> <profile> …`. */
|
|
6
|
+
export declare const PROFILE_FREE: ReadonlySet<string>;
|
|
5
7
|
/**
|
|
6
8
|
* Every word that means a command at the top level. Taken off the built program rather than
|
|
7
9
|
* written out, because a list written out goes stale the day a command is added — and `help` is in
|
|
@@ -13,10 +15,12 @@ export declare const commandWords: (program: Command) => Set<string>;
|
|
|
13
15
|
* `TG_PROFILE=personal tg chats list`, with no flag in either. An option is never a profile, so
|
|
14
16
|
* `tg --json chats list` still works.
|
|
15
17
|
*/
|
|
16
|
-
export declare const liftProfile: (argv: string[], words: Set<string>) => {
|
|
18
|
+
export declare const liftProfile: (argv: string[], words: Set<string>, valued?: ReadonlySet<string>) => {
|
|
17
19
|
profile?: string;
|
|
18
20
|
rest: string[];
|
|
19
21
|
};
|
|
22
|
+
/** The root's options that take the next word as their value: `--timeout 30s`. */
|
|
23
|
+
export declare const valuedOptions: (program: Command) => Set<string>;
|
|
20
24
|
/**
|
|
21
25
|
* Refused at creation, which is the only moment it can still be explained: a profile named after a
|
|
22
26
|
* command could never be selected, because `tg chats` has to mean the command.
|
package/dist/cli/profile.js
CHANGED
|
@@ -14,6 +14,8 @@ export const usableProfileName = (profile) => {
|
|
|
14
14
|
}
|
|
15
15
|
return profile;
|
|
16
16
|
};
|
|
17
|
+
/** The top-level commands that never read a profile; every other one is `<tool> <profile> …`. */
|
|
18
|
+
export const PROFILE_FREE = new Set(["commands", "complete", "guide", "help", "skill", "upgrade"]);
|
|
17
19
|
/**
|
|
18
20
|
* Every word that means a command at the top level. Taken off the built program rather than
|
|
19
21
|
* written out, because a list written out goes stale the day a command is added — and `help` is in
|
|
@@ -25,12 +27,20 @@ export const commandWords = (program) => new Set(["help", ...program.commands.fl
|
|
|
25
27
|
* `TG_PROFILE=personal tg chats list`, with no flag in either. An option is never a profile, so
|
|
26
28
|
* `tg --json chats list` still works.
|
|
27
29
|
*/
|
|
28
|
-
export const liftProfile = (argv, words) => {
|
|
29
|
-
|
|
30
|
+
export const liftProfile = (argv, words, valued = new Set()) => {
|
|
31
|
+
// `tg --json personal chats list`: options before the profile are skipped over, with the value an option takes.
|
|
32
|
+
let at = 0;
|
|
33
|
+
while (argv[at]?.startsWith("-") && argv[at] !== "--")
|
|
34
|
+
at += valued.has(argv[at]) ? 2 : 1;
|
|
35
|
+
const first = argv[at];
|
|
30
36
|
if (first === undefined || first.startsWith("-") || words.has(first))
|
|
31
37
|
return { rest: argv };
|
|
32
|
-
return { profile: first, rest };
|
|
38
|
+
return { profile: first, rest: [...argv.slice(0, at), ...argv.slice(at + 1)] };
|
|
33
39
|
};
|
|
40
|
+
/** The root's options that take the next word as their value: `--timeout 30s`. */
|
|
41
|
+
export const valuedOptions = (program) => new Set(program.options
|
|
42
|
+
.filter((option) => option.required)
|
|
43
|
+
.flatMap((option) => [option.long, option.short].filter((flag) => flag !== undefined)));
|
|
34
44
|
/**
|
|
35
45
|
* Refused at creation, which is the only moment it can still be explained: a profile named after a
|
|
36
46
|
* command could never be selected, because `tg chats` has to mean the command.
|
package/dist/cli/program.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { type AppIdentity } from "../ports/app.js";
|
|
3
3
|
import { type BaseEnvironment } from "./context.js";
|
|
4
|
+
import { type FirstScreen } from "./help/first-screen.js";
|
|
4
5
|
import { type GlobalFlags, type ResolveOptions, type Settings } from "./settings.js";
|
|
5
6
|
export interface ProgramDefinition {
|
|
6
7
|
app: AppIdentity;
|
|
@@ -11,6 +12,8 @@ export interface ProgramDefinition {
|
|
|
11
12
|
resolveSettings: (flags?: GlobalFlags, options?: ResolveOptions) => Pick<Settings, "profile" | "keepFailedRuns" | "keepRunsForDays" | "skillHint">;
|
|
12
13
|
};
|
|
13
14
|
configure?: (program: Command) => void;
|
|
15
|
+
/** The tool's pitch and "what you can do" lines on `--help` and a bare run. */
|
|
16
|
+
firstScreen?: FirstScreen;
|
|
14
17
|
prepare?: (program: Command, environment: RunOptions) => void | Promise<void>;
|
|
15
18
|
onFailure?: (error: unknown, program: Command) => void | Promise<void>;
|
|
16
19
|
}
|
|
@@ -23,7 +26,7 @@ export interface ProgramOptions {
|
|
|
23
26
|
* The command tree, built fresh on each call. Commander is global state by default —
|
|
24
27
|
* `exitOverride` and the write hooks turn it into a value a test can drive.
|
|
25
28
|
*/
|
|
26
|
-
export declare const createProgram: ({ app, commands, configure }: ProgramDefinition, { out, err }?: ProgramOptions) => Command;
|
|
29
|
+
export declare const createProgram: ({ app, commands, configure, firstScreen }: ProgramDefinition, { out, err }?: ProgramOptions) => Command;
|
|
27
30
|
export type RunOptions = BaseEnvironment & Record<string, unknown>;
|
|
28
31
|
/**
|
|
29
32
|
* An interactive command run without a terminal — an agent's `session start --qr-file` — waits on a person scanning a
|
package/dist/cli/program.js
CHANGED
|
@@ -2,7 +2,7 @@ import { join } from "node:path";
|
|
|
2
2
|
import { CliError, errorObjectOf, errorRecordOf, exitCodeFor, exitCodeOfError, processStreams, renderError, resolvePaths, } from "@wirecat/cli-base";
|
|
3
3
|
import { metaOf, refuseJoinedPath } from "@wirecat/cli-base/commands";
|
|
4
4
|
import { skillHint } from "@wirecat/cli-base/skill";
|
|
5
|
-
import { Command } from "commander";
|
|
5
|
+
import { Command, Help } from "commander";
|
|
6
6
|
import { relayCommand } from "../background/relay.js";
|
|
7
7
|
import { failingNotice, scheduledRuns } from "../background/schedules.js";
|
|
8
8
|
import { parseDuration } from "../domain/duration.js";
|
|
@@ -18,11 +18,13 @@ import { refuseWriteBeforeConnecting } from "./early-refusal.js";
|
|
|
18
18
|
import { changesAuthorityNow, isLogin, isStoreChange, judgedEffects, judgedTargets, STORE_CHANGE_REFUSAL, } from "./effects.js";
|
|
19
19
|
import { itemOf, renderAnswer, snakeKeys } from "./envelope.js";
|
|
20
20
|
import { byteCount, DEFAULT_COMMAND_MS, DEFAULT_OUTPUT_BYTES, execution } from "./execution.js";
|
|
21
|
+
import { installHelp } from "./help/first-screen.js";
|
|
22
|
+
import { sectionCoreGroups } from "./help/sections.js";
|
|
21
23
|
import { MAX_BUFFERED_INPUT, provideInputPolicy } from "./input-policy.js";
|
|
22
24
|
import { checkJson, liftJson } from "./json-fields.js";
|
|
23
25
|
import { movedPath, movedPointer } from "./moved.js";
|
|
24
26
|
import { PreviewComplete, preview } from "./preview.js";
|
|
25
|
-
import { commandWords, liftProfile } from "./profile.js";
|
|
27
|
+
import { commandWords, liftProfile, PROFILE_FREE, valuedOptions } from "./profile.js";
|
|
26
28
|
import { absolutePaths, relayTarget } from "./relay-route.js";
|
|
27
29
|
import { settingsFor } from "./settings.js";
|
|
28
30
|
import { commandAt } from "./steps.js";
|
|
@@ -30,7 +32,7 @@ import { commandAt } from "./steps.js";
|
|
|
30
32
|
* The command tree, built fresh on each call. Commander is global state by default —
|
|
31
33
|
* `exitOverride` and the write hooks turn it into a value a test can drive.
|
|
32
34
|
*/
|
|
33
|
-
export const createProgram = ({ app, commands, configure }, { out, err } = {}) => {
|
|
35
|
+
export const createProgram = ({ app, commands, configure, firstScreen }, { out, err } = {}) => {
|
|
34
36
|
const program = new Command();
|
|
35
37
|
program
|
|
36
38
|
.name(app.command)
|
|
@@ -51,8 +53,8 @@ export const createProgram = ({ app, commands, configure }, { out, err } = {}) =
|
|
|
51
53
|
.option("--no-input", "never prompt or open interactive login; piped input remains available")
|
|
52
54
|
.option("--max-input-bytes <bytes>", "maximum buffered input bytes (default: 16777216)")
|
|
53
55
|
.option("--max-output-bytes <bytes>", "maximum machine output bytes (default: 4194304; 0 disables)")
|
|
54
|
-
.option("--brief", "found messages
|
|
55
|
-
.option("--full", "found messages
|
|
56
|
+
.option("--brief", "found messages as reference, title, time, sender and excerpt only")
|
|
57
|
+
.option("--full", "found messages with their whole text and every provider field")
|
|
56
58
|
.option("--dry-run", "preview parsed arguments and permissions before running the action")
|
|
57
59
|
.option("--yes", "go ahead without the question an ask level puts before a write")
|
|
58
60
|
.option("--record", "keep this run — ids and timings, never message content")
|
|
@@ -61,6 +63,16 @@ export const createProgram = ({ app, commands, configure }, { out, err } = {}) =
|
|
|
61
63
|
for (const command of commands())
|
|
62
64
|
program.addCommand(command);
|
|
63
65
|
configure?.(program);
|
|
66
|
+
sectionCoreGroups(program);
|
|
67
|
+
installHelp(program, { pitch: app.description, ...firstScreen });
|
|
68
|
+
// Commander builds a usage line from the command tree, where the profile is not a word.
|
|
69
|
+
for (const top of program.commands) {
|
|
70
|
+
if (PROFILE_FREE.has(top.name()))
|
|
71
|
+
continue;
|
|
72
|
+
forEachCommand(top, (command) => command.configureHelp({
|
|
73
|
+
commandUsage: (shown) => `${app.command} <profile>${new Help().commandUsage(shown).slice(app.command.length)}`,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
64
76
|
// Depth-first: Commander does not pass `configureOutput` down to a command added with
|
|
65
77
|
// `addCommand`, so `tg messages --help` would write to the real terminal.
|
|
66
78
|
if (out || err) {
|
|
@@ -327,7 +339,7 @@ export const run = async (argv, definition, options = {}) => {
|
|
|
327
339
|
forEachCommand(program, (child) => child.exitOverride());
|
|
328
340
|
// Before commander, not inside it: commander has no hook that runs before it decides which
|
|
329
341
|
// subcommand it is looking at.
|
|
330
|
-
const { profile, rest: lifted } = liftProfile(argv, commandWords(program));
|
|
342
|
+
const { profile, rest: lifted } = liftProfile(argv, commandWords(program), valuedOptions(program));
|
|
331
343
|
// `wa 'chats list' --help` reads "chats list" as a profile and prints the root help with exit 0.
|
|
332
344
|
try {
|
|
333
345
|
// Only before `--`: what follows is a positional, an agent's text among them, never a command word.
|
|
@@ -364,7 +376,9 @@ export const run = async (argv, definition, options = {}) => {
|
|
|
364
376
|
try {
|
|
365
377
|
await control.race(() => program.parseAsync(rest, { from: "user" }));
|
|
366
378
|
const code = exit.code ?? (process.exitCode === undefined ? 0 : Number(process.exitCode));
|
|
367
|
-
const hint = code === 0 && rest[0] !== "commands" && !rest.includes("--quiet")
|
|
379
|
+
const hint = code === 0 && rest[0] !== "commands" && rest[0] !== "help" && !rest.includes("--quiet")
|
|
380
|
+
? hintFor(definition, rest, options)
|
|
381
|
+
: undefined;
|
|
368
382
|
if (hint)
|
|
369
383
|
streams.diagnostic(hint);
|
|
370
384
|
return code;
|
|
@@ -384,7 +398,8 @@ export const run = async (argv, definition, options = {}) => {
|
|
|
384
398
|
logVersion(argvLog);
|
|
385
399
|
if (error.exitCode === 0)
|
|
386
400
|
return 0;
|
|
387
|
-
|
|
401
|
+
// Commander puts its "(Did you mean …?)" on a line of its own; an error is one line.
|
|
402
|
+
let message = error.message.replace(/^error: /, "").replace(/\s*\n\s*/g, " ");
|
|
388
403
|
if (error.code === "commander.help") {
|
|
389
404
|
if (reporting.tty ?? process.stdout.isTTY === true) {
|
|
390
405
|
streams.diagnostic(help.replace(/\n$/, ""));
|
package/dist/mcp/index.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export { type AgentAnswer, type AgentContext, type AgentProgram, type AgentTool,
|
|
|
2
2
|
export { type BotServerOptions, createBotServer, type RunBotCommand } from "./bot/server.js";
|
|
3
3
|
export { revokeAll } from "./http/oauth.js";
|
|
4
4
|
export { type HttpOptions, MCP_PATH, serveOverHttp } from "./http/serve.js";
|
|
5
|
+
export { type McpCommand, registerCommands } from "./surface.js";
|
package/dist/mcp/index.js
CHANGED
|
@@ -3,3 +3,4 @@ export { runCommandForAgent, toolsFromCommands, } from "./agent.js";
|
|
|
3
3
|
export { createBotServer } from "./bot/server.js";
|
|
4
4
|
export { revokeAll } from "./http/oauth.js";
|
|
5
5
|
export { MCP_PATH, serveOverHttp } from "./http/serve.js";
|
|
6
|
+
export { registerCommands } from "./surface.js";
|
|
@@ -14,4 +14,5 @@ export interface MeetingCommandDeps {
|
|
|
14
14
|
export declare const addMeetingCommands: (program: Command, deps: MeetingCommandDeps) => void;
|
|
15
15
|
export { addMeetingPassageCommand, createMeetingPassageCommand, type MeetingPassageFlags, meetingPassageRequest, } from "./passage.js";
|
|
16
16
|
export { addMeetingProjectEvidenceCommand } from "./project-evidence.js";
|
|
17
|
+
export { queryWordsAfterOptions } from "./query-options.js";
|
|
17
18
|
export { addMeetingSearchCommand, createMeetingSearchCommand, type MeetingHitSearch, type MeetingHitSearchOptions, type MeetingSearchFlags, meetingHitSearchRequest, meetingSearchArguments, validateMeetingHitSearch, } from "./search.js";
|
|
@@ -7,6 +7,7 @@ import { MEETING_QUERY } from "../item.js";
|
|
|
7
7
|
import { pull } from "../pull.js";
|
|
8
8
|
import { listed, MEETING_QUERY_GUIDE, MeetingError, meetingDetailsAnswer, meetingPeople, meetingSummaryAnswer, meetingsList, meetingsQuery, meetingTranscriptAnswer, } from "../reads.js";
|
|
9
9
|
import { SAVE_HELP } from "../save-hints.js";
|
|
10
|
+
import { queryWordsAfterOptions } from "./query-options.js";
|
|
10
11
|
const integer = (text) => {
|
|
11
12
|
if (!/^[1-9]\d*$/.test(text) || !Number.isSafeInteger(Number(text)))
|
|
12
13
|
throw new InvalidArgumentError("expected a positive integer");
|
|
@@ -39,10 +40,7 @@ export const addMeetingCommands = (program, deps) => {
|
|
|
39
40
|
const opts = command.optsWithGlobals();
|
|
40
41
|
deps.write(value, opts.json ? "json" : opts.jsonl ? "jsonl" : "text");
|
|
41
42
|
};
|
|
42
|
-
const list = group
|
|
43
|
-
.command("list")
|
|
44
|
-
.description("stored meetings, newest first")
|
|
45
|
-
.allowUnknownOption()
|
|
43
|
+
const list = queryWordsAfterOptions(group.command("list").description("stored meetings, newest first"))
|
|
46
44
|
.argument("[query...]", "field:value terms that narrow the list — the fields are below")
|
|
47
45
|
.addHelpText("after", `\n${fieldsHelp(MEETING_QUERY, MEETING_QUERY_GUIDE)}`);
|
|
48
46
|
annotate(list, { effects: LOCAL_READ, query: defineQuery(MEETING_QUERY), output: { entity: "meeting" } });
|
|
@@ -139,4 +137,5 @@ export const addMeetingCommands = (program, deps) => {
|
|
|
139
137
|
};
|
|
140
138
|
export { addMeetingPassageCommand, createMeetingPassageCommand, meetingPassageRequest, } from "./passage.js";
|
|
141
139
|
export { addMeetingProjectEvidenceCommand } from "./project-evidence.js";
|
|
140
|
+
export { queryWordsAfterOptions } from "./query-options.js";
|
|
142
141
|
export { addMeetingSearchCommand, createMeetingSearchCommand, meetingHitSearchRequest, meetingSearchArguments, validateMeetingHitSearch, } from "./search.js";
|
|
@@ -3,6 +3,7 @@ import { annotate } from "@wirecat/cli-base/commands";
|
|
|
3
3
|
import { fieldsHelp } from "@wirecat/cli-base/query";
|
|
4
4
|
import { readMeetingProjectEvidence } from "../project-evidence.js";
|
|
5
5
|
import { MEETING_PROJECT_EVIDENCE_QUERY } from "../project-evidence-query.js";
|
|
6
|
+
import { queryWordsAfterOptions } from "./query-options.js";
|
|
6
7
|
const integer = (value) => {
|
|
7
8
|
if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value)))
|
|
8
9
|
throw new CliError("validation_error", "expected a positive safe integer");
|
|
@@ -14,12 +15,11 @@ export const addMeetingProjectEvidenceCommand = (program, deps) => {
|
|
|
14
15
|
throw new CliError("configuration_error", "meeting command group is unavailable");
|
|
15
16
|
const projects = group.commands.find((command) => command.name() === "projects") ??
|
|
16
17
|
group.command("projects").description("project-name evidence in stored meetings");
|
|
17
|
-
return annotate(projects
|
|
18
|
+
return annotate(queryWordsAfterOptions(projects
|
|
18
19
|
.command("evidence")
|
|
19
|
-
.description("find cited project-name and alias evidence in the local meeting index; does not refresh it or confirm membership")
|
|
20
|
+
.description("find cited project-name and alias evidence in the local meeting index; does not refresh it or confirm membership"))
|
|
20
21
|
.argument("<project>", "existing project name, key or explicit alias")
|
|
21
22
|
.argument("[query...]", "exact ID fields and a UTC started_at range, joined by AND")
|
|
22
|
-
.allowUnknownOption()
|
|
23
23
|
.addHelpText("after", `\n${fieldsHelp(MEETING_PROJECT_EVIDENCE_QUERY)}`)
|
|
24
24
|
.option("--limit <n>", "hits in one name page, 1 through 1000 (default 100)", integer)
|
|
25
25
|
.option("--cursor <cursor>", "continue with the same project and frozen selection from next")
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* Lets a query's `-kind:channel` through as a negated term while a mistyped `--limt` still fails as an unknown
|
|
4
|
+
* option, with commander's suggestion; after `--` every word is query.
|
|
5
|
+
*/
|
|
6
|
+
export declare const queryWordsAfterOptions: (command: Command) => Command;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lets a query's `-kind:channel` through as a negated term while a mistyped `--limt` still fails as an unknown
|
|
3
|
+
* option, with commander's suggestion; after `--` every word is query.
|
|
4
|
+
*/
|
|
5
|
+
export const queryWordsAfterOptions = (command) => {
|
|
6
|
+
command.allowUnknownOption();
|
|
7
|
+
const parse = command.parseOptions.bind(command);
|
|
8
|
+
command.parseOptions = (args) => {
|
|
9
|
+
const parsed = parse(args);
|
|
10
|
+
const end = parsed.unknown.indexOf("--");
|
|
11
|
+
const typo = parsed.unknown.slice(0, end < 0 ? undefined : end).find((word) => word.startsWith("--"));
|
|
12
|
+
if (typo !== undefined) {
|
|
13
|
+
command.allowUnknownOption(false);
|
|
14
|
+
command.unknownOption(typo);
|
|
15
|
+
}
|
|
16
|
+
if (end >= 0)
|
|
17
|
+
parsed.unknown.splice(end, 1);
|
|
18
|
+
return parsed;
|
|
19
|
+
};
|
|
20
|
+
return command;
|
|
21
|
+
};
|