create-walle 0.9.31 → 0.9.32
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/package.json +1 -1
- package/template/claude-task-manager/api-prompts.js +406 -1
- package/template/claude-task-manager/api-reviews.js +104 -6
- package/template/claude-task-manager/approval-agent.js +293 -27
- package/template/claude-task-manager/db.js +77 -13
- package/template/claude-task-manager/docs/codex-app-server-approvals.md +35 -1
- package/template/claude-task-manager/docs/postgres-concurrency-dgx-spark.html +407 -0
- package/template/claude-task-manager/docs/prompt-manager-redesign-proposal.html +24 -23
- package/template/claude-task-manager/docs/unified-auto-approver-proposal.html +520 -0
- package/template/claude-task-manager/git-utils.js +164 -26
- package/template/claude-task-manager/lib/agent-capabilities.js +26 -5
- package/template/claude-task-manager/lib/agent-cli-cache.js +1 -1
- package/template/claude-task-manager/lib/agent-presets.js +12 -0
- package/template/claude-task-manager/lib/agent-version-service.js +650 -0
- package/template/claude-task-manager/lib/approval-drift.js +57 -0
- package/template/claude-task-manager/lib/approval-hook.js +13 -0
- package/template/claude-task-manager/lib/auth-rules.js +6 -0
- package/template/claude-task-manager/lib/broadcast-payload-memo.js +109 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +89 -8
- package/template/claude-task-manager/lib/document-review.js +75 -1
- package/template/claude-task-manager/lib/escalation-review.js +33 -3
- package/template/claude-task-manager/lib/headless-term-service.js +42 -8
- package/template/claude-task-manager/lib/mcp-risk.js +128 -0
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +51 -2
- package/template/claude-task-manager/lib/native-agent-model-args.js +31 -0
- package/template/claude-task-manager/lib/opencode-conversation-store.js +269 -0
- package/template/claude-task-manager/lib/permission-match.js +77 -5
- package/template/claude-task-manager/lib/read-pool-client.js +15 -6
- package/template/claude-task-manager/lib/restore-backoff.js +82 -0
- package/template/claude-task-manager/lib/runtime-registry.js +1 -0
- package/template/claude-task-manager/lib/scheduled-wake.js +42 -0
- package/template/claude-task-manager/lib/session-capture.js +33 -6
- package/template/claude-task-manager/lib/session-history.js +56 -2
- package/template/claude-task-manager/lib/session-jobs.js +19 -0
- package/template/claude-task-manager/lib/session-restore.js +5 -4
- package/template/claude-task-manager/lib/session-standup.js +2 -0
- package/template/claude-task-manager/lib/session-stream.js +9 -0
- package/template/claude-task-manager/lib/state-sync/cell-diff.js +31 -4
- package/template/claude-task-manager/lib/state-sync/restore-frame-hold.js +39 -0
- package/template/claude-task-manager/lib/walle-ctm-history.js +103 -36
- package/template/claude-task-manager/lib/walle-session-model-catalog.js +7 -1
- package/template/claude-task-manager/lib/walle-token-chip.js +65 -0
- package/template/claude-task-manager/package.json +1 -1
- package/template/claude-task-manager/providers/codex-mcp-extract.js +33 -0
- package/template/claude-task-manager/providers/codex-mcp.js +23 -20
- package/template/claude-task-manager/providers/codex.js +65 -1
- package/template/claude-task-manager/public/css/prompts.css +345 -0
- package/template/claude-task-manager/public/css/reviews.css +16 -40
- package/template/claude-task-manager/public/css/walle-session.css +90 -26
- package/template/claude-task-manager/public/index.html +1251 -299
- package/template/claude-task-manager/public/js/document-review-links.js +215 -9
- package/template/claude-task-manager/public/js/message-renderer.js +19 -0
- package/template/claude-task-manager/public/js/mobile-review-core.js +54 -0
- package/template/claude-task-manager/public/js/prompt-diff.js +55 -0
- package/template/claude-task-manager/public/js/prompt-editor-chrome.js +108 -0
- package/template/claude-task-manager/public/js/prompt-editor-modes.js +87 -0
- package/template/claude-task-manager/public/js/prompt-organize.js +179 -0
- package/template/claude-task-manager/public/js/prompt-reuse.js +77 -0
- package/template/claude-task-manager/public/js/prompts.js +683 -215
- package/template/claude-task-manager/public/js/reviews.js +28 -54
- package/template/claude-task-manager/public/js/session-search-utils.js +55 -0
- package/template/claude-task-manager/public/js/state-sync-client.js +40 -1
- package/template/claude-task-manager/public/js/stream-view.js +6 -0
- package/template/claude-task-manager/public/js/walle-session.js +183 -32
- package/template/claude-task-manager/public/js/walle.js +21 -9
- package/template/claude-task-manager/public/m/app.css +117 -0
- package/template/claude-task-manager/public/m/app.js +309 -3
- package/template/claude-task-manager/public/m/index.html +56 -1
- package/template/claude-task-manager/server.js +573 -299
- package/template/claude-task-manager/workers/db-owner-worker.js +8 -0
- package/template/claude-task-manager/workers/read-pool-worker.js +4 -0
- package/template/claude-task-manager/workers/state-detectors/claude-code.js +10 -1
- package/template/docs/proposals/2026-06-23-local-model-right-path.html +340 -0
- package/template/docs/proposals/2026-06-24-qlora-done-right.md +255 -0
- package/template/package.json +1 -1
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +27 -4
- package/template/wall-e/brain.js +50 -5
- package/template/wall-e/chat/force-compact.js +73 -0
- package/template/wall-e/chat.js +54 -19
- package/template/wall-e/coding/action-memory-policy.js +120 -1
- package/template/wall-e/coding/compaction-service.js +10 -3
- package/template/wall-e/coding/model-router.js +116 -0
- package/template/wall-e/coding-orchestrator.js +303 -11
- package/template/wall-e/coding-prompts.js +1 -1
- package/template/wall-e/docs/skill-self-heal-design.html +429 -0
- package/template/wall-e/llm/client.js +53 -3
- package/template/wall-e/llm/default-fallback.js +59 -5
- package/template/wall-e/llm/mlx-worker.js +0 -0
- package/template/wall-e/llm/mlx.js +46 -12
- package/template/wall-e/llm/provider-error.js +2 -2
- package/template/wall-e/llm/provider-health-state.js +6 -2
- package/template/wall-e/llm/tool-call-validator.js +156 -0
- package/template/wall-e/skills/skill-dispatch-decision.js +82 -2
- package/template/wall-e/skills/skill-planner.js +72 -5
- package/template/wall-e/tools/command-registry.js +34 -0
- package/template/wall-e/training/gemma-e4b-qlora.js +197 -1
- package/template/wall-e/training/mlx_lora_launch.py +196 -0
- package/template/wall-e/training/training-coexistence.sh +87 -0
- package/template/wall-e/training/training-mem-guard.sh +76 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { execFile, spawn } = require('child_process');
|
|
8
|
+
const agentCliCache = require('./agent-cli-cache');
|
|
9
|
+
|
|
10
|
+
const CACHE_VERSION = 1;
|
|
11
|
+
const DEFAULT_TTL_MS = 6 * 60 * 60 * 1000;
|
|
12
|
+
const DEFAULT_REGISTRY_BASE_URL = 'https://registry.npmjs.org';
|
|
13
|
+
const DEFAULT_VERSION_TIMEOUT_MS = 2500;
|
|
14
|
+
const DEFAULT_REGISTRY_TIMEOUT_MS = 3000;
|
|
15
|
+
const DEFAULT_UPGRADE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
16
|
+
const MAX_OUTPUT_BYTES = 128 * 1024;
|
|
17
|
+
const MAX_JOB_OUTPUT_BYTES = 96 * 1024;
|
|
18
|
+
const MAX_UPGRADE_JOBS = 24;
|
|
19
|
+
const UPGRADE_JOB_TTL_MS = 60 * 60 * 1000;
|
|
20
|
+
let _upgradeJobSeq = 0;
|
|
21
|
+
const _upgradeJobs = new Map();
|
|
22
|
+
|
|
23
|
+
const AGENT_DEFINITIONS = Object.freeze({
|
|
24
|
+
'claude-code': Object.freeze({
|
|
25
|
+
id: 'claude-code',
|
|
26
|
+
cli: 'claude',
|
|
27
|
+
name: 'Claude Code',
|
|
28
|
+
versionArgs: ['--version'],
|
|
29
|
+
latest: { kind: 'npm', package: '@anthropic-ai/claude-code' },
|
|
30
|
+
selfUpdate: { args: ['update'], label: 'claude update' },
|
|
31
|
+
}),
|
|
32
|
+
codex: Object.freeze({
|
|
33
|
+
id: 'codex',
|
|
34
|
+
cli: 'codex',
|
|
35
|
+
name: 'Codex',
|
|
36
|
+
versionArgs: ['--version'],
|
|
37
|
+
latest: { kind: 'npm', package: '@openai/codex' },
|
|
38
|
+
selfUpdate: { args: ['update'], label: 'codex update' },
|
|
39
|
+
}),
|
|
40
|
+
'gemini-cli': Object.freeze({
|
|
41
|
+
id: 'gemini-cli',
|
|
42
|
+
cli: 'gemini',
|
|
43
|
+
name: 'Gemini CLI',
|
|
44
|
+
versionArgs: ['--version'],
|
|
45
|
+
latest: { kind: 'npm', package: '@google/gemini-cli' },
|
|
46
|
+
packageManagers: Object.freeze({
|
|
47
|
+
'npm-global': { command: 'npm', args: ['install', '-g', '@google/gemini-cli@latest'], label: 'npm install -g @google/gemini-cli@latest' },
|
|
48
|
+
homebrew: { command: 'brew', args: ['upgrade', 'gemini-cli'], label: 'brew upgrade gemini-cli' },
|
|
49
|
+
}),
|
|
50
|
+
}),
|
|
51
|
+
opencode: Object.freeze({
|
|
52
|
+
id: 'opencode',
|
|
53
|
+
cli: 'opencode',
|
|
54
|
+
name: 'OpenCode',
|
|
55
|
+
versionArgs: ['--version'],
|
|
56
|
+
latest: { kind: 'npm', package: 'opencode-ai' },
|
|
57
|
+
selfUpdate: { args: ['upgrade'], label: 'opencode upgrade' },
|
|
58
|
+
}),
|
|
59
|
+
cursor: Object.freeze({
|
|
60
|
+
id: 'cursor',
|
|
61
|
+
cli: 'cursor-agent',
|
|
62
|
+
name: 'Cursor',
|
|
63
|
+
versionArgs: ['--version'],
|
|
64
|
+
latest: null,
|
|
65
|
+
selfUpdate: { args: ['update'], label: 'cursor-agent update' },
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const DEFAULT_AGENT_IDS = Object.freeze(Object.keys(AGENT_DEFINITIONS));
|
|
70
|
+
|
|
71
|
+
function _readJson(filePath) {
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function _writeJson(filePath, payload) {
|
|
80
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
81
|
+
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2) + '\n');
|
|
82
|
+
try { fs.chmodSync(filePath, 0o600); } catch {}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeSemver(value) {
|
|
86
|
+
const match = String(value || '').match(/v?(\d+)\.(\d+)\.(\d+)(?:[-+][0-9A-Za-z.-]+)?/);
|
|
87
|
+
if (!match) return null;
|
|
88
|
+
const parts = match.slice(1).map((part) => Number(part));
|
|
89
|
+
if (parts.some((part) => !Number.isFinite(part))) return null;
|
|
90
|
+
return { major: parts[0], minor: parts[1], patch: parts[2], version: `${parts[0]}.${parts[1]}.${parts[2]}` };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function compareSemver(a, b) {
|
|
94
|
+
const left = normalizeSemver(a);
|
|
95
|
+
const right = normalizeSemver(b);
|
|
96
|
+
if (!left || !right) return null;
|
|
97
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
98
|
+
if (left[key] < right[key]) return -1;
|
|
99
|
+
if (left[key] > right[key]) return 1;
|
|
100
|
+
}
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseVersionText(stdout, stderr) {
|
|
105
|
+
const primary = String(stdout || '');
|
|
106
|
+
const primaryMatch = primary.match(/v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/);
|
|
107
|
+
if (primaryMatch) return primaryMatch[1].replace(/^v/, '');
|
|
108
|
+
const combined = `${primary}\n${String(stderr || '')}`;
|
|
109
|
+
const semverMatch = combined.match(/v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/);
|
|
110
|
+
if (semverMatch) return semverMatch[1].replace(/^v/, '');
|
|
111
|
+
const buildMatch = combined.match(/\b(\d{4}\.\d{2}\.\d{2}-[0-9A-Za-z.-]+)\b/);
|
|
112
|
+
return buildMatch ? buildMatch[1] : '';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function _trimOutput(text) {
|
|
116
|
+
const value = String(text || '');
|
|
117
|
+
if (Buffer.byteLength(value) <= MAX_OUTPUT_BYTES) return value;
|
|
118
|
+
return value.slice(0, MAX_OUTPUT_BYTES) + '\n[output truncated]';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function redactOutput(text) {
|
|
122
|
+
return String(text || '')
|
|
123
|
+
.replace(/sk-[A-Za-z0-9_-]{16,}/g, 'sk-[redacted]')
|
|
124
|
+
.replace(/pk_[A-Za-z0-9_-]{16,}/g, 'pk_[redacted]')
|
|
125
|
+
.replace(/npm_[A-Za-z0-9_-]{16,}/g, 'npm_[redacted]')
|
|
126
|
+
.replace(/ghp_[A-Za-z0-9_-]{16,}/g, 'ghp_[redacted]')
|
|
127
|
+
.replace(/glpat-[A-Za-z0-9_-]{16,}/g, 'glpat-[redacted]')
|
|
128
|
+
.replace(/xox[bpas]-[A-Za-z0-9-]{16,}/g, 'xox[redacted]')
|
|
129
|
+
.replace(/(password|passwd|secret|token)(\s*[=:]\s*)(".*?"|'.*?'|[^\s]+)/gi, '$1$2[redacted]');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function runCommand(command, args, opts = {}) {
|
|
133
|
+
const execFileImpl = opts.execFileImpl || execFile;
|
|
134
|
+
const timeout = Number(opts.timeoutMs) || DEFAULT_VERSION_TIMEOUT_MS;
|
|
135
|
+
const env = opts.env || process.env;
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
execFileImpl(command, args || [], {
|
|
138
|
+
encoding: 'utf8',
|
|
139
|
+
timeout,
|
|
140
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
141
|
+
env,
|
|
142
|
+
}, (error, stdout, stderr) => {
|
|
143
|
+
resolve({
|
|
144
|
+
ok: !error,
|
|
145
|
+
error: error ? (error.message || String(error)) : '',
|
|
146
|
+
code: error && typeof error.code !== 'undefined' ? error.code : 0,
|
|
147
|
+
signal: error && error.signal ? error.signal : '',
|
|
148
|
+
stdout: _trimOutput(stdout),
|
|
149
|
+
stderr: _trimOutput(stderr),
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function _newUpgradeJobId(agentId) {
|
|
156
|
+
_upgradeJobSeq = (_upgradeJobSeq + 1) % Number.MAX_SAFE_INTEGER;
|
|
157
|
+
return `agent-upgrade-${String(agentId || 'agent').replace(/[^a-z0-9_-]/gi, '-')}-${Date.now().toString(36)}-${_upgradeJobSeq.toString(36)}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function _appendJobOutput(job, stream, chunk) {
|
|
161
|
+
if (!job) return;
|
|
162
|
+
let text = redactOutput(Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk || ''));
|
|
163
|
+
if (!text) return;
|
|
164
|
+
const maxBytes = Number(job.maxOutputBytes) || MAX_JOB_OUTPUT_BYTES;
|
|
165
|
+
if (Buffer.byteLength(text) > maxBytes) {
|
|
166
|
+
text = text.slice(-maxBytes);
|
|
167
|
+
job.outputTruncated = true;
|
|
168
|
+
}
|
|
169
|
+
const entry = { at: Date.now(), stream: stream || 'system', text };
|
|
170
|
+
job.output.push(entry);
|
|
171
|
+
job.outputBytes += Buffer.byteLength(text);
|
|
172
|
+
while (job.outputBytes > maxBytes && job.output.length > 0) {
|
|
173
|
+
const removed = job.output.shift();
|
|
174
|
+
job.outputBytes -= Buffer.byteLength(removed.text || '');
|
|
175
|
+
job.outputTruncated = true;
|
|
176
|
+
}
|
|
177
|
+
job.updatedAt = entry.at;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function _upgradeJobSnapshot(job) {
|
|
181
|
+
if (!job) return null;
|
|
182
|
+
return {
|
|
183
|
+
id: job.id,
|
|
184
|
+
agent: job.agent,
|
|
185
|
+
name: job.name,
|
|
186
|
+
status: job.status,
|
|
187
|
+
phase: job.phase,
|
|
188
|
+
pid: job.pid || 0,
|
|
189
|
+
commandLabel: job.commandLabel || '',
|
|
190
|
+
command: job.command || '',
|
|
191
|
+
args: Array.isArray(job.args) ? job.args.slice() : [],
|
|
192
|
+
plan: job.plan || null,
|
|
193
|
+
startedAt: job.startedAt,
|
|
194
|
+
updatedAt: job.updatedAt,
|
|
195
|
+
finishedAt: job.finishedAt || 0,
|
|
196
|
+
durationMs: job.finishedAt ? Math.max(0, job.finishedAt - job.startedAt) : Math.max(0, Date.now() - job.startedAt),
|
|
197
|
+
code: typeof job.code === 'number' ? job.code : null,
|
|
198
|
+
signal: job.signal || '',
|
|
199
|
+
error: job.error || '',
|
|
200
|
+
refreshError: job.refreshError || '',
|
|
201
|
+
reused: !!job.reused,
|
|
202
|
+
output: job.output.slice(),
|
|
203
|
+
outputBytes: job.outputBytes || 0,
|
|
204
|
+
outputTruncated: !!job.outputTruncated,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function _pruneUpgradeJobs(now = Date.now()) {
|
|
209
|
+
for (const [id, job] of _upgradeJobs) {
|
|
210
|
+
if (job.status !== 'running' && job.finishedAt && now - job.finishedAt > UPGRADE_JOB_TTL_MS) {
|
|
211
|
+
_upgradeJobs.delete(id);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
while (_upgradeJobs.size > MAX_UPGRADE_JOBS) {
|
|
215
|
+
const oldest = _upgradeJobs.keys().next().value;
|
|
216
|
+
if (!oldest) break;
|
|
217
|
+
_upgradeJobs.delete(oldest);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function _findRunningUpgradeJob(agentId) {
|
|
222
|
+
for (const job of _upgradeJobs.values()) {
|
|
223
|
+
if (job.agent === agentId && job.status === 'running') return job;
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function getUpgradeJob(jobId) {
|
|
229
|
+
_pruneUpgradeJobs();
|
|
230
|
+
return _upgradeJobSnapshot(_upgradeJobs.get(String(jobId || '')));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function listUpgradeJobs() {
|
|
234
|
+
_pruneUpgradeJobs();
|
|
235
|
+
const jobs = [..._upgradeJobs.values()]
|
|
236
|
+
.sort((a, b) => (b.updatedAt || b.startedAt || 0) - (a.updatedAt || a.startedAt || 0))
|
|
237
|
+
.map(_upgradeJobSnapshot);
|
|
238
|
+
return { ok: true, jobs };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function startUpgradeJob(agentId, opts = {}) {
|
|
242
|
+
_pruneUpgradeJobs();
|
|
243
|
+
const status = opts.statusPayload || await getAgentVersionStatus({ ...opts, force: false });
|
|
244
|
+
const plan = buildUpgradePlan(agentId, status);
|
|
245
|
+
const existing = _findRunningUpgradeJob(plan.agent);
|
|
246
|
+
if (existing) {
|
|
247
|
+
const snapshot = _upgradeJobSnapshot(existing);
|
|
248
|
+
snapshot.reused = true;
|
|
249
|
+
return { ok: true, job: snapshot };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const now = Date.now();
|
|
253
|
+
const job = {
|
|
254
|
+
id: _newUpgradeJobId(plan.agent),
|
|
255
|
+
agent: plan.agent,
|
|
256
|
+
name: plan.name,
|
|
257
|
+
status: 'running',
|
|
258
|
+
phase: 'running',
|
|
259
|
+
commandLabel: plan.commandLabel || '',
|
|
260
|
+
command: plan.command,
|
|
261
|
+
args: plan.args.slice(),
|
|
262
|
+
plan,
|
|
263
|
+
pid: 0,
|
|
264
|
+
startedAt: now,
|
|
265
|
+
updatedAt: now,
|
|
266
|
+
finishedAt: 0,
|
|
267
|
+
code: null,
|
|
268
|
+
signal: '',
|
|
269
|
+
error: '',
|
|
270
|
+
refreshError: '',
|
|
271
|
+
output: [],
|
|
272
|
+
outputBytes: 0,
|
|
273
|
+
outputTruncated: false,
|
|
274
|
+
maxOutputBytes: Number(opts.maxJobOutputBytes) || MAX_JOB_OUTPUT_BYTES,
|
|
275
|
+
};
|
|
276
|
+
_upgradeJobs.set(job.id, job);
|
|
277
|
+
_appendJobOutput(job, 'system', `Starting ${plan.commandLabel || [path.basename(plan.command)].concat(plan.args).join(' ')}\n`);
|
|
278
|
+
|
|
279
|
+
const spawnImpl = opts.spawnImpl || spawn;
|
|
280
|
+
let child = null;
|
|
281
|
+
try {
|
|
282
|
+
child = spawnImpl(plan.command, plan.args, {
|
|
283
|
+
env: opts.env || process.env,
|
|
284
|
+
shell: false,
|
|
285
|
+
windowsHide: true,
|
|
286
|
+
});
|
|
287
|
+
} catch (e) {
|
|
288
|
+
job.status = 'failed';
|
|
289
|
+
job.phase = 'failed';
|
|
290
|
+
job.error = e && e.message ? e.message : String(e);
|
|
291
|
+
job.finishedAt = Date.now();
|
|
292
|
+
_appendJobOutput(job, 'stderr', `${job.error}\n`);
|
|
293
|
+
return { ok: true, job: _upgradeJobSnapshot(job) };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
job.child = child;
|
|
297
|
+
job.pid = child && child.pid ? child.pid : 0;
|
|
298
|
+
const timeoutMs = Number(opts.upgradeTimeoutMs) || DEFAULT_UPGRADE_TIMEOUT_MS;
|
|
299
|
+
const timeout = setTimeout(() => {
|
|
300
|
+
if (job.status !== 'running') return;
|
|
301
|
+
job.error = `upgrade_timeout_after_${timeoutMs}ms`;
|
|
302
|
+
job.phase = 'failed';
|
|
303
|
+
job.status = 'failed';
|
|
304
|
+
job.finishedAt = Date.now();
|
|
305
|
+
_appendJobOutput(job, 'stderr', `Upgrade timed out after ${Math.round(timeoutMs / 1000)}s\n`);
|
|
306
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
307
|
+
delete job.child;
|
|
308
|
+
delete job.timeout;
|
|
309
|
+
}, timeoutMs);
|
|
310
|
+
if (timeout.unref) timeout.unref();
|
|
311
|
+
job.timeout = timeout;
|
|
312
|
+
|
|
313
|
+
if (child.stdout && child.stdout.on) {
|
|
314
|
+
child.stdout.on('data', (chunk) => _appendJobOutput(job, 'stdout', chunk));
|
|
315
|
+
}
|
|
316
|
+
if (child.stderr && child.stderr.on) {
|
|
317
|
+
child.stderr.on('data', (chunk) => _appendJobOutput(job, 'stderr', chunk));
|
|
318
|
+
}
|
|
319
|
+
child.on('error', (e) => {
|
|
320
|
+
if (job.finishedAt) return;
|
|
321
|
+
job.status = 'failed';
|
|
322
|
+
job.phase = 'failed';
|
|
323
|
+
job.error = e && e.message ? e.message : String(e);
|
|
324
|
+
job.finishedAt = Date.now();
|
|
325
|
+
clearTimeout(timeout);
|
|
326
|
+
_appendJobOutput(job, 'stderr', `${job.error}\n`);
|
|
327
|
+
delete job.child;
|
|
328
|
+
delete job.timeout;
|
|
329
|
+
});
|
|
330
|
+
child.on('close', async (code, signal) => {
|
|
331
|
+
if (job.finishedAt && job.status === 'failed') return;
|
|
332
|
+
clearTimeout(timeout);
|
|
333
|
+
job.code = typeof code === 'number' ? code : null;
|
|
334
|
+
job.signal = signal || '';
|
|
335
|
+
if (code === 0) {
|
|
336
|
+
job.phase = 'refreshing';
|
|
337
|
+
_appendJobOutput(job, 'system', 'Updater finished. Refreshing version status...\n');
|
|
338
|
+
if (!opts.skipRefresh) {
|
|
339
|
+
try {
|
|
340
|
+
job.refreshed = await getAgentVersionStatus({ ...opts, force: true });
|
|
341
|
+
} catch (e) {
|
|
342
|
+
job.refreshError = e && e.message ? e.message : String(e);
|
|
343
|
+
_appendJobOutput(job, 'stderr', `Version refresh failed: ${job.refreshError}\n`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
job.status = 'succeeded';
|
|
347
|
+
job.phase = job.refreshError ? 'refresh_failed' : 'succeeded';
|
|
348
|
+
_appendJobOutput(job, 'system', job.refreshError ? 'Upgrade command succeeded; version refresh failed.\n' : 'Upgrade complete.\n');
|
|
349
|
+
} else {
|
|
350
|
+
job.status = 'failed';
|
|
351
|
+
job.phase = 'failed';
|
|
352
|
+
job.error = job.error || `upgrade_exited_${code === null ? 'unknown' : code}`;
|
|
353
|
+
_appendJobOutput(job, 'stderr', `Upgrade failed with exit code ${code === null ? 'unknown' : code}${signal ? ` (${signal})` : ''}\n`);
|
|
354
|
+
}
|
|
355
|
+
job.finishedAt = Date.now();
|
|
356
|
+
job.updatedAt = job.finishedAt;
|
|
357
|
+
delete job.child;
|
|
358
|
+
delete job.timeout;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
return { ok: true, job: _upgradeJobSnapshot(job) };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function _registryPackageUrl(packageName, registryBaseUrl) {
|
|
365
|
+
const base = String(registryBaseUrl || DEFAULT_REGISTRY_BASE_URL).replace(/\/+$/, '');
|
|
366
|
+
return `${base}/${encodeURIComponent(packageName)}/latest`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function fetchJson(url, opts = {}) {
|
|
370
|
+
const timeoutMs = Number(opts.timeoutMs) || DEFAULT_REGISTRY_TIMEOUT_MS;
|
|
371
|
+
return new Promise((resolve, reject) => {
|
|
372
|
+
let settled = false;
|
|
373
|
+
const parsed = new URL(url);
|
|
374
|
+
const client = parsed.protocol === 'http:' ? http : https;
|
|
375
|
+
const req = client.get(parsed, {
|
|
376
|
+
headers: {
|
|
377
|
+
Accept: 'application/json',
|
|
378
|
+
'User-Agent': 'ctm-agent-version-service',
|
|
379
|
+
},
|
|
380
|
+
}, (res) => {
|
|
381
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
382
|
+
res.resume();
|
|
383
|
+
const next = new URL(res.headers.location, parsed).toString();
|
|
384
|
+
fetchJson(next, opts).then(resolve, reject);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
388
|
+
res.resume();
|
|
389
|
+
reject(new Error(`registry_http_${res.statusCode}`));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
let raw = '';
|
|
393
|
+
res.setEncoding('utf8');
|
|
394
|
+
res.on('data', (chunk) => {
|
|
395
|
+
raw += chunk;
|
|
396
|
+
if (Buffer.byteLength(raw) > MAX_OUTPUT_BYTES) {
|
|
397
|
+
req.destroy(new Error('registry_response_too_large'));
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
res.on('end', () => {
|
|
401
|
+
if (settled) return;
|
|
402
|
+
settled = true;
|
|
403
|
+
try {
|
|
404
|
+
resolve(JSON.parse(raw));
|
|
405
|
+
} catch (e) {
|
|
406
|
+
reject(new Error(`registry_json_parse_failed: ${e.message}`));
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
req.setTimeout(timeoutMs, () => {
|
|
411
|
+
req.destroy(new Error('registry_timeout'));
|
|
412
|
+
});
|
|
413
|
+
req.on('error', (error) => {
|
|
414
|
+
if (settled) return;
|
|
415
|
+
settled = true;
|
|
416
|
+
reject(error);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function _latestVersion(def, opts) {
|
|
422
|
+
if (!def.latest) return { version: '', source: 'none', error: '' };
|
|
423
|
+
if (def.latest.kind !== 'npm') return { version: '', source: def.latest.kind || 'unknown', error: 'unsupported_latest_source' };
|
|
424
|
+
const fetchJsonImpl = opts.fetchJsonImpl || fetchJson;
|
|
425
|
+
const url = _registryPackageUrl(def.latest.package, opts.registryBaseUrl);
|
|
426
|
+
try {
|
|
427
|
+
const body = await fetchJsonImpl(url, { timeoutMs: opts.registryTimeoutMs || DEFAULT_REGISTRY_TIMEOUT_MS });
|
|
428
|
+
return {
|
|
429
|
+
version: String(body && body.version ? body.version : '').replace(/^v/, ''),
|
|
430
|
+
source: `npm:${def.latest.package}`,
|
|
431
|
+
url,
|
|
432
|
+
error: '',
|
|
433
|
+
};
|
|
434
|
+
} catch (e) {
|
|
435
|
+
return {
|
|
436
|
+
version: '',
|
|
437
|
+
source: `npm:${def.latest.package}`,
|
|
438
|
+
url,
|
|
439
|
+
error: e && e.message ? e.message : String(e),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function detectInstallSource(entry, def) {
|
|
445
|
+
const combined = `${entry && entry.path ? entry.path : ''}\n${entry && entry.realpath ? entry.realpath : ''}`.toLowerCase();
|
|
446
|
+
if (!entry || !entry.available) return 'missing';
|
|
447
|
+
if (def.id === 'cursor' && combined.includes('/cursor-agent/versions/')) return 'self-update';
|
|
448
|
+
if (combined.includes('/caskroom/')) return 'homebrew-cask';
|
|
449
|
+
if (combined.includes('/cellar/')) return 'homebrew';
|
|
450
|
+
if (def.latest && def.latest.package) {
|
|
451
|
+
const packagePath = `/node_modules/${def.latest.package.toLowerCase()}/`;
|
|
452
|
+
if (combined.includes(packagePath)) return 'npm-global';
|
|
453
|
+
}
|
|
454
|
+
if (combined.includes('/node_modules/')) return 'npm-global';
|
|
455
|
+
if (def.selfUpdate) return 'self-update';
|
|
456
|
+
return 'unknown';
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function _deriveStatus(installedVersion, latestVersion, available) {
|
|
460
|
+
if (!available) return { status: 'missing', hasUpdate: false };
|
|
461
|
+
if (!installedVersion) return { status: 'version_unknown', hasUpdate: false };
|
|
462
|
+
if (!latestVersion) return { status: 'latest_unknown', hasUpdate: false };
|
|
463
|
+
const comparison = compareSemver(installedVersion, latestVersion);
|
|
464
|
+
if (comparison === null) return { status: 'latest_unknown', hasUpdate: false };
|
|
465
|
+
if (comparison < 0) return { status: 'update_available', hasUpdate: true };
|
|
466
|
+
if (comparison > 0) return { status: 'ahead', hasUpdate: false };
|
|
467
|
+
return { status: 'current', hasUpdate: false };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function _checkAgent(def, availability, opts) {
|
|
471
|
+
const entry = availability && availability.agents ? availability.agents[def.cli] : null;
|
|
472
|
+
const available = !!(entry && entry.available && !entry.stale);
|
|
473
|
+
const installSource = detectInstallSource(entry, def);
|
|
474
|
+
let versionResult = { ok: false, stdout: '', stderr: '', error: '' };
|
|
475
|
+
let installedVersion = '';
|
|
476
|
+
if (available) {
|
|
477
|
+
versionResult = await runCommand(entry.path || def.cli, def.versionArgs || ['--version'], {
|
|
478
|
+
execFileImpl: opts.execFileImpl,
|
|
479
|
+
timeoutMs: opts.versionTimeoutMs || DEFAULT_VERSION_TIMEOUT_MS,
|
|
480
|
+
env: opts.env || process.env,
|
|
481
|
+
});
|
|
482
|
+
installedVersion = parseVersionText(versionResult.stdout, versionResult.stderr);
|
|
483
|
+
}
|
|
484
|
+
const latest = await _latestVersion(def, opts);
|
|
485
|
+
const derived = _deriveStatus(installedVersion, latest.version, available);
|
|
486
|
+
const canUpgrade = available && !!(
|
|
487
|
+
def.selfUpdate ||
|
|
488
|
+
(def.packageManagers && def.packageManagers[installSource])
|
|
489
|
+
);
|
|
490
|
+
return {
|
|
491
|
+
id: def.id,
|
|
492
|
+
cli: def.cli,
|
|
493
|
+
name: def.name,
|
|
494
|
+
available,
|
|
495
|
+
path: entry && entry.path ? entry.path : null,
|
|
496
|
+
realpath: entry && entry.realpath ? entry.realpath : null,
|
|
497
|
+
installSource,
|
|
498
|
+
installedVersion,
|
|
499
|
+
latestVersion: latest.version || '',
|
|
500
|
+
latestSource: latest.source || '',
|
|
501
|
+
latestUrl: latest.url || '',
|
|
502
|
+
status: derived.status,
|
|
503
|
+
hasUpdate: derived.hasUpdate,
|
|
504
|
+
canUpgrade,
|
|
505
|
+
upgradeMode: def.selfUpdate ? 'self-update' : (def.packageManagers && def.packageManagers[installSource] ? installSource : ''),
|
|
506
|
+
versionError: available && !installedVersion ? (versionResult.error || 'version_parse_failed') : '',
|
|
507
|
+
latestError: latest.error || '',
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function _statusMap(rows) {
|
|
512
|
+
const map = {};
|
|
513
|
+
for (const row of rows || []) {
|
|
514
|
+
if (row && row.id) map[row.id] = row;
|
|
515
|
+
}
|
|
516
|
+
return map;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function checkAgentVersions(opts = {}) {
|
|
520
|
+
const now = Number(opts.now) || Date.now();
|
|
521
|
+
const agentIds = Array.isArray(opts.agentIds) && opts.agentIds.length ? opts.agentIds : DEFAULT_AGENT_IDS;
|
|
522
|
+
const defs = agentIds.map((id) => AGENT_DEFINITIONS[id]).filter(Boolean);
|
|
523
|
+
const cliBins = defs.map((def) => def.cli);
|
|
524
|
+
const availability = agentCliCache.getAvailableAgents({
|
|
525
|
+
cacheFile: opts.cliCacheFile,
|
|
526
|
+
agents: cliBins,
|
|
527
|
+
env: opts.env || process.env,
|
|
528
|
+
now,
|
|
529
|
+
extraBinDirs: opts.extraBinDirs,
|
|
530
|
+
});
|
|
531
|
+
const agents = await Promise.all(defs.map((def) => _checkAgent(def, availability, opts)));
|
|
532
|
+
const updateCount = agents.filter((agent) => agent.hasUpdate).length;
|
|
533
|
+
return {
|
|
534
|
+
ok: true,
|
|
535
|
+
version: CACHE_VERSION,
|
|
536
|
+
generatedAt: now,
|
|
537
|
+
cached: false,
|
|
538
|
+
updateCount,
|
|
539
|
+
agents: _statusMap(agents),
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async function getAgentVersionStatus(opts = {}) {
|
|
544
|
+
const now = Number(opts.now) || Date.now();
|
|
545
|
+
const ttlMs = Number(opts.ttlMs) || DEFAULT_TTL_MS;
|
|
546
|
+
if (!opts.force && opts.cacheFile) {
|
|
547
|
+
const cached = _readJson(opts.cacheFile);
|
|
548
|
+
if (cached && cached.version === CACHE_VERSION && cached.generatedAt && now - cached.generatedAt < ttlMs && cached.agents) {
|
|
549
|
+
return { ...cached, cached: true };
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const status = await checkAgentVersions({ ...opts, now });
|
|
553
|
+
if (opts.cacheFile) {
|
|
554
|
+
try { _writeJson(opts.cacheFile, status); } catch (e) {
|
|
555
|
+
return { ...status, cacheError: e && e.message ? e.message : String(e) };
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return status;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function _getDefinition(agentId) {
|
|
562
|
+
const def = AGENT_DEFINITIONS[String(agentId || '')];
|
|
563
|
+
if (!def) throw new Error('unknown_agent');
|
|
564
|
+
return def;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function buildUpgradePlan(agentId, statusPayload) {
|
|
568
|
+
const def = _getDefinition(agentId);
|
|
569
|
+
const row = statusPayload && statusPayload.agents ? statusPayload.agents[def.id] : null;
|
|
570
|
+
if (!row || !row.available) throw new Error('agent_not_available');
|
|
571
|
+
let command = row.path || def.cli;
|
|
572
|
+
let args = null;
|
|
573
|
+
let commandKind = '';
|
|
574
|
+
let commandLabel = '';
|
|
575
|
+
if (def.selfUpdate) {
|
|
576
|
+
args = def.selfUpdate.args.slice();
|
|
577
|
+
commandKind = 'self-update';
|
|
578
|
+
commandLabel = def.selfUpdate.label;
|
|
579
|
+
} else if (def.packageManagers && def.packageManagers[row.installSource]) {
|
|
580
|
+
const manager = def.packageManagers[row.installSource];
|
|
581
|
+
command = manager.command;
|
|
582
|
+
args = manager.args.slice();
|
|
583
|
+
commandKind = row.installSource;
|
|
584
|
+
commandLabel = manager.label;
|
|
585
|
+
}
|
|
586
|
+
if (!args) throw new Error('upgrade_not_supported_for_install_source');
|
|
587
|
+
return {
|
|
588
|
+
ok: true,
|
|
589
|
+
agent: def.id,
|
|
590
|
+
name: def.name,
|
|
591
|
+
currentVersion: row.installedVersion || '',
|
|
592
|
+
latestVersion: row.latestVersion || '',
|
|
593
|
+
status: row.status || '',
|
|
594
|
+
hasUpdate: !!row.hasUpdate,
|
|
595
|
+
installSource: row.installSource || '',
|
|
596
|
+
command,
|
|
597
|
+
args,
|
|
598
|
+
commandKind,
|
|
599
|
+
commandLabel,
|
|
600
|
+
changes: [
|
|
601
|
+
{ description: row.latestVersion ? `Upgrade ${def.name} from ${row.installedVersion || 'unknown'} to ${row.latestVersion}` : `Run ${def.name}'s updater to check for the latest version` },
|
|
602
|
+
{ description: `Command: ${commandLabel || [path.basename(command)].concat(args).join(' ')}` },
|
|
603
|
+
],
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async function upgradeAgentVersion(agentId, opts = {}) {
|
|
608
|
+
const status = opts.statusPayload || await getAgentVersionStatus({ ...opts, force: false });
|
|
609
|
+
const plan = buildUpgradePlan(agentId, status);
|
|
610
|
+
const result = await runCommand(plan.command, plan.args, {
|
|
611
|
+
execFileImpl: opts.execFileImpl,
|
|
612
|
+
timeoutMs: opts.upgradeTimeoutMs || DEFAULT_UPGRADE_TIMEOUT_MS,
|
|
613
|
+
env: opts.env || process.env,
|
|
614
|
+
});
|
|
615
|
+
if (!result.ok) {
|
|
616
|
+
const error = new Error(result.error || 'upgrade_failed');
|
|
617
|
+
error.result = result;
|
|
618
|
+
error.plan = plan;
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
621
|
+
const refreshed = opts.skipRefresh
|
|
622
|
+
? null
|
|
623
|
+
: await getAgentVersionStatus({ ...opts, force: true });
|
|
624
|
+
return {
|
|
625
|
+
ok: true,
|
|
626
|
+
plan,
|
|
627
|
+
result,
|
|
628
|
+
refreshed,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
module.exports = {
|
|
633
|
+
AGENT_DEFINITIONS,
|
|
634
|
+
CACHE_VERSION,
|
|
635
|
+
DEFAULT_AGENT_IDS,
|
|
636
|
+
compareSemver,
|
|
637
|
+
normalizeSemver,
|
|
638
|
+
parseVersionText,
|
|
639
|
+
redactOutput,
|
|
640
|
+
detectInstallSource,
|
|
641
|
+
checkAgentVersions,
|
|
642
|
+
getAgentVersionStatus,
|
|
643
|
+
buildUpgradePlan,
|
|
644
|
+
upgradeAgentVersion,
|
|
645
|
+
startUpgradeJob,
|
|
646
|
+
getUpgradeJob,
|
|
647
|
+
listUpgradeJobs,
|
|
648
|
+
_registryPackageUrl,
|
|
649
|
+
_upgradeJobs,
|
|
650
|
+
};
|