@wirecat/cli-core 0.462.0 → 0.464.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 +106 -0
- package/dist/background/after-upgrade.d.ts +51 -0
- package/dist/background/after-upgrade.js +245 -0
- package/dist/background/budget.d.ts +37 -0
- package/dist/background/budget.js +48 -0
- package/dist/background/index.d.ts +1 -0
- package/dist/background/index.js +1 -0
- package/dist/calendar/busy.d.ts +68 -0
- package/dist/calendar/busy.js +148 -0
- package/dist/calendar/cli/index.d.ts +7 -0
- package/dist/calendar/cli/index.js +94 -12
- package/dist/calendar/index.d.ts +3 -2
- package/dist/calendar/index.js +2 -1
- package/dist/calendar/mcp/index.d.ts +55 -13
- package/dist/calendar/mcp/index.js +19 -4
- package/dist/calendar/recurrence.d.ts +10 -0
- package/dist/calendar/recurrence.js +65 -0
- package/dist/calendar/source.d.ts +19 -0
- package/dist/calendar/sync.d.ts +86 -7
- package/dist/calendar/sync.js +271 -105
- package/dist/calendar/testing/source-contract.js +14 -0
- package/dist/calendar/testing/source.d.ts +6 -0
- package/dist/calendar/testing/source.js +30 -2
- package/dist/calendar/testing/sync-contract.d.ts +2 -1
- package/dist/calendar/testing/sync-contract.js +80 -5
- package/dist/calendar/testing/writer-contract.d.ts +2 -1
- package/dist/calendar/testing/writer-contract.js +46 -2
- package/dist/calendar/testing/writer.js +1 -1
- package/dist/calendar/writer.d.ts +7 -2
- package/dist/calendar/writes.d.ts +12 -2
- package/dist/calendar/writes.js +32 -6
- package/dist/cli/bot/mcp.js +11 -3
- package/dist/cli/context.d.ts +8 -0
- package/dist/cli/context.js +15 -0
- package/dist/cli/execution.d.ts +7 -0
- package/dist/cli/execution.js +7 -0
- package/dist/cli/guide.js +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/mcp-server-command.d.ts +9 -4
- package/dist/cli/mcp-server-command.js +32 -25
- package/dist/cli/meeting-preparation-answer.d.ts +1 -0
- package/dist/cli/meeting-preparation-projects.d.ts +1 -0
- package/dist/cli/meeting-preparation-read.d.ts +1 -0
- package/dist/cli/messenger/chat-answers.js +1 -0
- package/dist/cli/messenger/mcp-command.d.ts +3 -4
- package/dist/cli/messenger/mcp-command.js +42 -26
- package/dist/cli/messenger/search-answers.js +29 -4
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/program.js +15 -5
- package/dist/cli/upgrade-command.d.ts +7 -2
- package/dist/cli/upgrade-command.js +65 -9
- package/dist/entities/fields.d.ts +2 -0
- package/dist/entities/fields.js +2 -0
- package/dist/entities/hit.d.ts +1 -1
- package/dist/entities/hit.js +3 -3
- package/dist/entities/index.d.ts +10 -800
- package/dist/entities/index.js +37 -31
- package/dist/entities/meeting-preparation.d.ts +1 -0
- package/dist/entities/topic.d.ts +1 -0
- package/dist/entities/topic.js +2 -1
- package/dist/errors/reasons.js +4 -0
- package/dist/kit/mcp-stdio.d.ts +2 -2
- package/dist/kit/mcp-stdio.js +2 -2
- package/dist/mcp/http/activity.d.ts +1 -1
- package/dist/mcp/http/activity.js +1 -1
- package/dist/mcp/http/oauth.js +1 -1
- package/dist/mcp/http/state.d.ts +14 -0
- package/dist/mcp/http/state.js +16 -0
- package/dist/mcp/meeting-preparation.d.ts +1 -0
- package/dist/meetings/answers.d.ts +2 -0
- package/dist/meetings/answers.js +3 -0
- package/dist/meetings/mcp/search.d.ts +1 -0
- package/dist/meetings/project-evidence-answer.d.ts +1 -0
- package/dist/ports/app.d.ts +5 -0
- package/dist/services/package-upgrade.d.ts +9 -0
- package/dist/services/package-upgrade.js +8 -1
- package/package.json +3 -3
- package/parity.json +30 -22
|
@@ -19,31 +19,36 @@ import { nextStep } from "../steps.js";
|
|
|
19
19
|
import { recalledAccount } from "./accounts.js";
|
|
20
20
|
import { messengerContext } from "./context.js";
|
|
21
21
|
const DEFAULT_PORT = MCP_HTTP_DEFAULT_PORT;
|
|
22
|
-
/** The token file of `<tool> mcp --http` for every profile: no profile name can start with `_`. */
|
|
22
|
+
/** The token file of `<tool> mcp serve --http` for every profile: no profile name can start with `_`. */
|
|
23
23
|
export const ALL_PROFILES = "_all";
|
|
24
|
-
/** Where `mcp --http` keeps the hashes of the tokens it issued — per profile, in the state folder. */
|
|
24
|
+
/** Where `mcp serve --http` keeps the hashes of the tokens it issued — per profile, in the state folder. */
|
|
25
25
|
export const httpTokenFile = (app, profile, env) => join(resolvePaths({ appName: app.appName, prefix: app.envPrefix, env }).state, "mcp-http", `${profile}.json`);
|
|
26
26
|
const withFlags = (command) => command.option("--permission <key=level>", "make a permission stricter for this server only, like messages.send=deny; it never loosens the profile; repeat for more keys", (value, previous = []) => [...previous, value]);
|
|
27
27
|
export const mcpCommand = (messenger) => {
|
|
28
28
|
const { app } = messenger;
|
|
29
|
-
const command =
|
|
29
|
+
const command = annotate(new Command("mcp"), { effects: LOCAL_READ, mcp: false })
|
|
30
|
+
.summary("serve this profile's commands to an AI agent, and set that up")
|
|
31
|
+
.description(`serve this profile to an agent over MCP and set it up — \`claude mcp add ${app.command} -- ${app.command} <profile> mcp serve\``);
|
|
32
|
+
command.addCommand(withFlags(annotate(new Command("serve"), {
|
|
33
|
+
effects: REMOTE_WRITE,
|
|
34
|
+
lifetime: "persistent",
|
|
35
|
+
mcp: false,
|
|
36
|
+
examples: [
|
|
37
|
+
`${app.command} personal mcp serve`,
|
|
38
|
+
`${app.command} personal mcp serve --http --public-url https://laptop.example.ts.net`,
|
|
39
|
+
],
|
|
40
|
+
})
|
|
30
41
|
.summary("serve this profile's commands to an AI agent")
|
|
31
|
-
.description(`serve this profile to an agent over MCP, on stdin and stdout — \`claude mcp add ${app.command} -- ${app.command} <profile> mcp\`; with no profile, \`${app.command} mcp\` serves every profile and each tool asks which`))
|
|
42
|
+
.description(`serve this profile to an agent over MCP, on stdin and stdout — \`claude mcp add ${app.command} -- ${app.command} <profile> mcp serve\`; with no profile, \`${app.command} mcp serve\` serves every profile and each tool asks which`))
|
|
32
43
|
.option("--http", "serve over HTTP on 127.0.0.1 for ChatGPT and Claude in the browser, behind your tunnel; the profile's permissions decide")
|
|
33
44
|
.option("--port <port>", `the local port for --http (default ${DEFAULT_PORT})`)
|
|
34
45
|
.option("--public-url <url>", "the tunnel's https address the browser apps use, e.g. https://<name>.ts.net or https://<name>.ts.net/tg")
|
|
35
|
-
.
|
|
36
|
-
command.action(async function () {
|
|
46
|
+
.action(async function () {
|
|
37
47
|
const flags = this.optsWithGlobals();
|
|
38
48
|
const context = messengerContext(this, messenger, { needsProfile: false });
|
|
39
|
-
|
|
49
|
+
const profile = namedProfile(context.settings);
|
|
50
|
+
if (profile === undefined)
|
|
40
51
|
return serveEveryProfile(this, context, messenger, flags);
|
|
41
|
-
if (flags.revoke) {
|
|
42
|
-
const { revokeAll } = await import("../../mcp/http/oauth.js");
|
|
43
|
-
revokeAll(httpTokenFile(app, context.settings.profile, context.env));
|
|
44
|
-
renderAnswer(this, context, itemOf({ revoked: true, profile: context.settings.profile }));
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
52
|
if (flags.http) {
|
|
48
53
|
const publicUrl = publicUrlOf(flags.publicUrl, {
|
|
49
54
|
missing: "--http",
|
|
@@ -51,7 +56,7 @@ export const mcpCommand = (messenger) => {
|
|
|
51
56
|
path: true,
|
|
52
57
|
});
|
|
53
58
|
const { serveOverHttpUntilStopped } = await import("../../mcp/server.js");
|
|
54
|
-
const tokenFile = httpTokenFile(app,
|
|
59
|
+
const tokenFile = httpTokenFile(app, profile, context.env);
|
|
55
60
|
await serveOverHttpUntilStopped(this, context, messenger, {}, {
|
|
56
61
|
publicUrl,
|
|
57
62
|
port: portOf(flags.port, DEFAULT_PORT),
|
|
@@ -64,9 +69,24 @@ export const mcpCommand = (messenger) => {
|
|
|
64
69
|
// Loaded here, not at the top: every other command would otherwise pay for the SDK.
|
|
65
70
|
const { serveOverStdio } = await import("../../mcp/server.js");
|
|
66
71
|
await serveOverStdio(this, context, messenger, {});
|
|
67
|
-
});
|
|
72
|
+
}));
|
|
73
|
+
command.addCommand(annotate(new Command("revoke"), {
|
|
74
|
+
effects: LOCAL_WRITE,
|
|
75
|
+
mcp: false,
|
|
76
|
+
examples: [`${app.command} personal mcp revoke`],
|
|
77
|
+
})
|
|
78
|
+
.summary("forget every login given to a browser app")
|
|
79
|
+
.description(`forget every login \`mcp serve --http\` gave a browser app, of this profile or, with no profile, of the server of every profile; each app must log in again`)
|
|
80
|
+
.action(async function () {
|
|
81
|
+
const context = messengerContext(this, messenger, { needsProfile: false });
|
|
82
|
+
const profile = namedProfile(context.settings);
|
|
83
|
+
const { revokeAll } = await import("../../mcp/http/oauth.js");
|
|
84
|
+
revokeAll(httpTokenFile(app, profile ?? ALL_PROFILES, context.env));
|
|
85
|
+
renderAnswer(this, context, itemOf({ revoked: true, profile: profile ?? null }));
|
|
86
|
+
}));
|
|
68
87
|
command.addCommand(mcpServerCommand({
|
|
69
88
|
example: `${app.command} personal`,
|
|
89
|
+
permission: true,
|
|
70
90
|
target: (self) => {
|
|
71
91
|
const { settings, env } = messengerContext(self, messenger, { needsProfile: false });
|
|
72
92
|
if (namedProfile(settings) === undefined)
|
|
@@ -75,7 +95,7 @@ export const mcpCommand = (messenger) => {
|
|
|
75
95
|
envPrefix: app.envPrefix,
|
|
76
96
|
env,
|
|
77
97
|
tokenFile: httpTokenFile(app, ALL_PROFILES, env),
|
|
78
|
-
argv: ["mcp", "--http"],
|
|
98
|
+
argv: ["mcp", "serve", "--http"],
|
|
79
99
|
version: app.version,
|
|
80
100
|
};
|
|
81
101
|
return {
|
|
@@ -84,7 +104,7 @@ export const mcpCommand = (messenger) => {
|
|
|
84
104
|
envPrefix: app.envPrefix,
|
|
85
105
|
env,
|
|
86
106
|
tokenFile: httpTokenFile(app, settings.profile, env),
|
|
87
|
-
argv: ["mcp", "--http"],
|
|
107
|
+
argv: ["mcp", "serve", "--http"],
|
|
88
108
|
childEnv: { [`${app.envPrefix}_PROFILE`]: settings.profile },
|
|
89
109
|
version: app.version,
|
|
90
110
|
};
|
|
@@ -194,22 +214,17 @@ export const mcpCommand = (messenger) => {
|
|
|
194
214
|
return command;
|
|
195
215
|
};
|
|
196
216
|
/**
|
|
197
|
-
* `<tool> mcp` with no profile: every profile on this computer, each call in that profile's own
|
|
217
|
+
* `<tool> mcp serve` with no profile: every profile on this computer, each call in that profile's own
|
|
218
|
+
* `<tool> <profile> mcp serve`
|
|
198
219
|
* (`src/mcp/all-profiles.ts`). Nothing picks a profile: each tool takes one, and the agent is told to ask.
|
|
199
220
|
*/
|
|
200
221
|
const serveEveryProfile = async (self, context, messenger, flags) => {
|
|
201
222
|
const { app } = messenger;
|
|
202
223
|
const { env } = context;
|
|
203
224
|
const tokenFile = httpTokenFile(app, ALL_PROFILES, env);
|
|
204
|
-
if (flags.revoke) {
|
|
205
|
-
const { revokeAll } = await import("../../mcp/http/oauth.js");
|
|
206
|
-
revokeAll(tokenFile);
|
|
207
|
-
renderAnswer(self, context, itemOf({ revoked: true, profile: null }));
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
225
|
const profiles = () => messenger.resolveSettings(self.optsWithGlobals(), { env, needsProfile: false, readOnly: true }).profilesHere;
|
|
211
226
|
if (profiles().length === 0)
|
|
212
|
-
throw new CliError("configuration_error", `no ${app.command} profile on this computer yet — set one up, then run \`${app.command} mcp\` again`);
|
|
227
|
+
throw new CliError("configuration_error", `no ${app.command} profile on this computer yet — set one up, then run \`${app.command} mcp serve\` again`);
|
|
213
228
|
const given = environmentOf(self).mcp ?? {};
|
|
214
229
|
const execPath = given.execPath ?? process.execPath;
|
|
215
230
|
const scriptPath = given.scriptPath ?? realpathSync(process.argv[1] ?? "");
|
|
@@ -220,7 +235,7 @@ const serveEveryProfile = async (self, context, messenger, flags) => {
|
|
|
220
235
|
version: app.version,
|
|
221
236
|
name: messenger.name ?? app.command,
|
|
222
237
|
profiles,
|
|
223
|
-
child: (profile) => ({ command: execPath, args: [scriptPath, profile, "mcp", ...tightened], env }),
|
|
238
|
+
child: (profile) => ({ command: execPath, args: [scriptPath, profile, "mcp", "serve", ...tightened], env }),
|
|
224
239
|
instructions: instructions({
|
|
225
240
|
command: app.command,
|
|
226
241
|
name: messenger.name ?? app.command,
|
|
@@ -337,6 +352,7 @@ export const serverEntry = (app, { profile, flags = {}, execPath, scriptPath, en
|
|
|
337
352
|
scriptPath,
|
|
338
353
|
...(profile === undefined ? [] : [profile]),
|
|
339
354
|
"mcp",
|
|
355
|
+
"serve",
|
|
340
356
|
...(flags.permission ?? []).flatMap((entry) => ["--permission", entry]),
|
|
341
357
|
],
|
|
342
358
|
...(Object.keys(directories).length > 0 ? { env: directories } : {}),
|
|
@@ -10,6 +10,16 @@ export const SOURCE_SEARCHED = ["messages", "notes", "events", "meetings"];
|
|
|
10
10
|
const defined = (value) => Object.fromEntries(Object.entries(value).filter(([, one]) => one !== undefined));
|
|
11
11
|
/** The page a `--page` asks for, as the offset the search services take. */
|
|
12
12
|
export const offsetOf = (page, limit) => page === undefined || page <= 1 ? undefined : (page - 1) * limit;
|
|
13
|
+
/** Who wrote it and what, the chat's title, the mail's headers: others write them; the owner's own messages too. */
|
|
14
|
+
const MESSAGE_HIT_UNTRUSTED = ["chat_title", "sender_name", "excerpt", "context.sender_name", "context.text"];
|
|
15
|
+
const MAIL_HIT_UNTRUSTED = [
|
|
16
|
+
"email.subject",
|
|
17
|
+
"email.from.name",
|
|
18
|
+
"email.to.name",
|
|
19
|
+
"email.cc.name",
|
|
20
|
+
"email.bcc.name",
|
|
21
|
+
"email.reply_addresses.name",
|
|
22
|
+
];
|
|
13
23
|
export const messageHitItem = (hit, query = "") => {
|
|
14
24
|
const at = parseLocator(hit.locator);
|
|
15
25
|
const around = hit.thread?.items ?? hit.context;
|
|
@@ -55,6 +65,7 @@ export const messageHitItem = (hit, query = "") => {
|
|
|
55
65
|
sender_name: one.senderName,
|
|
56
66
|
text: one.text,
|
|
57
67
|
})),
|
|
68
|
+
untrusted: hit.email ? [...MESSAGE_HIT_UNTRUSTED, ...MAIL_HIT_UNTRUSTED] : MESSAGE_HIT_UNTRUSTED,
|
|
58
69
|
});
|
|
59
70
|
};
|
|
60
71
|
const accountName = ({ provider, account }) => `${provider}:${account}`;
|
|
@@ -160,6 +171,7 @@ const pictureItems = (found) => {
|
|
|
160
171
|
sender_name: senderName,
|
|
161
172
|
matched_in: "picture",
|
|
162
173
|
score,
|
|
174
|
+
untrusted: ["sender_name"],
|
|
163
175
|
}));
|
|
164
176
|
};
|
|
165
177
|
/** `search messages` and `search mail`, CLI and MCP alike. */
|
|
@@ -175,9 +187,21 @@ export const messagesAnswer = (command, found, { limit, page, again }, query = "
|
|
|
175
187
|
meta: metaOf(accounts, found.skipped),
|
|
176
188
|
});
|
|
177
189
|
};
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
190
|
+
/**
|
|
191
|
+
* A document comes from a shared drive, where others write and edit; a note is the owner's own, in a folder of
|
|
192
|
+
* theirs or written in memo, so it is left unmarked.
|
|
193
|
+
*/
|
|
194
|
+
const DOCUMENT_HIT_UNTRUSTED = ["title", "excerpt"];
|
|
195
|
+
export const noteHitItem = (hit) => defined({ ...hit, untrusted: hit.kind === "document" ? DOCUMENT_HIT_UNTRUSTED : undefined });
|
|
196
|
+
/** The text of each kind of `search all` hit that someone other than the owner may have written. */
|
|
197
|
+
const ALL_HIT_UNTRUSTED = {
|
|
198
|
+
message: ["title", "excerpt", "sender_name"],
|
|
199
|
+
mail: ["title", "excerpt", "sender_name"],
|
|
200
|
+
document: DOCUMENT_HIT_UNTRUSTED,
|
|
201
|
+
note: undefined,
|
|
202
|
+
event: ["title", "excerpt"],
|
|
203
|
+
meeting: ["title", "excerpt"],
|
|
204
|
+
};
|
|
181
205
|
export const allHitItem = (item) => item.kind === "meeting"
|
|
182
206
|
? {
|
|
183
207
|
kind: "meeting",
|
|
@@ -190,6 +214,7 @@ export const allHitItem = (item) => item.kind === "meeting"
|
|
|
190
214
|
offset_ms: item.startMs,
|
|
191
215
|
scope: item.scope,
|
|
192
216
|
id: String(item.id),
|
|
217
|
+
untrusted: ALL_HIT_UNTRUSTED.meeting,
|
|
193
218
|
}
|
|
194
219
|
: defined({
|
|
195
220
|
kind: item.kind,
|
|
@@ -203,7 +228,7 @@ export const allHitItem = (item) => item.kind === "meeting"
|
|
|
203
228
|
sender_name: item.from?.name,
|
|
204
229
|
sender_ref: item.from?.ref,
|
|
205
230
|
found_by: item.foundBy,
|
|
206
|
-
untrusted: item.kind
|
|
231
|
+
untrusted: ALL_HIT_UNTRUSTED[item.kind],
|
|
207
232
|
});
|
|
208
233
|
/** Meaning was not searched because the model is not downloaded; `--exact` leaving it out is no gap. */
|
|
209
234
|
const meaningGap = (skipped, exact) => skipped === undefined || exact
|
|
@@ -3,9 +3,9 @@ import type { CacheDatabase } from "../../store/driver.js";
|
|
|
3
3
|
import type { Messenger } from "./context.js";
|
|
4
4
|
/** `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. */
|
|
5
5
|
export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
|
|
6
|
-
error?: undefined;
|
|
7
6
|
path: string;
|
|
8
7
|
exists: boolean;
|
|
8
|
+
error?: undefined;
|
|
9
9
|
} | {
|
|
10
10
|
path: string;
|
|
11
11
|
exists: boolean;
|
package/dist/cli/program.js
CHANGED
|
@@ -20,7 +20,7 @@ import { outputFor, provide } from "./context.js";
|
|
|
20
20
|
import { refuseByCommandPermissions, refuseWriteBeforeConnecting } from "./early-refusal.js";
|
|
21
21
|
import { changesAuthorityAt, changesAuthorityNow, isLogin, isStoreChange, judgedEffects, judgedTargets, storeChangeRefusal, } from "./effects.js";
|
|
22
22
|
import { itemOf, renderAnswer, snakeKeys } from "./envelope.js";
|
|
23
|
-
import { byteCount, DEFAULT_COMMAND_MS, DEFAULT_OUTPUT_BYTES, execution } from "./execution.js";
|
|
23
|
+
import { byteCount, DEFAULT_AGENT_COOPERATIVE_MS, DEFAULT_COMMAND_MS, DEFAULT_COOPERATIVE_MS, DEFAULT_OUTPUT_BYTES, execution, } from "./execution.js";
|
|
24
24
|
import { setupAtStart } from "./first-run.js";
|
|
25
25
|
import { installHelp } from "./help/first-screen.js";
|
|
26
26
|
import { showInputFields } from "./input-fields.js";
|
|
@@ -150,6 +150,11 @@ const runOnce = async (argv, definition, options) => {
|
|
|
150
150
|
let oneShot;
|
|
151
151
|
// Headless, an interactive command's 5 minutes are for its phase; a person at a terminal may still be answering.
|
|
152
152
|
let afterPhase;
|
|
153
|
+
let running;
|
|
154
|
+
const startDeadline = (ms) => {
|
|
155
|
+
running = ms === undefined ? undefined : { endsAt: Date.now() + ms, ms };
|
|
156
|
+
control.start(ms);
|
|
157
|
+
};
|
|
153
158
|
let finished = false;
|
|
154
159
|
// The steps after a phase are one-shot again, however it ended: bounded, and ended by a signal as any command is.
|
|
155
160
|
const endPhase = () => {
|
|
@@ -161,7 +166,7 @@ const runOnce = async (argv, definition, options) => {
|
|
|
161
166
|
// A later phase keeps the --timeout the steps after this one got, not the one this phase already used up.
|
|
162
167
|
if (explicitEnd !== undefined && oneShot !== undefined)
|
|
163
168
|
explicitEnd = Date.now() + oneShot;
|
|
164
|
-
|
|
169
|
+
startDeadline(afterPhase);
|
|
165
170
|
};
|
|
166
171
|
const interrupted = (signal) => {
|
|
167
172
|
// A second signal ends the command as any signal does: one that ignores its phase can still be stopped.
|
|
@@ -234,7 +239,7 @@ const runOnce = async (argv, definition, options) => {
|
|
|
234
239
|
phased = true;
|
|
235
240
|
phase = new AbortController();
|
|
236
241
|
phase.signal.addEventListener("abort", endPhase, { once: true });
|
|
237
|
-
|
|
242
|
+
startDeadline(undefined);
|
|
238
243
|
if (explicitEnd !== undefined)
|
|
239
244
|
phaseTimer = setTimeout(() => phase.abort(new CliError("timeout", "the command reached its --timeout", {
|
|
240
245
|
retryable: false,
|
|
@@ -243,6 +248,7 @@ const runOnce = async (argv, definition, options) => {
|
|
|
243
248
|
return phase.signal;
|
|
244
249
|
},
|
|
245
250
|
endUntilStopped: endPhase,
|
|
251
|
+
deadline: () => running,
|
|
246
252
|
};
|
|
247
253
|
provide(program, environment);
|
|
248
254
|
program.hook("preAction", async (_root, action) => {
|
|
@@ -330,7 +336,11 @@ const runOnce = async (argv, definition, options) => {
|
|
|
330
336
|
? undefined
|
|
331
337
|
: lifetime === "interactive"
|
|
332
338
|
? HEADLESS_INTERACTIVE_MS
|
|
333
|
-
:
|
|
339
|
+
: stopsAtItsPace === true
|
|
340
|
+
? options.agent
|
|
341
|
+
? DEFAULT_AGENT_COOPERATIVE_MS
|
|
342
|
+
: DEFAULT_COOPERATIVE_MS
|
|
343
|
+
: DEFAULT_COMMAND_MS
|
|
334
344
|
: persistent
|
|
335
345
|
? undefined
|
|
336
346
|
: parseDuration(timeout, flags.timeout === undefined ? envName(definition.app, "TIMEOUT") : "--timeout");
|
|
@@ -372,7 +382,7 @@ const runOnce = async (argv, definition, options) => {
|
|
|
372
382
|
});
|
|
373
383
|
throw new Relayed(code);
|
|
374
384
|
}
|
|
375
|
-
|
|
385
|
+
startDeadline(deadline);
|
|
376
386
|
oneShot = deadline;
|
|
377
387
|
afterPhase = timeout !== undefined || persistent || job || interactiveLogin ? deadline : DEFAULT_COMMAND_MS;
|
|
378
388
|
if (timeout !== undefined && deadline !== undefined)
|
|
@@ -9,11 +9,16 @@ export interface UpgradeContext extends Omit<PackageUpgradePorts, "onInstall"> {
|
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
11
|
* A running serve keeps the code it started with; the update replaced the files under it. Each is restarted by
|
|
12
|
-
* the new `script` — `server restart` refuses a serve started by hand, which is then named in `left
|
|
13
|
-
* `spawn` replaces the child process, as in `UpdateEnvironment`.
|
|
12
|
+
* the new `script` — `server restart` refuses a serve started by hand, which is then named in `left`, and one that
|
|
13
|
+
* ran out of time is named in `timedOut` too. `spawn` replaces the child process, as in `UpdateEnvironment`.
|
|
14
14
|
*/
|
|
15
15
|
export declare const restartServers: (app: AppIdentity, script: string, env: NodeJS.ProcessEnv, spawn?: (argv: string[]) => number) => {
|
|
16
16
|
restarted: string[];
|
|
17
17
|
left: string[];
|
|
18
|
+
timedOut: string[];
|
|
19
|
+
} | {
|
|
20
|
+
timedOut?: undefined;
|
|
21
|
+
restarted: string[];
|
|
22
|
+
left: string[];
|
|
18
23
|
};
|
|
19
24
|
export declare const upgradeCommand: (app: AppIdentity, packageName: string, contextFor: (command: Command) => UpgradeContext) => Command;
|
|
@@ -2,19 +2,25 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { CliError } from "@wirecat/cli-base";
|
|
3
3
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
+
import { restartMcpServers, staleMcpSessions, thisSystem, } from "../background/after-upgrade.js";
|
|
5
6
|
import { servingProfiles } from "../background/lock.js";
|
|
6
|
-
import { upgradePackage } from "../services/package-upgrade.js";
|
|
7
|
+
import { upgradePackage, } from "../services/package-upgrade.js";
|
|
8
|
+
import { environmentOf } from "./context.js";
|
|
7
9
|
import { grantsAuthority, LOCAL_WRITE } from "./effects.js";
|
|
8
10
|
import { itemOf, renderAnswer, snakeKeys } from "./envelope.js";
|
|
9
11
|
import { usableProfileName } from "./profile.js";
|
|
12
|
+
/** `server restart` waits 30 s for serve to stop and 30 s for it to listen; this is a ceiling over both. */
|
|
13
|
+
const RESTART_CEILING_MS = 90_000;
|
|
14
|
+
const TIMEOUT_EXIT = 9;
|
|
10
15
|
/**
|
|
11
16
|
* A running serve keeps the code it started with; the update replaced the files under it. Each is restarted by
|
|
12
|
-
* the new `script` — `server restart` refuses a serve started by hand, which is then named in `left
|
|
13
|
-
* `spawn` replaces the child process, as in `UpdateEnvironment`.
|
|
17
|
+
* the new `script` — `server restart` refuses a serve started by hand, which is then named in `left`, and one that
|
|
18
|
+
* ran out of time is named in `timedOut` too. `spawn` replaces the child process, as in `UpdateEnvironment`.
|
|
14
19
|
*/
|
|
15
20
|
export const restartServers = (app, script, env, spawn) => {
|
|
16
21
|
const restarted = [];
|
|
17
22
|
const left = [];
|
|
23
|
+
const timedOut = [];
|
|
18
24
|
for (const profile of servingProfiles(app, env)) {
|
|
19
25
|
try {
|
|
20
26
|
usableProfileName(profile);
|
|
@@ -25,11 +31,40 @@ export const restartServers = (app, script, env, spawn) => {
|
|
|
25
31
|
const argv = [script, profile, "server", "restart"];
|
|
26
32
|
const code = spawn
|
|
27
33
|
? spawn([process.execPath, ...argv])
|
|
28
|
-
: (spawnSync(process.execPath, argv, {
|
|
34
|
+
: (spawnSync(process.execPath, argv, {
|
|
35
|
+
env,
|
|
36
|
+
shell: false,
|
|
37
|
+
stdio: ["ignore", 2, 2],
|
|
38
|
+
timeout: RESTART_CEILING_MS,
|
|
39
|
+
}).status ?? TIMEOUT_EXIT);
|
|
29
40
|
(code === 0 ? restarted : left).push(profile);
|
|
41
|
+
if (code === TIMEOUT_EXIT)
|
|
42
|
+
timedOut.push(profile);
|
|
30
43
|
}
|
|
31
|
-
return { restarted, left };
|
|
44
|
+
return timedOut.length > 0 ? { restarted, left, timedOut } : { restarted, left };
|
|
32
45
|
};
|
|
46
|
+
const RESTART_FROM_APP = "restart them from your agent app (start a new chat, or reload its MCP servers)";
|
|
47
|
+
const mcpWarnings = (command, current, servers, stale) => [
|
|
48
|
+
...servers.flatMap((server) => !server.restarted
|
|
49
|
+
? [`the HTTP MCP server ${server.name} (PID ${server.pid}) was not restarted: ${server.reason}`]
|
|
50
|
+
: server.to !== null && server.to === server.from
|
|
51
|
+
? [
|
|
52
|
+
`the HTTP MCP server ${server.name} came back on ${command} ${server.to}: it runs from another install than the one upgraded`,
|
|
53
|
+
]
|
|
54
|
+
: []),
|
|
55
|
+
...(stale === undefined
|
|
56
|
+
? []
|
|
57
|
+
: !stale.checked
|
|
58
|
+
? [
|
|
59
|
+
`could not check for MCP sessions over stdio (${stale.reason}); any that run keep ${command} ${current} or older — ${RESTART_FROM_APP}`,
|
|
60
|
+
]
|
|
61
|
+
: stale.sessions.length > 0
|
|
62
|
+
? [
|
|
63
|
+
`${stale.sessions.length} MCP ${stale.sessions.length === 1 ? "session" : "sessions"} over stdio still ${stale.sessions.length === 1 ? "runs" : "run"} ${command} ${current} or older ` +
|
|
64
|
+
`(PID ${stale.sessions.map((session) => session.pid).join(", ")}) — ${RESTART_FROM_APP}`,
|
|
65
|
+
]
|
|
66
|
+
: []),
|
|
67
|
+
];
|
|
33
68
|
export const upgradeCommand = (app, packageName, contextFor) => grantsAuthority(annotate(new Command("upgrade"), {
|
|
34
69
|
effects: LOCAL_WRITE,
|
|
35
70
|
offline: false,
|
|
@@ -42,7 +77,19 @@ export const upgradeCommand = (app, packageName, contextFor) => grantsAuthority(
|
|
|
42
77
|
throw new CliError("validation_error", `\`${app.command} upgrade\` asks npm for the newest version; not with --offline`);
|
|
43
78
|
const context = contextFor(this);
|
|
44
79
|
const { renderer } = context;
|
|
45
|
-
const
|
|
80
|
+
const environment = environmentOf(this);
|
|
81
|
+
const env = environment.env ?? process.env;
|
|
82
|
+
// Built before the install: after it, the old process may no longer find its own lazily loaded files.
|
|
83
|
+
const system = { ...thisSystem(packageName, app.command), ...environment.afterUpgrade };
|
|
84
|
+
const afterUpdate = async () => {
|
|
85
|
+
const servers = (await context.afterUpdate?.()) ?? { restarted: [], left: [] };
|
|
86
|
+
return {
|
|
87
|
+
...servers,
|
|
88
|
+
mcpServers: await restartMcpServers(app, env, system),
|
|
89
|
+
staleSessions: staleMcpSessions(system),
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
const { result, reason, left, timedOut } = await upgradePackage({ current: app.version, command: app.command, packageName, check: check === true }, { ...context, afterUpdate, onInstall: (argv) => renderer.note(`running: ${argv.join(" ")}`) });
|
|
46
93
|
if (reason === "held_back" && result.heldBack)
|
|
47
94
|
renderer.warn(heldBackAdvice(result, result.heldBack));
|
|
48
95
|
if (reason === "unavailable")
|
|
@@ -58,9 +105,14 @@ export const upgradeCommand = (app, packageName, contextFor) => grantsAuthority(
|
|
|
58
105
|
renderer.warn(advice);
|
|
59
106
|
}
|
|
60
107
|
for (const profile of left) {
|
|
61
|
-
renderer.warn(
|
|
62
|
-
|
|
108
|
+
renderer.warn(timedOut?.includes(profile)
|
|
109
|
+
? `the server for profile ${profile} did not restart in time and may still run ${app.command} ${app.version} — ` +
|
|
110
|
+
`\`${app.command} ${profile} server status\``
|
|
111
|
+
: `the server for profile ${profile} still runs ${app.command} ${app.version} — ` +
|
|
112
|
+
`\`${app.command} ${profile} server restart\``);
|
|
63
113
|
}
|
|
114
|
+
for (const line of mcpWarnings(app.command, app.version, result.mcpServers ?? [], result.staleSessions))
|
|
115
|
+
renderer.warn(line);
|
|
64
116
|
renderAnswer(this, context, itemOf(snakeKeys(result)), () => summary(app.command, result));
|
|
65
117
|
});
|
|
66
118
|
const heldBackAdvice = ({ installed, latest, installer }, { until, command }) => {
|
|
@@ -75,7 +127,11 @@ const summary = (command, result) => {
|
|
|
75
127
|
const servers = result.restarted.length > 0
|
|
76
128
|
? `; restarted the server for ${result.restarted.map((profile) => `profile ${profile}`).join(", ")}`
|
|
77
129
|
: "";
|
|
78
|
-
|
|
130
|
+
const mcp = (result.mcpServers ?? []).filter((server) => server.restarted);
|
|
131
|
+
const http = mcp.length > 0
|
|
132
|
+
? `; restarted the HTTP MCP server ${mcp.map((server) => `${server.name} (${server.from ?? "?"} → ${server.to ?? "?"})`).join(", ")}`
|
|
133
|
+
: "";
|
|
134
|
+
return `${command} ${result.current} → ${result.installed ?? result.latest}${held}${servers}${http}`;
|
|
79
135
|
}
|
|
80
136
|
if (held)
|
|
81
137
|
return `${command} ${result.current}${held}`;
|
|
@@ -7,6 +7,8 @@ export declare const time: v.SchemaWithPipe<readonly [v.StringSchema<undefined>,
|
|
|
7
7
|
/** A time a caller sends: any offset, read as the moment it names. Answers carry `time`, always UTC. */
|
|
8
8
|
export declare const zonedTime: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time with a zone">]>;
|
|
9
9
|
export declare const count: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
10
|
+
/** Fields of an answer item someone other than the owner wrote, as dotted paths: an agent reads them as data. */
|
|
11
|
+
export declare const untrusted: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
10
12
|
export declare const providerMetadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
|
|
11
13
|
export declare const author: v.StrictObjectSchema<{
|
|
12
14
|
readonly type: v.PicklistSchema<["person", "bot"], undefined>;
|
package/dist/entities/fields.js
CHANGED
|
@@ -7,6 +7,8 @@ export const time = isoTime;
|
|
|
7
7
|
/** A time a caller sends: any offset, read as the moment it names. Answers carry `time`, always UTC. */
|
|
8
8
|
export const zonedTime = v.pipe(v.string(), v.isoTimestamp("an ISO 8601 time with a zone"));
|
|
9
9
|
export const count = v.pipe(v.number(), v.integer(), v.minValue(0));
|
|
10
|
+
/** Fields of an answer item someone other than the owner wrote, as dotted paths: an agent reads them as data. */
|
|
11
|
+
export const untrusted = v.optional(v.array(text));
|
|
10
12
|
export const providerMetadata = v.optional(v.record(v.string(), v.unknown()));
|
|
11
13
|
export const author = v.strictObject({
|
|
12
14
|
type: v.picklist(["person", "bot"]),
|
package/dist/entities/hit.d.ts
CHANGED
|
@@ -22,6 +22,6 @@ export declare const hit: v.StrictObjectSchema<{
|
|
|
22
22
|
readonly scope: v.OptionalSchema<v.PicklistSchema<["transcript", "chat", "summary"], undefined>, undefined>;
|
|
23
23
|
/** A meeting passage's stored line, so two passages of one meeting stay apart; `search meetings` gives the same. */
|
|
24
24
|
readonly id: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
|
|
25
|
-
/**
|
|
25
|
+
/** Absent on a note hit: the owner wrote it. */
|
|
26
26
|
readonly untrusted: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
27
27
|
}, undefined>;
|
package/dist/entities/hit.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
|
-
import { count, reference, text, time } from "./fields.js";
|
|
2
|
+
import { count, reference, text, time, untrusted } from "./fields.js";
|
|
3
3
|
/** One result of `search all`: a message, a mail, a note, a calendar event or a meeting passage, best first. */
|
|
4
4
|
export const hit = v.strictObject({
|
|
5
5
|
kind: v.picklist(["message", "mail", "note", "document", "event", "meeting"]),
|
|
@@ -23,6 +23,6 @@ export const hit = v.strictObject({
|
|
|
23
23
|
scope: v.optional(v.picklist(["transcript", "chat", "summary"])),
|
|
24
24
|
/** A meeting passage's stored line, so two passages of one meeting stay apart; `search meetings` gives the same. */
|
|
25
25
|
id: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
26
|
-
/**
|
|
27
|
-
untrusted
|
|
26
|
+
/** Absent on a note hit: the owner wrote it. */
|
|
27
|
+
untrusted,
|
|
28
28
|
});
|