@tbrandenburg/node-red-agents 0.3.6 → 0.3.7

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.
@@ -250,6 +250,14 @@ module.exports = function (RED) {
250
250
  timedOut: result.timedOut,
251
251
  durationMs: result.durationMs,
252
252
  sessionID: result.sessionID,
253
+ // Raw error object from the adapter (e.g. opencode's full
254
+ // {"type":"error"} payload, or pi's failing assistant
255
+ // message) when the run failed -- the `done(err)` string
256
+ // below only carries a single summarized message/name, so
257
+ // anything needing the fuller detail (extra fields the
258
+ // adapter didn't fold into errorMessage) should wire a
259
+ // Debug node to output 1 and inspect this field.
260
+ errorDetail: result.errorDetail,
253
261
  },
254
262
  });
255
263
  send([resultMsg, null]);
@@ -119,11 +119,25 @@ class OpenCodeAdapter extends AgentAdapter {
119
119
  .trim();
120
120
 
121
121
  if (errorEvent) {
122
+ const errDetail = errorEvent.error || {};
122
123
  const message =
123
- (errorEvent.error &&
124
- ((errorEvent.error.data && errorEvent.error.data.message) || errorEvent.error.name)) ||
125
- "opencode reported an error";
126
- return { payload, sessionID, status: "failed", errorMessage: message };
124
+ (errDetail.data && errDetail.data.message) || errDetail.name || "opencode reported an error";
125
+ // The JSON error event only carries opencode's own top-level
126
+ // message/name -- append name (if distinct) and any stderr output
127
+ // opencode wrote alongside it, since both would otherwise be
128
+ // silently dropped here (unlike the exitCode!==0 branch below,
129
+ // which already surfaces stderr).
130
+ const extras = [];
131
+ if (errDetail.name && errDetail.name !== message) extras.push(errDetail.name);
132
+ if (stderr && String(stderr).trim()) extras.push(String(stderr).trim());
133
+ const errorMessage = extras.length ? `${message} (${extras.join("; ")})` : message;
134
+ return {
135
+ payload,
136
+ sessionID,
137
+ status: "failed",
138
+ errorMessage,
139
+ errorDetail: errDetail,
140
+ };
127
141
  }
128
142
  if (signal) {
129
143
  return {
@@ -186,9 +186,10 @@ class PiAdapter extends AgentAdapter {
186
186
 
187
187
  let payload = "";
188
188
  let errorMessage;
189
+ let lastAssistant;
189
190
 
190
191
  if (agentEnd && Array.isArray(agentEnd.messages)) {
191
- const lastAssistant = [...agentEnd.messages].reverse().find((m) => m.role === "assistant");
192
+ lastAssistant = [...agentEnd.messages].reverse().find((m) => m.role === "assistant");
192
193
  if (lastAssistant) {
193
194
  if (lastAssistant.stopReason === "error") {
194
195
  // pi does NOT set a non-zero exit code for a model/API
@@ -207,7 +208,17 @@ class PiAdapter extends AgentAdapter {
207
208
  }
208
209
 
209
210
  if (errorMessage) {
210
- return { payload, sessionID, status: "failed", errorMessage };
211
+ // Same rationale as the opencode adapter: stderr is otherwise
212
+ // silently dropped for this branch (it's only appended in the
213
+ // exitCode!==0 branch below).
214
+ const extra = stderr && String(stderr).trim() ? ` (${String(stderr).trim()})` : "";
215
+ return {
216
+ payload,
217
+ sessionID,
218
+ status: "failed",
219
+ errorMessage: `${errorMessage}${extra}`,
220
+ errorDetail: lastAssistant,
221
+ };
211
222
  }
212
223
  if (signal) {
213
224
  return {
@@ -49,7 +49,23 @@ configuration for that message.
49
49
 
50
50
  A non-zero exit code, a timeout, or a missing `gh` executable all go through
51
51
  `done(error)` (catchable with a Catch node) instead of producing an output
52
- message.
52
+ message. Every such error carries an `err.errorType` string so flows can
53
+ branch on it without regex-matching gh's (locale/version dependent) human
54
+ -readable stderr text themselves:
55
+
56
+ | `errorType` | When |
57
+ | ------------------- | ------------------------------------------------------------------------------ |
58
+ | `auth` | Not logged in / bad credentials (stderr mentions `gh auth login`, etc.) |
59
+ | `rate-limit` | GitHub API (primary or secondary) rate limit exceeded |
60
+ | `feature-disabled` | Repo has issues/PRs/projects disabled (e.g. this ticket's original report) |
61
+ | `not-found` | Repository/resource doesn't exist or can't be resolved |
62
+ | `permission` | 403 / insufficient permissions |
63
+ | `network` | Network failure or timeout talking to GitHub |
64
+ | `not-installed` | `gh` executable not found on `PATH` (spawn `ENOENT`) |
65
+ | `spawn-failed` | The child process could not be started for another reason |
66
+ | `timeout` | The command exceeded the configured Timeout and was killed |
67
+ | `killed` | The command was killed by a signal other than the node's own timeout |
68
+ | `unknown` | Non-zero exit whose stderr didn't match any known pattern |
53
69
 
54
70
  ## Example
55
71
 
package/nodes/gh/gh.js CHANGED
@@ -38,6 +38,27 @@ function validateCommand(command) {
38
38
  return null;
39
39
  }
40
40
 
41
+ // Classifies a gh stderr message into a stable, machine-checkable category so
42
+ // downstream flows can branch on `err.errorType` instead of regex-matching
43
+ // the (locale/version dependent) human-readable message themselves. Order
44
+ // matters: more specific patterns are checked before generic ones.
45
+ const ERROR_TYPE_PATTERNS = [
46
+ [/not logged into|to authenticate|gh auth login|authentication required|bad credentials/i, "auth"],
47
+ [/api rate limit exceeded|secondary rate limit/i, "rate-limit"],
48
+ [/has disabled issues|has disabled pull requests|has disabled projects/i, "feature-disabled"],
49
+ [/could not resolve to a repository|repository not found|404/i, "not-found"],
50
+ [/403|resource not accessible|must have admin rights|permission/i, "permission"],
51
+ [/network|timed out|econnreset|enotfound/i, "network"],
52
+ ];
53
+
54
+ function classifyGhError(stderr) {
55
+ if (!stderr) return "unknown";
56
+ for (const [pattern, type] of ERROR_TYPE_PATTERNS) {
57
+ if (pattern.test(stderr)) return type;
58
+ }
59
+ return "unknown";
60
+ }
61
+
41
62
  // Resolves an "args" value (from config or msg.gh.args) into a string[].
42
63
  // Strings are tokenized (quote-aware, no shell evaluation); arrays are used
43
64
  // as-is (each element coerced to a string); anything else is rejected.
@@ -172,6 +193,7 @@ module.exports = function (RED) {
172
193
  const wrapped = new Error("gh: " + message);
173
194
  wrapped.command = command;
174
195
  wrapped.args = args;
196
+ wrapped.errorType = err && err.code === "ENOENT" ? "not-installed" : "spawn-failed";
175
197
  done(wrapped);
176
198
  });
177
199
 
@@ -188,19 +210,23 @@ module.exports = function (RED) {
188
210
  err.args = args;
189
211
  err.signal = signal;
190
212
  err.timedOut = timedOut;
213
+ err.errorType = timedOut ? "timeout" : "killed";
191
214
  done(err);
192
215
  return;
193
216
  }
194
217
 
195
218
  if (code !== 0) {
196
- node.status({ fill: "red", shape: "dot", text: "exit " + code });
219
+ const trimmedStderr = stderr.trim();
220
+ const errorType = classifyGhError(trimmedStderr);
221
+ node.status({ fill: "red", shape: "dot", text: errorType + " (exit " + code + ")" });
197
222
  const err = new Error(
198
- "gh: command failed (exit " + code + ")" + (stderr.trim() ? ": " + stderr.trim() : ""),
223
+ "gh: command failed (exit " + code + ")" + (trimmedStderr ? ": " + trimmedStderr : ""),
199
224
  );
200
225
  err.command = command;
201
226
  err.args = args;
202
227
  err.exitCode = code;
203
- err.stderr = stderr.trim();
228
+ err.stderr = trimmedStderr;
229
+ err.errorType = errorType;
204
230
  done(err);
205
231
  return;
206
232
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tbrandenburg/node-red-agents",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "Node-RED nodes for running coding agents (opencode, pi) and GitHub CLI operations from flows.",
5
5
  "keywords": [
6
6
  "node-red",