@co0ontty/wand 4.3.0 → 4.4.0-beta.g4c9bbce
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/dist/build-info.json +4 -4
- package/dist/distribution-manager.d.ts +50 -0
- package/dist/distribution-manager.js +276 -0
- package/dist/server-session-routes.js +21 -1
- package/dist/server.js +10 -420
- package/dist/structured-claude-adapter.d.ts +10 -0
- package/dist/structured-claude-adapter.js +116 -0
- package/dist/structured-claude-protocol.d.ts +34 -0
- package/dist/structured-claude-protocol.js +246 -0
- package/dist/structured-codex-adapter.d.ts +5 -0
- package/dist/structured-codex-adapter.js +94 -0
- package/dist/structured-codex-protocol.d.ts +78 -0
- package/dist/structured-codex-protocol.js +995 -0
- package/dist/structured-content.d.ts +5 -0
- package/dist/structured-content.js +17 -0
- package/dist/structured-opencode-adapter.d.ts +10 -7
- package/dist/structured-opencode-adapter.js +103 -0
- package/dist/structured-runner.d.ts +42 -0
- package/dist/structured-runner.js +1 -0
- package/dist/structured-session-manager.d.ts +13 -78
- package/dist/structured-session-manager.js +507 -2252
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-07-
|
|
4
|
-
"version": "4.
|
|
5
|
-
"channel": "
|
|
2
|
+
"commit": "4c9bbce65ef02fdc9b8b5d2891e8839f0f2a74a2",
|
|
3
|
+
"builtAt": "2026-07-15T13:05:18.468Z",
|
|
4
|
+
"version": "4.4.0-beta.g4c9bbce",
|
|
5
|
+
"channel": "beta"
|
|
6
6
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { WandConfig } from "./types.js";
|
|
2
|
+
export type ApkUpdateChannel = "stable" | "beta";
|
|
3
|
+
export interface LocalDistributionAsset {
|
|
4
|
+
fileName: string;
|
|
5
|
+
filePath: string;
|
|
6
|
+
size: number;
|
|
7
|
+
updatedAt: string;
|
|
8
|
+
version: string | null;
|
|
9
|
+
downloadUrl: string;
|
|
10
|
+
source: "local";
|
|
11
|
+
}
|
|
12
|
+
export interface ResolvedDistributionAsset {
|
|
13
|
+
version: string;
|
|
14
|
+
downloadUrl: string;
|
|
15
|
+
fileName: string;
|
|
16
|
+
size: number;
|
|
17
|
+
source: "local" | "github";
|
|
18
|
+
releaseNotes?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface DistributionSettings {
|
|
21
|
+
androidApk: Record<string, unknown>;
|
|
22
|
+
macosDmg: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
export interface DistributionManagerOptions {
|
|
25
|
+
configDir: string;
|
|
26
|
+
configPath: string;
|
|
27
|
+
config: WandConfig;
|
|
28
|
+
repositoryUrl: string;
|
|
29
|
+
fetch?: typeof fetch;
|
|
30
|
+
now?: () => number;
|
|
31
|
+
}
|
|
32
|
+
export declare class DistributionManager {
|
|
33
|
+
private readonly options;
|
|
34
|
+
private readonly fetchImpl;
|
|
35
|
+
private readonly now;
|
|
36
|
+
private readonly githubCache;
|
|
37
|
+
constructor(options: DistributionManagerOptions);
|
|
38
|
+
resolveLatestApk(channel: ApkUpdateChannel): Promise<ResolvedDistributionAsset | null>;
|
|
39
|
+
resolveAndroidDownload(channel?: ApkUpdateChannel): Promise<LocalDistributionAsset | null>;
|
|
40
|
+
resolveLatestDmg(): Promise<ResolvedDistributionAsset | null>;
|
|
41
|
+
resolveMacosDownload(): Promise<LocalDistributionAsset | null>;
|
|
42
|
+
getSettings(): Promise<DistributionSettings>;
|
|
43
|
+
private refreshConfig;
|
|
44
|
+
private resolveLocalAsset;
|
|
45
|
+
private readLocalAsset;
|
|
46
|
+
private fetchGitHubAsset;
|
|
47
|
+
private fetchGitHubReleaseAsset;
|
|
48
|
+
private buildSettings;
|
|
49
|
+
private publicAsset;
|
|
50
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { compareApkInstallOrder, compareSemver, extractSemver } from "./version-utils.js";
|
|
4
|
+
const GITHUB_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
5
|
+
function asRecord(value) {
|
|
6
|
+
return value && typeof value === "object" ? value : null;
|
|
7
|
+
}
|
|
8
|
+
function resolveConfiguredDir(configDir, configuredDir, fallback) {
|
|
9
|
+
const value = configuredDir?.trim();
|
|
10
|
+
if (!value)
|
|
11
|
+
return path.join(configDir, fallback);
|
|
12
|
+
return path.isAbsolute(value) ? value : path.resolve(configDir, value);
|
|
13
|
+
}
|
|
14
|
+
function extractArtifactVersion(fileName, extension) {
|
|
15
|
+
return extractSemver(fileName.replace(new RegExp(`\\${extension}$`, "i"), ""));
|
|
16
|
+
}
|
|
17
|
+
function isPrerelease(version) {
|
|
18
|
+
return !!version && version.includes("-");
|
|
19
|
+
}
|
|
20
|
+
export class DistributionManager {
|
|
21
|
+
options;
|
|
22
|
+
fetchImpl;
|
|
23
|
+
now;
|
|
24
|
+
githubCache = new Map();
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.options = options;
|
|
27
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
28
|
+
this.now = options.now ?? Date.now;
|
|
29
|
+
}
|
|
30
|
+
async resolveLatestApk(channel) {
|
|
31
|
+
const [localApk, githubApk] = await Promise.all([
|
|
32
|
+
this.resolveAndroidDownload(channel),
|
|
33
|
+
this.fetchGitHubAsset(".apk"),
|
|
34
|
+
]);
|
|
35
|
+
const local = localApk?.version ? {
|
|
36
|
+
version: localApk.version,
|
|
37
|
+
downloadUrl: `${localApk.downloadUrl}?channel=${channel}`,
|
|
38
|
+
fileName: localApk.fileName,
|
|
39
|
+
size: localApk.size,
|
|
40
|
+
source: "local",
|
|
41
|
+
} : null;
|
|
42
|
+
const github = githubApk ? { ...githubApk, source: "github" } : null;
|
|
43
|
+
if (local && github) {
|
|
44
|
+
return compareApkInstallOrder(github.version, local.version) > 0 ? github : local;
|
|
45
|
+
}
|
|
46
|
+
return local ?? github;
|
|
47
|
+
}
|
|
48
|
+
async resolveAndroidDownload(channel = "beta") {
|
|
49
|
+
await this.refreshConfig();
|
|
50
|
+
const { config, configDir } = this.options;
|
|
51
|
+
if (config.android?.enabled !== true)
|
|
52
|
+
return null;
|
|
53
|
+
const directory = resolveConfiguredDir(configDir, config.android.apkDir, "android");
|
|
54
|
+
return this.resolveLocalAsset({
|
|
55
|
+
directory,
|
|
56
|
+
extension: ".apk",
|
|
57
|
+
configuredFile: channel === "beta" ? "" : config.android.currentApkFile,
|
|
58
|
+
downloadUrl: "/android/download",
|
|
59
|
+
compareVersions: compareApkInstallOrder,
|
|
60
|
+
acceptVersion: channel === "stable" ? (version) => !isPrerelease(version) : undefined,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async resolveLatestDmg() {
|
|
64
|
+
const localDmg = await this.resolveMacosDownload();
|
|
65
|
+
if (localDmg?.version) {
|
|
66
|
+
return {
|
|
67
|
+
version: localDmg.version,
|
|
68
|
+
downloadUrl: localDmg.downloadUrl,
|
|
69
|
+
fileName: localDmg.fileName,
|
|
70
|
+
size: localDmg.size,
|
|
71
|
+
source: "local",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const github = await this.fetchGitHubAsset(".dmg");
|
|
75
|
+
return github ? { ...github, source: "github" } : null;
|
|
76
|
+
}
|
|
77
|
+
async resolveMacosDownload() {
|
|
78
|
+
await this.refreshConfig();
|
|
79
|
+
const { config, configDir } = this.options;
|
|
80
|
+
if (config.macos?.enabled !== true)
|
|
81
|
+
return null;
|
|
82
|
+
return this.resolveLocalAsset({
|
|
83
|
+
directory: resolveConfiguredDir(configDir, config.macos.dmgDir, "macos"),
|
|
84
|
+
extension: ".dmg",
|
|
85
|
+
configuredFile: config.macos.currentDmgFile,
|
|
86
|
+
downloadUrl: "/macos/download",
|
|
87
|
+
compareVersions: compareSemver,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async getSettings() {
|
|
91
|
+
const [localApk, githubApk, localDmg, githubDmg] = await Promise.all([
|
|
92
|
+
this.resolveAndroidDownload("beta"),
|
|
93
|
+
this.fetchGitHubAsset(".apk"),
|
|
94
|
+
this.resolveMacosDownload(),
|
|
95
|
+
this.fetchGitHubAsset(".dmg"),
|
|
96
|
+
]);
|
|
97
|
+
const apkDir = resolveConfiguredDir(this.options.configDir, this.options.config.android?.apkDir, "android");
|
|
98
|
+
const dmgDir = resolveConfiguredDir(this.options.configDir, this.options.config.macos?.dmgDir, "macos");
|
|
99
|
+
return {
|
|
100
|
+
androidApk: this.buildSettings("apk", apkDir, this.options.config.android?.enabled === true, localApk, githubApk),
|
|
101
|
+
macosDmg: this.buildSettings("dmg", dmgDir, this.options.config.macos?.enabled === true, localDmg, githubDmg),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async refreshConfig() {
|
|
105
|
+
let raw;
|
|
106
|
+
try {
|
|
107
|
+
raw = JSON.parse(await readFile(this.options.configPath, "utf8"));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const { config } = this.options;
|
|
113
|
+
const android = asRecord(raw.android);
|
|
114
|
+
if (android) {
|
|
115
|
+
config.android = { ...(config.android ?? {}) };
|
|
116
|
+
if (typeof android.enabled === "boolean")
|
|
117
|
+
config.android.enabled = android.enabled;
|
|
118
|
+
if (Object.hasOwn(android, "apkDir")) {
|
|
119
|
+
config.android.apkDir = typeof android.apkDir === "string" && android.apkDir.trim() ? android.apkDir.trim() : "android";
|
|
120
|
+
}
|
|
121
|
+
if (Object.hasOwn(android, "currentApkFile")) {
|
|
122
|
+
config.android.currentApkFile = typeof android.currentApkFile === "string" ? android.currentApkFile.trim() : "";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const macos = asRecord(raw.macos);
|
|
126
|
+
if (macos) {
|
|
127
|
+
config.macos = { ...(config.macos ?? {}) };
|
|
128
|
+
if (typeof macos.enabled === "boolean")
|
|
129
|
+
config.macos.enabled = macos.enabled;
|
|
130
|
+
if (Object.hasOwn(macos, "dmgDir")) {
|
|
131
|
+
config.macos.dmgDir = typeof macos.dmgDir === "string" && macos.dmgDir.trim() ? macos.dmgDir.trim() : "macos";
|
|
132
|
+
}
|
|
133
|
+
if (Object.hasOwn(macos, "currentDmgFile")) {
|
|
134
|
+
config.macos.currentDmgFile = typeof macos.currentDmgFile === "string" ? macos.currentDmgFile.trim() : "";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async resolveLocalAsset(options) {
|
|
139
|
+
await mkdir(options.directory, { recursive: true });
|
|
140
|
+
const configuredFile = options.configuredFile?.trim();
|
|
141
|
+
if (configuredFile) {
|
|
142
|
+
return this.readLocalAsset(path.join(options.directory, path.basename(configuredFile)), options);
|
|
143
|
+
}
|
|
144
|
+
const entries = await readdir(options.directory, { withFileTypes: true });
|
|
145
|
+
const files = entries.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(options.extension));
|
|
146
|
+
const candidates = (await Promise.all(files.map(async (entry) => {
|
|
147
|
+
const filePath = path.join(options.directory, entry.name);
|
|
148
|
+
return { entry, filePath, fileStat: await stat(filePath) };
|
|
149
|
+
}))).filter(({ entry }) => options.acceptVersion?.(extractArtifactVersion(entry.name, options.extension)) ?? true);
|
|
150
|
+
if (candidates.length === 0)
|
|
151
|
+
return null;
|
|
152
|
+
candidates.sort((a, b) => {
|
|
153
|
+
const aVersion = extractArtifactVersion(a.entry.name, options.extension);
|
|
154
|
+
const bVersion = extractArtifactVersion(b.entry.name, options.extension);
|
|
155
|
+
if (aVersion && bVersion) {
|
|
156
|
+
const comparison = options.compareVersions(bVersion, aVersion);
|
|
157
|
+
if (comparison !== 0)
|
|
158
|
+
return comparison;
|
|
159
|
+
}
|
|
160
|
+
else if (aVersion) {
|
|
161
|
+
return -1;
|
|
162
|
+
}
|
|
163
|
+
else if (bVersion) {
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
return b.fileStat.mtimeMs - a.fileStat.mtimeMs;
|
|
167
|
+
});
|
|
168
|
+
const selected = candidates[0];
|
|
169
|
+
return {
|
|
170
|
+
fileName: selected.entry.name,
|
|
171
|
+
filePath: selected.filePath,
|
|
172
|
+
size: selected.fileStat.size,
|
|
173
|
+
updatedAt: selected.fileStat.mtime.toISOString(),
|
|
174
|
+
version: extractArtifactVersion(selected.entry.name, options.extension),
|
|
175
|
+
downloadUrl: options.downloadUrl,
|
|
176
|
+
source: "local",
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
async readLocalAsset(filePath, options) {
|
|
180
|
+
try {
|
|
181
|
+
const fileStat = await stat(filePath);
|
|
182
|
+
if (!fileStat.isFile())
|
|
183
|
+
return null;
|
|
184
|
+
const fileName = path.basename(filePath);
|
|
185
|
+
const version = extractArtifactVersion(fileName, options.extension);
|
|
186
|
+
if (options.acceptVersion && !options.acceptVersion(version))
|
|
187
|
+
return null;
|
|
188
|
+
return {
|
|
189
|
+
fileName,
|
|
190
|
+
filePath,
|
|
191
|
+
size: fileStat.size,
|
|
192
|
+
updatedAt: fileStat.mtime.toISOString(),
|
|
193
|
+
version,
|
|
194
|
+
downloadUrl: options.downloadUrl,
|
|
195
|
+
source: "local",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async fetchGitHubAsset(extension) {
|
|
203
|
+
const cached = this.githubCache.get(extension);
|
|
204
|
+
if (cached && this.now() - cached.timestamp < GITHUB_CACHE_TTL_MS)
|
|
205
|
+
return cached.asset;
|
|
206
|
+
try {
|
|
207
|
+
const hit = await this.fetchGitHubReleaseAsset(extension);
|
|
208
|
+
if (!hit)
|
|
209
|
+
return cached?.asset ?? null;
|
|
210
|
+
const version = extractArtifactVersion(hit.asset.name, extension)
|
|
211
|
+
?? extractArtifactVersion(hit.tagName, extension)
|
|
212
|
+
?? hit.tagName.replace(/^v/, "");
|
|
213
|
+
const asset = {
|
|
214
|
+
version,
|
|
215
|
+
downloadUrl: hit.asset.browser_download_url,
|
|
216
|
+
fileName: hit.asset.name,
|
|
217
|
+
size: hit.asset.size,
|
|
218
|
+
...(extension === ".apk" && hit.body ? { releaseNotes: hit.body.trim().slice(0, 500) } : {}),
|
|
219
|
+
};
|
|
220
|
+
this.githubCache.set(extension, { asset, timestamp: this.now() });
|
|
221
|
+
return asset;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return cached?.asset ?? null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async fetchGitHubReleaseAsset(extension) {
|
|
228
|
+
const apiUrl = this.options.repositoryUrl.replace("github.com", "api.github.com/repos") + "/releases?per_page=30";
|
|
229
|
+
const response = await this.fetchImpl(apiUrl, {
|
|
230
|
+
headers: { Accept: "application/vnd.github.v3+json", "User-Agent": "wand-server" },
|
|
231
|
+
signal: AbortSignal.timeout(10000),
|
|
232
|
+
});
|
|
233
|
+
if (!response.ok)
|
|
234
|
+
return null;
|
|
235
|
+
const releases = await response.json();
|
|
236
|
+
for (const release of releases) {
|
|
237
|
+
if (release.draft || release.prerelease)
|
|
238
|
+
continue;
|
|
239
|
+
const asset = release.assets.find((candidate) => candidate.name.toLowerCase().endsWith(extension));
|
|
240
|
+
if (asset)
|
|
241
|
+
return { tagName: release.tag_name, body: release.body, asset };
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
buildSettings(kind, directory, enabled, local, github) {
|
|
246
|
+
const selected = local
|
|
247
|
+
? { ...local, source: "local" }
|
|
248
|
+
: github
|
|
249
|
+
? { ...github, updatedAt: null, source: "github" }
|
|
250
|
+
: null;
|
|
251
|
+
const hasKey = kind === "apk" ? "hasApk" : "hasDmg";
|
|
252
|
+
const dirKey = kind === "apk" ? "apkDir" : "dmgDir";
|
|
253
|
+
return {
|
|
254
|
+
enabled,
|
|
255
|
+
[dirKey]: directory,
|
|
256
|
+
[hasKey]: selected !== null,
|
|
257
|
+
fileName: selected?.fileName ?? null,
|
|
258
|
+
version: selected?.version ?? null,
|
|
259
|
+
size: selected?.size ?? null,
|
|
260
|
+
updatedAt: selected?.updatedAt ?? null,
|
|
261
|
+
downloadUrl: selected?.downloadUrl ?? null,
|
|
262
|
+
source: selected?.source ?? null,
|
|
263
|
+
local: local ? this.publicAsset(local) : null,
|
|
264
|
+
github: github ? this.publicAsset(github) : null,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
publicAsset(asset) {
|
|
268
|
+
return {
|
|
269
|
+
fileName: asset.fileName,
|
|
270
|
+
version: asset.version,
|
|
271
|
+
size: asset.size,
|
|
272
|
+
...("updatedAt" in asset ? { updatedAt: asset.updatedAt } : {}),
|
|
273
|
+
downloadUrl: asset.downloadUrl,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -959,7 +959,27 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
959
959
|
const shortcutKey = body.shortcutKey;
|
|
960
960
|
try {
|
|
961
961
|
if (structured.get(sessionId)) {
|
|
962
|
-
const
|
|
962
|
+
const completion = structured.sendMessage(sessionId, input);
|
|
963
|
+
if (body.respondImmediately === true) {
|
|
964
|
+
// sendMessage updates the canonical snapshot synchronously before it
|
|
965
|
+
// starts awaiting the runner. Native clients should not hold this
|
|
966
|
+
// request open for an entire model turn (which can exceed their HTTP
|
|
967
|
+
// timeout, especially while the first-turn title job is also active).
|
|
968
|
+
// The normal structured events continue to carry progress/failure.
|
|
969
|
+
completion.catch((error) => {
|
|
970
|
+
console.error("[wand] Accepted structured input later failed", {
|
|
971
|
+
sessionId,
|
|
972
|
+
error: getInputDebugMeta(error),
|
|
973
|
+
});
|
|
974
|
+
});
|
|
975
|
+
const accepted = structured.get(sessionId);
|
|
976
|
+
if (!accepted) {
|
|
977
|
+
throw new Error("未找到该结构化会话。");
|
|
978
|
+
}
|
|
979
|
+
res.status(202).json(sessionResponseDTO(accepted));
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
const snapshot = await completion;
|
|
963
983
|
res.json(sessionResponseDTO(snapshot));
|
|
964
984
|
return;
|
|
965
985
|
}
|