copillm 0.4.0 → 0.4.2
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.
|
@@ -19,6 +19,8 @@ import { isDevModeActive } from "../shared/devMode.js";
|
|
|
19
19
|
import { writeCommandOutput, writeHealthOutput } from "../shared/output.js";
|
|
20
20
|
import { spawnDetachedDaemon } from "../daemon/spawnDetached.js";
|
|
21
21
|
import { resolveRestartDecision } from "../daemon/restart.js";
|
|
22
|
+
import { selfUpdateToLatest, describeSelfUpdate } from "../daemon/selfUpdate.js";
|
|
23
|
+
import { getPackageInfo } from "../packageInfo.js";
|
|
22
24
|
export function register(program) {
|
|
23
25
|
program
|
|
24
26
|
.command("start")
|
|
@@ -182,6 +184,16 @@ export function register(program) {
|
|
|
182
184
|
const detectedDebug = lockSnapshot.state === "running" ? await probeDebugEndpoint(lockSnapshot.port) : false;
|
|
183
185
|
const decision = resolveRestartDecision({ lock: lockSnapshot, detectedDebug, forceDebug });
|
|
184
186
|
enableRuntimeDebug(decision.debug);
|
|
187
|
+
// Self-update to the latest published version before respawning. The old
|
|
188
|
+
// daemon is still serving here, so an `npm install -g` (which overwrites
|
|
189
|
+
// the files the new daemon will be spawned from) only updates the code
|
|
190
|
+
// the *next* daemon runs, never the one currently up. Best-effort: a
|
|
191
|
+
// failure or offline registry just restarts on the installed version.
|
|
192
|
+
const selfUpdate = await selfUpdateToLatest(getPackageInfo());
|
|
193
|
+
const selfUpdateNote = describeSelfUpdate(selfUpdate, getPackageInfo().name);
|
|
194
|
+
if (selfUpdateNote && !opts.json) {
|
|
195
|
+
process.stderr.write(`${selfUpdateNote}\n`);
|
|
196
|
+
}
|
|
185
197
|
if (decision.action === "restart" && decision.previousPid !== null) {
|
|
186
198
|
await stopByPid(decision.previousPid);
|
|
187
199
|
}
|
|
@@ -194,7 +206,8 @@ export function register(program) {
|
|
|
194
206
|
const started = await spawnDetachedDaemon({ debug: decision.debug, forcePort: decision.forcePort });
|
|
195
207
|
await emitDetachedStartOutput(opts, started, decision.debug, "restarted", {
|
|
196
208
|
previousPid: decision.previousPid,
|
|
197
|
-
claudeCache: cache
|
|
209
|
+
claudeCache: cache,
|
|
210
|
+
selfUpdate
|
|
198
211
|
});
|
|
199
212
|
});
|
|
200
213
|
program
|
|
@@ -425,6 +438,9 @@ async function emitDetachedStartOutput(opts, started, debug, mode, extra) {
|
|
|
425
438
|
if (extra?.claudeCache !== undefined) {
|
|
426
439
|
payload.claude_cache = extra.claudeCache;
|
|
427
440
|
}
|
|
441
|
+
if (extra?.selfUpdate !== undefined) {
|
|
442
|
+
payload.self_update = extra.selfUpdate;
|
|
443
|
+
}
|
|
428
444
|
writeCommandOutput(opts, banner, payload);
|
|
429
445
|
}
|
|
430
446
|
/** Narrow the lock inspection down to the shape `resolveRestartDecision` needs. */
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { fetchLatestNpmVersion, isNewerVersion } from "../updateNotifier.js";
|
|
4
|
+
export async function selfUpdateToLatest(packageInfo, deps = {}) {
|
|
5
|
+
const env = deps.env ?? process.env;
|
|
6
|
+
const moduleUrl = deps.moduleUrl ?? import.meta.url;
|
|
7
|
+
if (!isGlobalNpmRuntime(moduleUrl, packageInfo.name)) {
|
|
8
|
+
return { status: "skipped", reason: "not-global" };
|
|
9
|
+
}
|
|
10
|
+
const fetchLatest = deps.fetchLatest ??
|
|
11
|
+
((name) => fetchLatestNpmVersion(name, { registryUrl: env.COPILLM_UPDATE_REGISTRY_URL }));
|
|
12
|
+
let latest = null;
|
|
13
|
+
try {
|
|
14
|
+
latest = await fetchLatest(packageInfo.name);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
latest = null;
|
|
18
|
+
}
|
|
19
|
+
if (!latest) {
|
|
20
|
+
return { status: "skipped", reason: "no-latest" };
|
|
21
|
+
}
|
|
22
|
+
if (!isNewerVersion(latest, packageInfo.version)) {
|
|
23
|
+
return { status: "up-to-date", version: packageInfo.version };
|
|
24
|
+
}
|
|
25
|
+
const runInstall = deps.runInstall ?? defaultRunInstall;
|
|
26
|
+
let outcome;
|
|
27
|
+
try {
|
|
28
|
+
outcome = runInstall(packageInfo.name, latest);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
outcome = { ok: false, detail: error instanceof Error ? error.message : "npm install failed" };
|
|
32
|
+
}
|
|
33
|
+
if (!outcome.ok) {
|
|
34
|
+
return { status: "failed", from: packageInfo.version, to: latest, detail: outcome.detail };
|
|
35
|
+
}
|
|
36
|
+
return { status: "updated", from: packageInfo.version, to: latest };
|
|
37
|
+
}
|
|
38
|
+
/** One-line human summary, or null when there's nothing worth printing. */
|
|
39
|
+
export function describeSelfUpdate(result, packageName) {
|
|
40
|
+
switch (result.status) {
|
|
41
|
+
case "updated":
|
|
42
|
+
return `Updated ${packageName} ${result.from} -> ${result.to}.`;
|
|
43
|
+
case "up-to-date":
|
|
44
|
+
return `${packageName} is already up to date (${result.version}).`;
|
|
45
|
+
case "failed":
|
|
46
|
+
return `Self-update to ${result.to} failed; continuing on ${result.from}. (${result.detail})`;
|
|
47
|
+
case "skipped":
|
|
48
|
+
// `no-latest` means the registry was unreachable — worth a quiet note;
|
|
49
|
+
// `not-global` (dev/dist runs) stays silent.
|
|
50
|
+
return result.reason === "no-latest"
|
|
51
|
+
? "Could not check for updates; continuing on the installed version."
|
|
52
|
+
: null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function defaultRunInstall(packageName, version) {
|
|
56
|
+
const isWindows = process.platform === "win32";
|
|
57
|
+
const result = spawnSync("npm", ["install", "-g", `${packageName}@${version}`], {
|
|
58
|
+
encoding: "utf8",
|
|
59
|
+
timeout: 120_000,
|
|
60
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
61
|
+
shell: isWindows
|
|
62
|
+
});
|
|
63
|
+
if (result.error) {
|
|
64
|
+
return { ok: false, detail: result.error.message };
|
|
65
|
+
}
|
|
66
|
+
if (result.status === 0) {
|
|
67
|
+
return { ok: true, detail: "" };
|
|
68
|
+
}
|
|
69
|
+
const stderr = (result.stderr ?? "").trim();
|
|
70
|
+
const tail = stderr ? stderr.split("\n").slice(-2).join(" ") : `npm exited with code ${result.status ?? "null"}`;
|
|
71
|
+
return { ok: false, detail: tail };
|
|
72
|
+
}
|
|
73
|
+
function isGlobalNpmRuntime(moduleUrl, packageName) {
|
|
74
|
+
// Prefer a real filesystem path, but fall back to the raw URL when
|
|
75
|
+
// `fileURLToPath` can't convert it (e.g. a non-Windows-style `file://` URL on
|
|
76
|
+
// Windows). We only need to scan for the `node_modules/<pkg>` marker, and the
|
|
77
|
+
// regex handles both `\` and `/` separators so the check is platform-agnostic.
|
|
78
|
+
let candidate;
|
|
79
|
+
try {
|
|
80
|
+
candidate = fileURLToPath(moduleUrl);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
candidate = moduleUrl;
|
|
84
|
+
}
|
|
85
|
+
const normalized = candidate.replace(/\\/g, "/").toLowerCase();
|
|
86
|
+
return normalized.includes(`/node_modules/${packageName.toLowerCase()}/`);
|
|
87
|
+
}
|
package/dist/cli/packageInfo.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveModelId } from "../../models/discovery.js";
|
|
2
2
|
import { CopilotTokenManagerError } from "../../auth/copilotToken.js";
|
|
3
|
-
import { anthropicToOpenAI } from "../../translation/openaiAnthropic.js";
|
|
3
|
+
import { anthropicToOpenAI, ProtocolTranslationError } from "../../translation/openaiAnthropic.js";
|
|
4
4
|
import { writeAnthropicPrelude } from "../../translation/streamingOpenAIToAnthropic.js";
|
|
5
5
|
import { isBenignSocketError, safeEnd, safeSendJson } from "../requestLifecycle.js";
|
|
6
6
|
import { InvalidRequestShapeError, writeAnthropicSseError } from "../errors.js";
|
|
@@ -15,6 +15,9 @@ function translateRequestBody(routeKind, body) {
|
|
|
15
15
|
return anthropicToOpenAI(body);
|
|
16
16
|
}
|
|
17
17
|
catch (error) {
|
|
18
|
+
if (error instanceof ProtocolTranslationError) {
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
18
21
|
if (error instanceof Error) {
|
|
19
22
|
throw new InvalidRequestShapeError(error.message);
|
|
20
23
|
}
|
|
@@ -297,14 +297,68 @@ function translateImageSource(source) {
|
|
|
297
297
|
}
|
|
298
298
|
throw new ProtocolTranslationError("unsupported_image_source", `Unsupported Anthropic image source type: ${String(source.type)}.`);
|
|
299
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Translate Anthropic `tool_result.content` into the string that goes into the
|
|
302
|
+
* OpenAI `tool` message's `content` field.
|
|
303
|
+
*
|
|
304
|
+
* Anthropic permits a `tool_result.content` array to contain `text` blocks
|
|
305
|
+
* *and* `image` blocks (and tolerates unknown future block types), but the
|
|
306
|
+
* OpenAI chat-completions `tool` role only accepts string content. Rather
|
|
307
|
+
* than reject the whole request — which historically surfaced to Claude Code
|
|
308
|
+
* as the cryptic `invalid_request_shape: Anthropic tool_result content only
|
|
309
|
+
* supports text blocks.` 400 whenever a tool (e.g. a screenshot MCP, an image
|
|
310
|
+
* read) returned an image — we degrade gracefully:
|
|
311
|
+
*
|
|
312
|
+
* - text blocks → extracted verbatim
|
|
313
|
+
* - image blocks → a `[image: <media_type_or_url>]` placeholder so the
|
|
314
|
+
* model at least sees that the tool returned an image
|
|
315
|
+
* - other types → a `[unsupported tool_result content block: type=X]`
|
|
316
|
+
* placeholder
|
|
317
|
+
*
|
|
318
|
+
* Full image pass-through (forwarding the image bytes to the model as a
|
|
319
|
+
* follow-up multi-part user message) is a deliberate non-goal for this
|
|
320
|
+
* function — it would change the request structure and the upstream contract
|
|
321
|
+
* for `tool` messages. We can revisit if/when there is a confirmed need.
|
|
322
|
+
*
|
|
323
|
+
* Malformed `text` blocks (missing `.text`) still throw — that is a real
|
|
324
|
+
* client bug, not a content shape we should silently invent text for.
|
|
325
|
+
*/
|
|
300
326
|
function translateToolResultContent(content) {
|
|
301
327
|
if (typeof content === "string") {
|
|
302
328
|
return content;
|
|
303
329
|
}
|
|
304
330
|
if (!Array.isArray(content)) {
|
|
305
|
-
throw new ProtocolTranslationError("invalid_tool_result_content", "Anthropic tool_result content must be string or
|
|
331
|
+
throw new ProtocolTranslationError("invalid_tool_result_content", "Anthropic tool_result content must be a string or an array of content blocks.");
|
|
332
|
+
}
|
|
333
|
+
return content.map(stringifyToolResultContentBlock).join("\n");
|
|
334
|
+
}
|
|
335
|
+
function stringifyToolResultContentBlock(block) {
|
|
336
|
+
if (!isObject(block) || typeof block.type !== "string") {
|
|
337
|
+
throw new ProtocolTranslationError("invalid_tool_result_content", "Anthropic tool_result content blocks must be objects with a string type.");
|
|
338
|
+
}
|
|
339
|
+
if (block.type === "text") {
|
|
340
|
+
if (typeof block.text !== "string") {
|
|
341
|
+
throw new ProtocolTranslationError("invalid_text_block", "Anthropic text block must include string text.");
|
|
342
|
+
}
|
|
343
|
+
return block.text;
|
|
344
|
+
}
|
|
345
|
+
if (block.type === "image") {
|
|
346
|
+
const descriptor = describeImageSource(block.source);
|
|
347
|
+
return descriptor ? `[image: ${descriptor}]` : "[image]";
|
|
348
|
+
}
|
|
349
|
+
return `[unsupported tool_result content block: type=${block.type}]`;
|
|
350
|
+
}
|
|
351
|
+
function describeImageSource(source) {
|
|
352
|
+
if (!isObject(source) || typeof source.type !== "string") {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
if (source.type === "base64" && typeof source.media_type === "string" && source.media_type.length > 0) {
|
|
356
|
+
return source.media_type;
|
|
357
|
+
}
|
|
358
|
+
if (source.type === "url" && typeof source.url === "string" && source.url.length > 0) {
|
|
359
|
+
return source.url;
|
|
306
360
|
}
|
|
307
|
-
return
|
|
361
|
+
return null;
|
|
308
362
|
}
|
|
309
363
|
function translateToolDefinition(tool) {
|
|
310
364
|
if (!isObject(tool) || typeof tool.name !== "string" || tool.name.length === 0) {
|