@pixelbyte-software/pixcode 1.35.5 → 1.36.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/README.de.md +116 -198
- package/README.ja.md +116 -192
- package/README.ko.md +116 -192
- package/README.md +201 -223
- package/README.ru.md +116 -198
- package/README.tr.md +205 -175
- package/README.zh-CN.md +116 -192
- package/dist/api-automation.html +110 -0
- package/dist/api-docs.html +18 -18
- package/dist/assets/index-BzRaZegN.css +32 -0
- package/dist/assets/{index-BnoShb9v.js → index-OkHfhUMk.js} +181 -180
- package/dist/docs.html +294 -0
- package/dist/features.html +112 -0
- package/dist/humans.txt +15 -0
- package/dist/index.html +2 -2
- package/dist/landing.html +217 -0
- package/dist/llms-full.txt +117 -0
- package/dist/llms.txt +53 -0
- package/dist/openapi.yaml +12 -9
- package/dist/orchestration.html +125 -0
- package/dist/robots.txt +4 -0
- package/dist/site.css +536 -0
- package/dist/sitemap.xml +51 -0
- package/dist-server/server/cli.js +51 -2
- package/dist-server/server/cli.js.map +1 -1
- package/dist-server/server/daemon/manager.js +0 -1
- package/dist-server/server/daemon/manager.js.map +1 -1
- package/dist-server/server/database/db.js +3 -2
- package/dist-server/server/database/db.js.map +1 -1
- package/dist-server/server/middleware/auth.js +9 -8
- package/dist-server/server/middleware/auth.js.map +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +156 -32
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +8 -1
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/routes/agent.js +72 -11
- package/dist-server/server/routes/agent.js.map +1 -1
- package/dist-server/server/services/notification-orchestrator.js +11 -2
- package/dist-server/server/services/notification-orchestrator.js.map +1 -1
- package/dist-server/server/services/provider-cli-versions.js +142 -0
- package/dist-server/server/services/provider-cli-versions.js.map +1 -0
- package/dist-server/server/services/startup-update.js +208 -0
- package/dist-server/server/services/startup-update.js.map +1 -0
- package/package.json +35 -10
- package/server/cli.js +58 -3
- package/server/daemon/manager.js +0 -1
- package/server/database/db.js +3 -2
- package/server/middleware/auth.js +9 -8
- package/server/modules/orchestration/workflows/workflow-runner.ts +172 -32
- package/server/modules/providers/provider.routes.ts +8 -1
- package/server/routes/agent.js +75 -10
- package/server/services/notification-orchestrator.js +11 -2
- package/server/services/provider-cli-versions.js +149 -0
- package/server/services/startup-update.js +234 -0
- package/dist/assets/index-Btxg-Dbl.css +0 -32
|
@@ -896,6 +896,124 @@ class WorkflowRunner {
|
|
|
896
896
|
}
|
|
897
897
|
}
|
|
898
898
|
|
|
899
|
+
private fallbackAgentFor(run: WorkflowRun, node: WorkflowNode): AgentAssignment | undefined {
|
|
900
|
+
if (node.stage === 'fallback' || node.id.startsWith('fallback_')) {
|
|
901
|
+
return undefined;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const settings = getMetadataRecord(run.metadata, 'settings');
|
|
905
|
+
const fallbackAgentInstanceId = readString(settings.fallbackAgentInstanceId);
|
|
906
|
+
if (!fallbackAgentInstanceId || fallbackAgentInstanceId === node.agentInstanceId) {
|
|
907
|
+
return undefined;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
return readAgentAssignments(run.metadata).find((agent) => agent.instanceId === fallbackAgentInstanceId);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
private createFallbackNode(node: WorkflowNode, fallbackAgent: AgentAssignment, reason: string): WorkflowNode {
|
|
914
|
+
const fallbackSuffix = safeNodeId(fallbackAgent.instanceId, 'fallback');
|
|
915
|
+
return {
|
|
916
|
+
...node,
|
|
917
|
+
id: `fallback_${node.id}_${fallbackSuffix}`,
|
|
918
|
+
adapterId: fallbackAgent.adapterId,
|
|
919
|
+
agentInstanceId: fallbackAgent.instanceId,
|
|
920
|
+
agentLabel: `${fallbackAgent.label} Fallback`,
|
|
921
|
+
assignment: `Fallback for ${node.agentLabel || node.id}`,
|
|
922
|
+
stage: 'fallback',
|
|
923
|
+
model: fallbackAgent.model,
|
|
924
|
+
permissionMode: fallbackAgent.permissionMode,
|
|
925
|
+
toolsSettings: fallbackAgent.toolsSettings,
|
|
926
|
+
prompt: [
|
|
927
|
+
'The previous CLI agent failed on this orchestration step.',
|
|
928
|
+
`Failed step: ${node.agentLabel || node.id}`,
|
|
929
|
+
`Failure: ${reason}`,
|
|
930
|
+
'Take over the same assignment as the backup CLI. Use the original goal and upstream context.',
|
|
931
|
+
'Do not repeat unrelated work; complete the failed step and report what you did.',
|
|
932
|
+
node.prompt,
|
|
933
|
+
].join('\n'),
|
|
934
|
+
onFail: 'continue',
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
private async runFallbackAfterFailure(
|
|
939
|
+
node: WorkflowNode,
|
|
940
|
+
workflow: Workflow,
|
|
941
|
+
run: WorkflowRun,
|
|
942
|
+
outputs: Map<string, string>,
|
|
943
|
+
started: Set<string>,
|
|
944
|
+
completed: Set<string>,
|
|
945
|
+
reason: string,
|
|
946
|
+
): Promise<boolean> {
|
|
947
|
+
const fallbackAgent = this.fallbackAgentFor(run, node);
|
|
948
|
+
if (!fallbackAgent) {
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
if (workflow.nodes.length + 1 > 64) {
|
|
952
|
+
run.metadata = {
|
|
953
|
+
...run.metadata,
|
|
954
|
+
fallbackSkipped: `Workflow node limit reached after ${node.id}.`,
|
|
955
|
+
};
|
|
956
|
+
workflowStore.setRun(run);
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
let fallbackNode = this.createFallbackNode(node, fallbackAgent, reason);
|
|
961
|
+
let collision = 1;
|
|
962
|
+
while (workflow.nodes.some((candidate) => candidate.id === fallbackNode.id)) {
|
|
963
|
+
collision += 1;
|
|
964
|
+
fallbackNode = {
|
|
965
|
+
...fallbackNode,
|
|
966
|
+
id: `${fallbackNode.id}_${collision}`,
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const nodeIndex = workflow.nodes.findIndex((candidate) => candidate.id === node.id);
|
|
971
|
+
const runIndex = run.nodeRuns.findIndex((candidate) => candidate.nodeId === node.id);
|
|
972
|
+
if (nodeIndex >= 0) {
|
|
973
|
+
workflow.nodes.splice(nodeIndex + 1, 0, fallbackNode);
|
|
974
|
+
} else {
|
|
975
|
+
workflow.nodes.push(fallbackNode);
|
|
976
|
+
}
|
|
977
|
+
if (runIndex >= 0) {
|
|
978
|
+
run.nodeRuns.splice(runIndex + 1, 0, nodeRunFromNode(fallbackNode));
|
|
979
|
+
} else {
|
|
980
|
+
run.nodeRuns.push(nodeRunFromNode(fallbackNode));
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const fallbackEvents = Array.isArray(run.metadata?.fallbackEvents)
|
|
984
|
+
? run.metadata.fallbackEvents
|
|
985
|
+
: [];
|
|
986
|
+
run.metadata = {
|
|
987
|
+
...run.metadata,
|
|
988
|
+
fallbackEvents: [
|
|
989
|
+
...fallbackEvents,
|
|
990
|
+
{
|
|
991
|
+
nodeId: node.id,
|
|
992
|
+
fallbackNodeId: fallbackNode.id,
|
|
993
|
+
fallbackAgentInstanceId: fallbackAgent.instanceId,
|
|
994
|
+
reason,
|
|
995
|
+
startedAt: Date.now(),
|
|
996
|
+
},
|
|
997
|
+
],
|
|
998
|
+
};
|
|
999
|
+
workflowStore.setRun(run);
|
|
1000
|
+
|
|
1001
|
+
await this.executeNode(fallbackNode, workflow, run, outputs, started, completed);
|
|
1002
|
+
|
|
1003
|
+
const fallbackRun = run.nodeRuns.find((candidate) => candidate.nodeId === fallbackNode.id);
|
|
1004
|
+
if (fallbackRun?.status !== 'completed') {
|
|
1005
|
+
return false;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const fallbackOutput = outputs.get(fallbackNode.id) || fallbackRun.outputText;
|
|
1009
|
+
if (fallbackOutput) {
|
|
1010
|
+
outputs.set(node.id, compactOutputForContext(fallbackOutput));
|
|
1011
|
+
}
|
|
1012
|
+
completed.add(node.id);
|
|
1013
|
+
workflowStore.setRun(run);
|
|
1014
|
+
return true;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
899
1017
|
private maybeAddRepairCycle(
|
|
900
1018
|
node: WorkflowNode,
|
|
901
1019
|
workflow: Workflow,
|
|
@@ -1087,40 +1205,56 @@ class WorkflowRunner {
|
|
|
1087
1205
|
const isolation = readIsolation(settings.isolation) ?? node.isolation ?? 'host';
|
|
1088
1206
|
const keepAfterCompletion = readBoolean(settings.keepWorkspace) ?? true;
|
|
1089
1207
|
const baseRef = readString(settings.baseRef) ?? 'HEAD';
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
workspace: {
|
|
1113
|
-
kind: isolation,
|
|
1208
|
+
let body: { id?: string; error?: { message?: string } };
|
|
1209
|
+
try {
|
|
1210
|
+
const submit = await fetch(`${localA2ABaseUrl()}/tasks`, {
|
|
1211
|
+
method: 'POST',
|
|
1212
|
+
headers: { 'content-type': 'application/json' },
|
|
1213
|
+
body: JSON.stringify({
|
|
1214
|
+
adapterId: node.adapterId,
|
|
1215
|
+
contextId: run.contextId,
|
|
1216
|
+
message: {
|
|
1217
|
+
messageId: newId('msg'),
|
|
1218
|
+
role: 'user',
|
|
1219
|
+
parts: [{ kind: 'text', text: prompt }],
|
|
1220
|
+
},
|
|
1221
|
+
metadata: {
|
|
1222
|
+
workflowRunId: run.id,
|
|
1223
|
+
workflowNodeId: node.id,
|
|
1224
|
+
agentInstanceId: node.agentInstanceId,
|
|
1225
|
+
agentLabel: node.agentLabel,
|
|
1226
|
+
assignment: node.assignment,
|
|
1227
|
+
model: node.model,
|
|
1228
|
+
permissionMode: node.permissionMode,
|
|
1229
|
+
toolsSettings: node.toolsSettings,
|
|
1114
1230
|
projectPath,
|
|
1115
|
-
|
|
1116
|
-
|
|
1231
|
+
workspaceTarget: workspaceTargetMetadata(workspaceTarget),
|
|
1232
|
+
workspace: {
|
|
1233
|
+
kind: isolation,
|
|
1234
|
+
projectPath,
|
|
1235
|
+
baseRef,
|
|
1236
|
+
keepAfterCompletion,
|
|
1237
|
+
},
|
|
1117
1238
|
},
|
|
1118
|
-
},
|
|
1119
|
-
})
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1239
|
+
}),
|
|
1240
|
+
});
|
|
1241
|
+
body = await submit.json() as { id?: string; error?: { message?: string } };
|
|
1242
|
+
if (!submit.ok || !body.id) {
|
|
1243
|
+
throw new Error(body.error?.message ?? `Workflow node ${node.id} submit failed.`);
|
|
1244
|
+
}
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
nodeRun.finishedAt = Date.now();
|
|
1247
|
+
nodeRun.status = 'failed';
|
|
1248
|
+
nodeRun.error = error instanceof Error ? error.message : String(error);
|
|
1249
|
+
workflowStore.setRun(run);
|
|
1250
|
+
if (await this.runFallbackAfterFailure(node, workflow, run, outputs, started, completed, nodeRun.error)) {
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
if (node.onFail === 'continue') {
|
|
1254
|
+
completed.add(node.id);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
throw error;
|
|
1124
1258
|
}
|
|
1125
1259
|
nodeRun.a2aTaskId = body.id;
|
|
1126
1260
|
workflowStore.setRun(run);
|
|
@@ -1160,6 +1294,9 @@ class WorkflowRunner {
|
|
|
1160
1294
|
outputs.set(node.id, compactOutputForContext(nodeRun.outputText));
|
|
1161
1295
|
}
|
|
1162
1296
|
workflowStore.setRun(run);
|
|
1297
|
+
if (await this.runFallbackAfterFailure(node, workflow, run, outputs, started, completed, nodeRun.error)) {
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1163
1300
|
if (node.onFail === 'continue') {
|
|
1164
1301
|
completed.add(node.id);
|
|
1165
1302
|
return;
|
|
@@ -1192,6 +1329,9 @@ class WorkflowRunner {
|
|
|
1192
1329
|
nodeRun.status = 'failed';
|
|
1193
1330
|
nodeRun.error = result.error ?? `A2A task ended with ${result.state}`;
|
|
1194
1331
|
workflowStore.setRun(run);
|
|
1332
|
+
if (await this.runFallbackAfterFailure(node, workflow, run, outputs, started, completed, nodeRun.error)) {
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1195
1335
|
if (node.onFail === 'continue') {
|
|
1196
1336
|
if (nodeRun.outputText) {
|
|
1197
1337
|
outputs.set(node.id, compactOutputForContext(nodeRun.outputText));
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
|
|
15
15
|
// @ts-ignore — plain-JS service
|
|
16
16
|
import { getProviderModels, clearProviderModelCache } from '@/services/provider-models.js';
|
|
17
|
+
// @ts-ignore — plain-JS service
|
|
18
|
+
import { getProviderCliVersionStatus } from '@/services/provider-cli-versions.js';
|
|
17
19
|
|
|
18
20
|
// @ts-ignore — plain-JS service
|
|
19
21
|
import {
|
|
@@ -262,8 +264,13 @@ router.get(
|
|
|
262
264
|
'/:provider/auth/status',
|
|
263
265
|
asyncHandler(async (req: Request, res: Response) => {
|
|
264
266
|
const provider = parseProvider(req.params.provider);
|
|
267
|
+
const forceRefresh = String(req.query.refresh || '').toLowerCase() === '1';
|
|
265
268
|
const status = await providerAuthService.getProviderAuthStatus(provider);
|
|
266
|
-
|
|
269
|
+
const cliVersion = await getProviderCliVersionStatus(provider, {
|
|
270
|
+
installed: status.installed,
|
|
271
|
+
forceRefresh,
|
|
272
|
+
});
|
|
273
|
+
res.json(createApiSuccessResponse({ ...status, ...cliVersion }));
|
|
267
274
|
}),
|
|
268
275
|
);
|
|
269
276
|
|
package/server/routes/agent.js
CHANGED
|
@@ -19,6 +19,7 @@ import { CODEX_MODELS } from '../../shared/modelConstants.js';
|
|
|
19
19
|
import { IS_PLATFORM } from '../constants/config.js';
|
|
20
20
|
|
|
21
21
|
const router = express.Router();
|
|
22
|
+
const isPixcodeApiKey = (token) => typeof token === 'string' && (token.startsWith('px_') || token.startsWith('ck_'));
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Middleware to authenticate agent API requests.
|
|
@@ -49,18 +50,18 @@ const validateExternalApiKey = (req, res, next) => {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
// Self-hosted mode: validate API key from any of the supported transports.
|
|
52
|
-
// - Authorization: Bearer
|
|
53
|
+
// - Authorization: Bearer px_... (legacy ck_... still accepted)
|
|
53
54
|
// auth shape as the rest of the API, per the auth-unify in this turn)
|
|
54
|
-
// - X-API-Key:
|
|
55
|
-
// - ?apiKey=
|
|
55
|
+
// - X-API-Key: px_...
|
|
56
|
+
// - ?apiKey=px_... (EventSource workaround)
|
|
56
57
|
const authHeader = req.headers['authorization'];
|
|
57
58
|
const bearer = authHeader && authHeader.startsWith('Bearer ') ? authHeader.slice(7).trim() : null;
|
|
58
|
-
const apiKey = (bearer
|
|
59
|
+
const apiKey = (isPixcodeApiKey(bearer) ? bearer : null)
|
|
59
60
|
|| req.headers['x-api-key']
|
|
60
61
|
|| (typeof req.query.apiKey === 'string' ? req.query.apiKey : null);
|
|
61
62
|
|
|
62
63
|
if (!apiKey) {
|
|
63
|
-
return res.status(401).json({ error: 'API key required (Authorization: Bearer
|
|
64
|
+
return res.status(401).json({ error: 'API key required (Authorization: Bearer px_..., X-API-Key, or ?apiKey=)' });
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
const user = apiKeysDb.validateApiKey(apiKey);
|
|
@@ -230,6 +231,61 @@ function validateBranchName(branchName) {
|
|
|
230
231
|
return { valid: true };
|
|
231
232
|
}
|
|
232
233
|
|
|
234
|
+
function providerDisplayName(provider) {
|
|
235
|
+
return ({
|
|
236
|
+
claude: 'Claude',
|
|
237
|
+
cursor: 'Cursor',
|
|
238
|
+
codex: 'Codex',
|
|
239
|
+
gemini: 'Gemini',
|
|
240
|
+
qwen: 'Qwen',
|
|
241
|
+
opencode: 'OpenCode',
|
|
242
|
+
})[provider] || 'Provider';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function describeProviderFailure(rawError, provider) {
|
|
246
|
+
const rawMessage = String(rawError || '').trim() || 'Provider returned no assistant text.';
|
|
247
|
+
const normalized = rawMessage.toLowerCase();
|
|
248
|
+
const name = providerDisplayName(provider);
|
|
249
|
+
|
|
250
|
+
const details = {
|
|
251
|
+
provider,
|
|
252
|
+
providerName: name,
|
|
253
|
+
category: 'provider_error',
|
|
254
|
+
title: `${name} could not answer.`,
|
|
255
|
+
action: 'Check the provider output, then retry with a shorter prompt or a different model.',
|
|
256
|
+
rawMessage,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
if (/(balance|billing|quota|credit|insufficient|payment required|402|usage limit|spend limit)/i.test(rawMessage)) {
|
|
260
|
+
details.category = 'quota';
|
|
261
|
+
details.title = `${name} could not answer because the account has no available balance or quota.`;
|
|
262
|
+
details.action = 'Add credits, increase the provider usage limit, or switch to a free/available model.';
|
|
263
|
+
} else if (/(rate limit|too many requests|429|temporarily overloaded|resource exhausted)/i.test(rawMessage)) {
|
|
264
|
+
details.category = 'rate_limit';
|
|
265
|
+
details.title = `${name} is rate limited right now.`;
|
|
266
|
+
details.action = 'Wait a bit, reduce parallel runs, or switch to another provider/model.';
|
|
267
|
+
} else if (/(unauthorized|forbidden|permission_denied|permission denied|api key|token|oauth|login|not authenticated|401|403|invalid credentials)/i.test(rawMessage)) {
|
|
268
|
+
details.category = 'auth';
|
|
269
|
+
details.title = `${name} is not authenticated or the selected model is not allowed.`;
|
|
270
|
+
details.action = 'Reconnect this provider in Settings, refresh the CLI login, or choose a model enabled for the account.';
|
|
271
|
+
} else if (/(not installed|command not found|enoent|spawn .* enoent|executable file not found)/i.test(rawMessage)) {
|
|
272
|
+
details.category = 'missing_cli';
|
|
273
|
+
details.title = `${name} CLI is not installed or not on PATH.`;
|
|
274
|
+
details.action = 'Install the CLI from Settings -> Agents or set the matching CLI path environment variable.';
|
|
275
|
+
} else if (/(timeout|timed out|aborted|etimedout|deadline)/i.test(rawMessage)) {
|
|
276
|
+
details.category = 'timeout';
|
|
277
|
+
details.title = `${name} timed out before returning a complete answer.`;
|
|
278
|
+
details.action = 'Retry with a shorter request, reduce orchestration parallelism, or inspect the provider session log.';
|
|
279
|
+
} else if (normalized.includes('no assistant text') || normalized.includes('empty')) {
|
|
280
|
+
details.category = 'no_output';
|
|
281
|
+
details.title = `${name} finished without visible assistant text.`;
|
|
282
|
+
details.action = 'Retry once; if it repeats, check provider stderr/session logs because the CLI may have exited before streaming text.';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
details.message = `${details.title} ${details.action}`;
|
|
286
|
+
return details;
|
|
287
|
+
}
|
|
288
|
+
|
|
233
289
|
/**
|
|
234
290
|
* Get recent commit messages from a repository
|
|
235
291
|
* @param {string} projectPath - Path to the git repository
|
|
@@ -1276,6 +1332,12 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
1276
1332
|
(m) => m.type === 'assistant' && m.message?.content?.some?.((p) => p.type === 'text' && p.text)
|
|
1277
1333
|
);
|
|
1278
1334
|
const succeeded = !errorEntry && (hasAssistantText || assistantMessages.some((m) => m.type === 'tool_use' || m.type === 'tool_result'));
|
|
1335
|
+
const failureDetails = succeeded
|
|
1336
|
+
? null
|
|
1337
|
+
: describeProviderFailure(
|
|
1338
|
+
errorEntry?.content || 'Provider returned no assistant text. Check backend log for details.',
|
|
1339
|
+
provider,
|
|
1340
|
+
);
|
|
1279
1341
|
|
|
1280
1342
|
const response = {
|
|
1281
1343
|
success: succeeded,
|
|
@@ -1284,10 +1346,10 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
1284
1346
|
tokens: tokenSummary,
|
|
1285
1347
|
projectPath: finalProjectPath
|
|
1286
1348
|
};
|
|
1287
|
-
if (
|
|
1288
|
-
response.error =
|
|
1289
|
-
|
|
1290
|
-
response.
|
|
1349
|
+
if (failureDetails) {
|
|
1350
|
+
response.error = failureDetails.message;
|
|
1351
|
+
response.rawError = failureDetails.rawMessage;
|
|
1352
|
+
response.errorDetails = failureDetails;
|
|
1291
1353
|
}
|
|
1292
1354
|
|
|
1293
1355
|
// Add branch/PR info if created
|
|
@@ -1353,10 +1415,13 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|
|
1353
1415
|
if (errEntry) collectedError = errEntry.content;
|
|
1354
1416
|
} catch { /* ignore — fall back to error.message */ }
|
|
1355
1417
|
}
|
|
1418
|
+
const failureDetails = describeProviderFailure(collectedError || error.message, provider);
|
|
1356
1419
|
res.status(502).json({
|
|
1357
1420
|
success: false,
|
|
1358
1421
|
sessionId: writer && typeof writer.getSessionId === 'function' ? writer.getSessionId() : null,
|
|
1359
|
-
error:
|
|
1422
|
+
error: failureDetails.message,
|
|
1423
|
+
rawError: failureDetails.rawMessage,
|
|
1424
|
+
errorDetails: failureDetails,
|
|
1360
1425
|
wrapperError: collectedError ? error.message : undefined,
|
|
1361
1426
|
messages: collectedMessages,
|
|
1362
1427
|
});
|
|
@@ -7,7 +7,8 @@ import { notifyUser as notifyTelegramUser } from './telegram/bot.js';
|
|
|
7
7
|
const KIND_TO_PREF_KEY = {
|
|
8
8
|
action_required: 'actionRequired',
|
|
9
9
|
stop: 'stop',
|
|
10
|
-
error: 'error'
|
|
10
|
+
error: 'error',
|
|
11
|
+
update: 'updates'
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
const PROVIDER_LABELS = {
|
|
@@ -15,6 +16,8 @@ const PROVIDER_LABELS = {
|
|
|
15
16
|
cursor: 'Cursor',
|
|
16
17
|
codex: 'Codex',
|
|
17
18
|
gemini: 'Gemini',
|
|
19
|
+
qwen: 'Qwen Code',
|
|
20
|
+
opencode: 'OpenCode',
|
|
18
21
|
system: 'System'
|
|
19
22
|
};
|
|
20
23
|
|
|
@@ -121,7 +124,13 @@ function buildPushBody(event) {
|
|
|
121
124
|
'run.stopped': event.meta?.stopReason || 'Run Stopped: The run has stopped',
|
|
122
125
|
'run.failed': event.meta?.error ? `Run Failed: ${event.meta.error}` : 'Run Failed: The run encountered an error',
|
|
123
126
|
'agent.notification': event.meta?.message ? String(event.meta.message) : 'You have a new notification',
|
|
124
|
-
'push.enabled': 'Push notifications are now enabled!'
|
|
127
|
+
'push.enabled': 'Push notifications are now enabled!',
|
|
128
|
+
'app.update.available': event.meta?.latestVersion
|
|
129
|
+
? `Pixcode ${event.meta.latestVersion} is available`
|
|
130
|
+
: 'A Pixcode update is available',
|
|
131
|
+
'cli.update.available': event.meta?.latestVersion
|
|
132
|
+
? `CLI update available: ${event.meta.latestVersion}`
|
|
133
|
+
: 'A CLI update is available'
|
|
125
134
|
};
|
|
126
135
|
const providerLabel = PROVIDER_LABELS[event.provider] || 'Assistant';
|
|
127
136
|
const sessionName = resolveSessionName(event);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
5
|
+
const cache = new Map();
|
|
6
|
+
const inflight = new Map();
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
|
|
9
|
+
const providerConfigs = {
|
|
10
|
+
claude: {
|
|
11
|
+
command: () => process.env.CLAUDE_CLI_PATH || 'claude',
|
|
12
|
+
args: ['--version'],
|
|
13
|
+
packageName: '@anthropic-ai/claude-code',
|
|
14
|
+
},
|
|
15
|
+
cursor: {
|
|
16
|
+
command: () => process.env.CURSOR_CLI_PATH || 'cursor-agent',
|
|
17
|
+
args: ['--version'],
|
|
18
|
+
packageName: null,
|
|
19
|
+
},
|
|
20
|
+
codex: {
|
|
21
|
+
command: () => process.env.CODEX_CLI_PATH || 'codex',
|
|
22
|
+
args: ['--version'],
|
|
23
|
+
packageName: '@openai/codex',
|
|
24
|
+
},
|
|
25
|
+
gemini: {
|
|
26
|
+
command: () => process.env.GEMINI_CLI_PATH || 'gemini',
|
|
27
|
+
args: ['--version'],
|
|
28
|
+
packageName: '@google/gemini-cli',
|
|
29
|
+
},
|
|
30
|
+
qwen: {
|
|
31
|
+
command: () => process.env.QWEN_CLI_PATH || 'qwen',
|
|
32
|
+
args: ['--version'],
|
|
33
|
+
packageName: '@qwen-code/qwen-code',
|
|
34
|
+
},
|
|
35
|
+
opencode: {
|
|
36
|
+
command: () => process.env.OPENCODE_CLI_PATH || 'opencode',
|
|
37
|
+
args: ['--version'],
|
|
38
|
+
packageName: 'opencode-ai',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function normalizeVersion(value) {
|
|
43
|
+
const match = String(value || '').match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
|
|
44
|
+
return match?.[0] || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function compareVersions(left, right) {
|
|
48
|
+
const a = String(left || '0.0.0').replace(/^v/, '').split(/[.+-]/).slice(0, 3).map(Number);
|
|
49
|
+
const b = String(right || '0.0.0').replace(/^v/, '').split(/[.+-]/).slice(0, 3).map(Number);
|
|
50
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
51
|
+
const av = Number.isFinite(a[index]) ? a[index] : 0;
|
|
52
|
+
const bv = Number.isFinite(b[index]) ? b[index] : 0;
|
|
53
|
+
if (av !== bv) return av - bv;
|
|
54
|
+
}
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function readInstalledVersion(config) {
|
|
59
|
+
try {
|
|
60
|
+
const result = await execFileAsync(config.command(), config.args, {
|
|
61
|
+
encoding: 'utf8',
|
|
62
|
+
timeout: 5000,
|
|
63
|
+
windowsHide: true,
|
|
64
|
+
maxBuffer: 64 * 1024,
|
|
65
|
+
});
|
|
66
|
+
return normalizeVersion(`${result.stdout || ''}\n${result.stderr || ''}`);
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function readLatestVersion(packageName) {
|
|
73
|
+
if (!packageName) return null;
|
|
74
|
+
let result;
|
|
75
|
+
try {
|
|
76
|
+
result = await execFileAsync('npm', ['view', packageName, 'version', '--json'], {
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
timeout: 7000,
|
|
79
|
+
windowsHide: true,
|
|
80
|
+
maxBuffer: 64 * 1024,
|
|
81
|
+
});
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(result.stdout);
|
|
88
|
+
return normalizeVersion(parsed);
|
|
89
|
+
} catch {
|
|
90
|
+
return normalizeVersion(result.stdout);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getSkipReason(config, installedVersion, latestVersion) {
|
|
95
|
+
if (!config.packageName) return 'external_installer';
|
|
96
|
+
if (!installedVersion) return 'installed_version_unavailable';
|
|
97
|
+
if (!latestVersion) return 'latest_version_unavailable';
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function resolveProviderCliVersionStatus(provider, config, now) {
|
|
102
|
+
const installedVersion = await readInstalledVersion(config);
|
|
103
|
+
const latestVersion = await readLatestVersion(config.packageName);
|
|
104
|
+
const updateAvailable = Boolean(
|
|
105
|
+
installedVersion
|
|
106
|
+
&& latestVersion
|
|
107
|
+
&& compareVersions(latestVersion, installedVersion) > 0,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const payload = {
|
|
111
|
+
checkedAt: new Date(now).toISOString(),
|
|
112
|
+
installedVersion,
|
|
113
|
+
latestVersion,
|
|
114
|
+
updateAvailable,
|
|
115
|
+
versionCheckSkipped: getSkipReason(config, installedVersion, latestVersion),
|
|
116
|
+
};
|
|
117
|
+
cache.set(provider, { checkedAtMs: now, payload });
|
|
118
|
+
return payload;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function getProviderCliVersionStatus(provider, { installed = true, forceRefresh = false } = {}) {
|
|
122
|
+
const config = providerConfigs[provider];
|
|
123
|
+
if (!config || !installed) {
|
|
124
|
+
return {
|
|
125
|
+
checkedAt: new Date().toISOString(),
|
|
126
|
+
installedVersion: null,
|
|
127
|
+
latestVersion: null,
|
|
128
|
+
updateAvailable: false,
|
|
129
|
+
versionCheckSkipped: !config ? 'unsupported_provider' : 'not_installed',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const now = Date.now();
|
|
134
|
+
const cached = cache.get(provider);
|
|
135
|
+
if (!forceRefresh && cached && now - cached.checkedAtMs < ONE_DAY_MS) {
|
|
136
|
+
return { ...cached.payload, fromCache: true };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const inflightKey = provider;
|
|
140
|
+
if (!forceRefresh && inflight.has(inflightKey)) {
|
|
141
|
+
return inflight.get(inflightKey);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const promise = resolveProviderCliVersionStatus(provider, config, now).finally(() => {
|
|
145
|
+
inflight.delete(inflightKey);
|
|
146
|
+
});
|
|
147
|
+
inflight.set(inflightKey, promise);
|
|
148
|
+
return promise;
|
|
149
|
+
}
|