@agentprojectcontext/apx 1.61.0 → 1.62.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.61.0",
3
+ "version": "1.62.0",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -60,6 +60,12 @@ export async function runSuperAgent({
60
60
  // Null disables human-in-the-loop (tools that need confirmation fail
61
61
  // immediately instead of waiting for user input).
62
62
  requestConfirmation = null,
63
+ // A2A callback sink: when a background tool (call_runtime) finishes out of
64
+ // band, it feeds the result back into the super-agent via this function so
65
+ // the agent — not a raw dump — relays it to the user. Channel-specific
66
+ // (telegram wires it to a follow-up streamed turn). Null → tools fall back to
67
+ // a direct channel send. See call-runtime.js.
68
+ backgroundResultSink = null,
63
69
  // When true, suppress the static "Available skills" slug-dump hint block
64
70
  // because a per-turn skill inspector already injected the right context.
65
71
  // Set by the daemon's super-agent endpoint when config.skills.inspector is on.
@@ -131,7 +137,7 @@ export async function runSuperAgent({
131
137
  overrideModel,
132
138
  toolSchemas,
133
139
  makeToolHandlers,
134
- toolHandlerCtx: { projects, plugins, registries, globalConfig, channel, toolSession, requestConfirmation },
140
+ toolHandlerCtx: { projects, plugins, registries, globalConfig, channel, channelMeta, toolSession, requestConfirmation, backgroundResultSink },
135
141
  onEvent,
136
142
  signal,
137
143
  onToken,
@@ -8,6 +8,7 @@ import {
8
8
  createRuntimeSession,
9
9
  extractRuntimeResult as extractApfResult,
10
10
  } from "#core/stores/runtime-sessions.js";
11
+ import { writePendingCallback, deletePendingCallback } from "#core/stores/runtime-callbacks.js";
11
12
  import { buildRuntimeBridgeHint as buildApfHint } from "#core/agent/runtime-bridge.js";
12
13
  import { detectAll } from "#core/runtimes/detect.js";
13
14
  import {
@@ -167,15 +168,38 @@ export default {
167
168
  type: "string",
168
169
  description: "Optional prior session id (claude/codex/apx) — APX prepends that session's title + last prompt to the prompt so the runtime has context.",
169
170
  },
170
- timeout_s: { type: "integer", description: "seconds before SIGTERM; default 300" },
171
+ timeout_s: { type: "integer", description: "seconds before SIGTERM. Foreground default 300; background runs default to 3600 (1h)." },
172
+ background: {
173
+ type: "boolean",
174
+ description: "Run detached instead of blocking this turn. On Telegram this is the DEFAULT (runtimes like claude-code can take many minutes to an hour; blocking would freeze the super-agent). Returns immediately with status:\"launched\"; when the runtime finishes, its result is delivered to this same chat as an automatic message. Set false ONLY when you genuinely need the runtime's output within THIS turn for an immediate follow-up (short tasks).",
175
+ },
171
176
  },
172
177
  required: ["runtime", "prompt"],
173
178
  },
174
179
  },
175
180
  },
176
- makeHandler: ({ projects, requirePermission }) => async ({ project, agent: slug, runtime, prompt, resume_session_id = null, timeout_s = 300, confirmed = false }) => {
181
+ makeHandler: ({ projects, requirePermission, plugins, channel, channelMeta, backgroundResultSink = null }) => async ({ project, agent: slug, runtime, prompt, resume_session_id = null, timeout_s = null, background = null, confirmed = false }) => {
177
182
  await requirePermission("call_runtime", { dangerous: true, confirmed, args: { runtime } });
178
183
 
184
+ // Async delivery sink: on Telegram we can push the runtime's result back to
185
+ // the originating chat when it finishes, so the call doesn't have to block
186
+ // the turn. channelMeta.chatId + the telegram plugin are what make that
187
+ // possible; without them there's nowhere to deliver a late result, so we
188
+ // stay synchronous (web/desktop/exec keep their current behavior).
189
+ const chatId = channelMeta?.chatId ?? null;
190
+ const tgChannelName = channelMeta?.channelName ?? null;
191
+ const telegramPlugin = channel === "telegram" && chatId != null ? plugins?.get?.("telegram") : null;
192
+ // We can report a late result either by re-entering the super-agent (A2A,
193
+ // preferred — the agent relays in its own voice) or by a direct channel
194
+ // send (fallback). Either makes background mode viable.
195
+ const canCallback = !!backgroundResultSink || !!telegramPlugin;
196
+ // Background by default when we can call back; the model opts out with
197
+ // background:false when it needs the output inside this same turn.
198
+ const runInBackground = canCallback && background !== false;
199
+ // Long runtimes (claude-code sessions) can run for an hour; a detached run
200
+ // must not be SIGTERM'd at the 5-min foreground default.
201
+ const effectiveTimeoutS = Number(timeout_s) || (runInBackground ? 3600 : 300);
202
+
179
203
  const p = slug ? resolveProjectForAgent(projects, project, slug) : resolveProject(projects, project);
180
204
  const agent = slug ? readAgents(p.path).find((a) => a.slug === slug) : null;
181
205
  if (slug && !agent) {
@@ -226,106 +250,191 @@ export default {
226
250
  project: p.path,
227
251
  resume_session_id: resume_session_id || null,
228
252
  resume_resolved: resume.meta ? `${resume.meta.engine}:${resume.meta.id}` : null,
229
- timeout_s,
253
+ timeout_s: effectiveTimeoutS,
254
+ background: runInBackground,
230
255
  });
231
256
 
232
- try {
233
- const r = await rt.run({
234
- system: buildRuntimeSystem(p, agent, runtime, session.id, "super_agent_tool"),
235
- prompt: effectivePrompt,
236
- cwd: p.path,
237
- timeoutMs: timeout_s * 1000,
238
- });
257
+ // Run the runtime to completion and finalize (close the session record, log
258
+ // the transcript, shape the result object). NEVER throws — a thrown spawn is
259
+ // caught and returned as an error object, so the background path can deliver
260
+ // it as a callback instead of crashing an un-awaited promise.
261
+ const runToCompletion = async () => {
262
+ try {
263
+ const r = await rt.run({
264
+ system: buildRuntimeSystem(p, agent, runtime, session.id, "super_agent_tool"),
265
+ prompt: effectivePrompt,
266
+ cwd: p.path,
267
+ timeoutMs: effectiveTimeoutS * 1000,
268
+ });
239
269
 
240
- const failure = runtimeLooksLikeFailure(r);
241
- const result = extractApfResult(r.output) || (r.output || "").slice(0, 200);
242
- closeRuntimeSession({
243
- filePath: session.path,
244
- externalSessionPath: r.externalSessionPath || null,
245
- exitCode: failure.failed && r.exitCode === 0 ? -1 : r.exitCode,
246
- result: failure.failed ? `failed: ${failure.reason}` : result,
247
- });
270
+ const failure = runtimeLooksLikeFailure(r);
271
+ const result = extractApfResult(r.output) || (r.output || "").slice(0, 200);
272
+ closeRuntimeSession({
273
+ filePath: session.path,
274
+ externalSessionPath: r.externalSessionPath || null,
275
+ exitCode: failure.failed && r.exitCode === 0 ? -1 : r.exitCode,
276
+ result: failure.failed ? `failed: ${failure.reason}` : result,
277
+ });
248
278
 
249
- p.logMessage({
250
- agent_slug: actor,
251
- channel: "runtime",
252
- direction: "in",
253
- author: "user",
254
- body: effectivePrompt,
255
- meta: { runtime, invoked_by: "super_agent_tool", apc_session: session.id, resume_session_id: resume_session_id || null },
256
- });
257
- p.logMessage({
258
- agent_slug: actor,
259
- channel: "runtime",
260
- direction: "out",
261
- type: "agent",
262
- actor_id: agent?.slug || runtime,
263
- actor_kind: agent?.slug ? "agent" : "engine",
264
- author: agent?.slug || runtime,
265
- body: r.output || "",
266
- meta: {
267
- runtime,
268
- exit_code: r.exitCode,
269
- external_session_path: r.externalSessionPath || null,
270
- session_id: r.sessionId || null,
271
- apc_session: session.id,
272
- invoked_by: "super_agent_tool",
273
- failed: failure.failed || false,
274
- failure_reason: failure.failed ? failure.reason : null,
275
- },
276
- });
279
+ p.logMessage({
280
+ agent_slug: actor,
281
+ channel: "runtime",
282
+ direction: "in",
283
+ author: "user",
284
+ body: effectivePrompt,
285
+ meta: { runtime, invoked_by: "super_agent_tool", apc_session: session.id, resume_session_id: resume_session_id || null },
286
+ });
287
+ p.logMessage({
288
+ agent_slug: actor,
289
+ channel: "runtime",
290
+ direction: "out",
291
+ type: "agent",
292
+ actor_id: agent?.slug || runtime,
293
+ actor_kind: agent?.slug ? "agent" : "engine",
294
+ author: agent?.slug || runtime,
295
+ body: r.output || "",
296
+ meta: {
297
+ runtime,
298
+ exit_code: r.exitCode,
299
+ external_session_path: r.externalSessionPath || null,
300
+ session_id: r.sessionId || null,
301
+ apc_session: session.id,
302
+ invoked_by: "super_agent_tool",
303
+ failed: failure.failed || false,
304
+ failure_reason: failure.failed ? failure.reason : null,
305
+ },
306
+ });
307
+
308
+ if (failure.failed) {
309
+ log.error(`${runtime} run failed: ${failure.reason}`, {
310
+ apc_session: session.id,
311
+ exit_code: r.exitCode,
312
+ stderr: String(r.stderr || "").slice(0, 500),
313
+ external_session_path: r.externalSessionPath || null,
314
+ });
315
+ return {
316
+ error: `runtime "${runtime}" did not complete successfully: ${failure.reason}`,
317
+ runtime,
318
+ agent: agent?.slug || null,
319
+ apc_session: session.id,
320
+ exit_code: r.exitCode,
321
+ stderr: (r.stderr || "").slice(0, 2000),
322
+ output: (r.output || "").slice(0, 2000),
323
+ external_session_path: r.externalSessionPath || null,
324
+ session_id: r.sessionId || null,
325
+ };
326
+ }
277
327
 
278
- if (failure.failed) {
279
- log.error(`${runtime} run failed: ${failure.reason}`, {
328
+ log.info(`${runtime} run ok`, {
280
329
  apc_session: session.id,
281
330
  exit_code: r.exitCode,
282
- stderr: String(r.stderr || "").slice(0, 500),
283
331
  external_session_path: r.externalSessionPath || null,
332
+ session_id: r.sessionId || null,
333
+ output_bytes: (r.output || "").length,
284
334
  });
335
+
285
336
  return {
286
- error: `runtime "${runtime}" did not complete successfully: ${failure.reason}`,
287
337
  runtime,
288
338
  agent: agent?.slug || null,
289
339
  apc_session: session.id,
290
340
  exit_code: r.exitCode,
341
+ result,
342
+ output: (r.output || "").slice(0, 4000),
291
343
  stderr: (r.stderr || "").slice(0, 2000),
292
- output: (r.output || "").slice(0, 2000),
344
+ truncated: (r.output || "").length > 4000,
293
345
  external_session_path: r.externalSessionPath || null,
294
346
  session_id: r.sessionId || null,
295
347
  };
348
+ } catch (e) {
349
+ log.error(`${runtime} run threw: ${e.message}`, { apc_session: session.id });
350
+ try {
351
+ closeRuntimeSession({
352
+ filePath: session.path,
353
+ exitCode: -1,
354
+ result: `error: ${e.message.slice(0, 200)}`,
355
+ });
356
+ } catch {}
357
+ return { error: `runtime "${runtime}" threw: ${e.message}`, runtime, agent: agent?.slug || null, apc_session: session.id };
296
358
  }
359
+ };
297
360
 
298
- log.info(`${runtime} run ok`, {
299
- apc_session: session.id,
300
- exit_code: r.exitCode,
301
- external_session_path: r.externalSessionPath || null,
302
- session_id: r.sessionId || null,
303
- output_bytes: (r.output || "").length,
304
- });
361
+ // Deliver a finished background run. Preferred path (A2A): feed the result
362
+ // back into the super-agent so it relays in its own voice / chains the next
363
+ // step — an internal agent-to-agent hand-off. Fallback: a direct channel
364
+ // send of the raw result. Best-effort: never throws (nothing awaits it).
365
+ const who = `${runtime}${agent ? ` (agente ${agent.slug})` : ""}`;
366
+ const deliverCallback = async (res) => {
367
+ // Take ownership of delivery: drop the durable IOU so the reconciler in
368
+ // another/next daemon can't also deliver this one. Fallbacks below still
369
+ // run in-process, so a delete here doesn't risk losing the callback.
370
+ deletePendingCallback(session.id);
371
+ const body = res.error ? "" : String(res.result || res.output || "").trim();
372
+
373
+ if (backgroundResultSink) {
374
+ // A2A report phrased for Roby (not the end user). Roby decides how to
375
+ // relay it and whether a next step is needed.
376
+ const report = res.error
377
+ ? `[callback A2A] La tarea que delegaste a ${who} (sesión ${session.id}) FALLÓ: ${res.error}. ` +
378
+ `Avisale al usuario en tu voz y proponé cómo seguir.`
379
+ : `[callback A2A] Terminó la tarea que delegaste a ${who} (sesión ${session.id}). ` +
380
+ `Resultado del agente:\n${body.slice(0, 6000)}\n\n` +
381
+ `Contale al usuario el resultado en tu voz, breve y claro. No vuelvas a delegar salvo que falte un paso siguiente.`;
382
+ try {
383
+ await backgroundResultSink(report);
384
+ log.info(`${runtime} callback relayed via A2A sink`, { apc_session: session.id });
385
+ return;
386
+ } catch (e) {
387
+ log.error(`${runtime} A2A sink failed, falling back to direct send: ${e.message}`, { apc_session: session.id });
388
+ }
389
+ }
390
+
391
+ if (!telegramPlugin) return;
392
+ const head = res.error
393
+ ? `⚠️ La sesión de ${who} terminó con error (sesión \`${session.id}\`): ${res.error}`
394
+ : `✅ Terminó la sesión de ${who} (\`${session.id}\`).`;
395
+ const text = body ? `${head}\n\n${body.slice(0, 3500)}` : head;
396
+ try {
397
+ await telegramPlugin.send({ channel: tgChannelName, chat_id: chatId, text });
398
+ log.info(`${runtime} callback delivered (direct)`, { apc_session: session.id, chat_id: chatId });
399
+ } catch (e) {
400
+ log.error(`${runtime} callback send failed: ${e.message}`, { apc_session: session.id });
401
+ }
402
+ };
305
403
 
404
+ if (runInBackground) {
405
+ // Durable IOU: if this daemon dies before the run finishes (crash, pull,
406
+ // or a task that restarts the daemon), the reconciler on the next daemon
407
+ // delivers the result from the session record. The in-process path below
408
+ // deletes it the moment it takes over. Only telegram delivery is wired.
409
+ if (chatId != null) {
410
+ writePendingCallback({
411
+ session_id: session.id,
412
+ session_path: session.path,
413
+ channel: "telegram",
414
+ chat_id: chatId,
415
+ tg_channel: tgChannelName,
416
+ runtime,
417
+ agent: agent?.slug || null,
418
+ who,
419
+ });
420
+ }
421
+ // Fire-and-forget: don't await into the turn. runToCompletion never
422
+ // rejects, so this promise is safe un-awaited; the result is pushed to the
423
+ // chat when the runtime exits.
424
+ runToCompletion().then(deliverCallback);
306
425
  return {
307
426
  runtime,
308
427
  agent: agent?.slug || null,
309
428
  apc_session: session.id,
310
- exit_code: r.exitCode,
311
- output: (r.output || "").slice(0, 4000),
312
- stderr: (r.stderr || "").slice(0, 2000),
313
- truncated: (r.output || "").length > 4000,
314
- external_session_path: r.externalSessionPath || null,
315
- session_id: r.sessionId || null,
429
+ status: "launched",
430
+ background: true,
431
+ note:
432
+ `La sesión de ${runtime} arrancó en segundo plano y puede tardar varios minutos u horas. ` +
433
+ `NO esperes ni vuelvas a llamar a call_runtime para esto: el resultado llegará AUTOMÁTICAMENTE a este chat cuando termine. ` +
434
+ `Respondé al usuario ahora, en una línea, avisándole que la lanzaste y que le vas a avisar acá cuando esté lista.`,
316
435
  };
317
- } catch (e) {
318
- log.error(`${runtime} run threw: ${e.message}`, {
319
- apc_session: session.id,
320
- });
321
- try {
322
- closeRuntimeSession({
323
- filePath: session.path,
324
- exitCode: -1,
325
- result: `error: ${e.message.slice(0, 200)}`,
326
- });
327
- } catch {}
328
- throw e;
329
436
  }
437
+
438
+ return await runToCompletion();
330
439
  },
331
440
  };
@@ -26,7 +26,7 @@ import * as askFlow from "./ask.js";
26
26
  import { telegramAuthorLabel } from "./helpers.js";
27
27
  import { handleIncomingPhoto } from "./inbound/photo.js";
28
28
  import { handleIncomingAudio } from "./inbound/audio.js";
29
- import { buildStreamHandler, runTelegramSuperAgent, telegramErrorText, sendFinalReply } from "./reply.js";
29
+ import { buildStreamHandler, runTelegramSuperAgent, telegramErrorText, sendFinalReply, runFollowupTurn } from "./reply.js";
30
30
  import { t, resolveLang } from "#core/i18n/index.js";
31
31
 
32
32
  const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
@@ -285,6 +285,28 @@ export async function handleUpdate(self, u) {
285
285
  // strip the prefix from the user prompt before sending to the loop.
286
286
  const slashed = tryResolveSkillCommand(text, { projectPath: target?.path });
287
287
 
288
+ // A2A callback sink: when a background call_runtime finishes out of band,
289
+ // it invokes this to feed the sub-agent/runtime result back into a fresh
290
+ // super-agent turn — so Roby relays it in its own voice instead of dumping
291
+ // raw output. Self-referential so a relay turn that delegates again keeps
292
+ // the loop. Only wired when we have a chat to stream back to.
293
+ let backgroundResultSink = null;
294
+ if (chat_id) {
295
+ backgroundResultSink = async (reportText) =>
296
+ runFollowupTurn(self, {
297
+ chat_id,
298
+ reportText,
299
+ target,
300
+ author,
301
+ authorId: msg.from?.id,
302
+ relationshipBlock,
303
+ allowedTools,
304
+ agentDisplay,
305
+ update_id: u.update_id,
306
+ backgroundResultSink,
307
+ });
308
+ }
309
+
288
310
  try {
289
311
  const sa = await runTelegramSuperAgent(self, {
290
312
  chat_id,
@@ -298,6 +320,7 @@ export async function handleUpdate(self, u) {
298
320
  contextNote: slashed.handled ? slashed.contextNote : "",
299
321
  signal: abortCtrl.signal,
300
322
  onEvent,
323
+ backgroundResultSink,
301
324
  });
302
325
  replyText = sa.text;
303
326
  replyAuthor = sa.name || agentDisplay;
@@ -8,7 +8,7 @@
8
8
  import { runSuperAgent } from "#core/agent/super-agent.js";
9
9
  import { TELEGRAM_TOOL_ITERS } from "#core/agent/constants.js";
10
10
  import { stripThinking } from "#core/util/thinking.js";
11
- import { appendGlobalMessage } from "#core/stores/messages.js";
11
+ import { appendGlobalMessage, getRecentTelegramTurnsFromFs } from "#core/stores/messages.js";
12
12
  import { CHANNELS } from "#core/constants/channels.js";
13
13
  import { SUPERAGENT_ACTOR_ID } from "#core/identity/index.js";
14
14
  import { createTelegramConfirmAdapter } from "#core/confirmation/adapters/telegram.js";
@@ -100,7 +100,7 @@ export function buildStreamHandler(self, { chat_id, update_id, agentDisplay }) {
100
100
  */
101
101
  export function runTelegramSuperAgent(self, {
102
102
  chat_id, prompt, previousMessages, target, author, authorId, relationshipBlock,
103
- allowedTools, contextNote, signal, onEvent,
103
+ allowedTools, contextNote, signal, onEvent, backgroundResultSink = null,
104
104
  }) {
105
105
  const confirmAdapter = createTelegramConfirmAdapter({
106
106
  token: resolveBotToken(self.channel),
@@ -130,6 +130,7 @@ export function runTelegramSuperAgent(self, {
130
130
  signal,
131
131
  onEvent,
132
132
  requestConfirmation: confirmAdapter.requestConfirmation,
133
+ backgroundResultSink,
133
134
  // Autonomy budget: Telegram is the "do the whole task for me" surface, so it
134
135
  // gets a real multi-step budget instead of the conversational default (which
135
136
  // cut tasks off after ~9 actions to ask "continue?"). Tunable via
@@ -138,6 +139,82 @@ export function runTelegramSuperAgent(self, {
138
139
  });
139
140
  }
140
141
 
142
+ /**
143
+ * Run a follow-up super-agent turn triggered internally (not by an inbound
144
+ * message) — the A2A callback path. A background tool (call_runtime) finished
145
+ * out of band; `reportText` is the sub-agent/runtime result phrased as an
146
+ * internal report. We log it as a synthetic inbound so it lands in history,
147
+ * then run a normal streamed turn so Roby relays it to the user in its own
148
+ * voice (and can chain the next step). The same `backgroundResultSink` is
149
+ * forwarded so a relay turn that delegates again keeps the A2A loop intact.
150
+ * Best-effort: never throws (nothing awaits it).
151
+ */
152
+ export async function runFollowupTurn(self, {
153
+ chat_id, reportText, target, author, authorId, relationshipBlock,
154
+ allowedTools, agentDisplay, update_id, backgroundResultSink = null,
155
+ }) {
156
+ if (!chat_id || !reportText) return;
157
+ try {
158
+ // Synthetic inbound so the report is part of the rolling history. Tagged
159
+ // a2a_callback + a distinct author so it reads as an internal hand-off, not
160
+ // a user turn.
161
+ appendGlobalMessage({
162
+ channel: CHANNELS.TELEGRAM,
163
+ direction: "in",
164
+ type: "user",
165
+ actor_id: "a2a",
166
+ external_id: `a2a-${update_id}-${chat_id}`,
167
+ author: "a2a",
168
+ body: reportText,
169
+ meta: { chat_id, tg_channel: self.channel.name, a2a_callback: true },
170
+ });
171
+
172
+ const previousMessages = getRecentTelegramTurnsFromFs({ chat_id, keepRecent: 40, max_age_hours: 24 });
173
+ const { onEvent, state } = buildStreamHandler(self, { chat_id, update_id, agentDisplay });
174
+ const stopTyping = self._startTyping(chat_id);
175
+ let replyText;
176
+ let replyAuthor;
177
+ let saUsage = null;
178
+ try {
179
+ const sa = await runTelegramSuperAgent(self, {
180
+ chat_id,
181
+ prompt: reportText,
182
+ previousMessages,
183
+ target,
184
+ author,
185
+ authorId,
186
+ relationshipBlock,
187
+ allowedTools,
188
+ onEvent,
189
+ backgroundResultSink,
190
+ });
191
+ replyText = sa.text;
192
+ replyAuthor = sa.name || agentDisplay;
193
+ saUsage = sa.usage;
194
+ } catch (e) {
195
+ self.log(`telegram[${self.channel.name}] a2a followup failed: ${e.message}`);
196
+ replyText = telegramErrorText(self, e);
197
+ replyAuthor = agentDisplay;
198
+ }
199
+ stopTyping();
200
+ await sendFinalReply(self, {
201
+ chat_id,
202
+ update_id,
203
+ replyText,
204
+ replyAuthor,
205
+ replyActorId: SUPERAGENT_ACTOR_ID,
206
+ replyKind: "superagent",
207
+ saUsage,
208
+ streamedCount: state.streamedCount,
209
+ lastStreamedText: state.lastStreamedText,
210
+ agentDisplay,
211
+ extraMeta: { a2a_relay: true },
212
+ });
213
+ } catch (e) {
214
+ self.log(`telegram[${self.channel.name}] a2a followup crashed: ${e.message}`);
215
+ }
216
+ }
217
+
141
218
  /** Localized "couldn't reply" text for a failed super-agent turn (model itself
142
219
  * failed, so it can't author this — templated, but follows the user's language). */
143
220
  export function telegramErrorText(self, e) {
@@ -143,45 +143,25 @@ export const asanaPlugin = {
143
143
  ],
144
144
 
145
145
  // Declarative UI descriptor consumed by the generic PluginConnect component
146
- // (see web/components/integrations/PluginConnect.tsx). configFields render as
147
- // inputs; `select` is a post-validate picker sourced from an action; and
148
- // connectedFields are the status keys shown once connected.
146
+ // (see web/components/integrations/PluginConnect.tsx). STRUCTURE ONLY — all
147
+ // display text lives in the web i18n dictionaries under integrations.<slug>.*
148
+ // (keyed by field key). configFields render as inputs; `select` is a
149
+ // post-validate picker sourced from an action; connectedFields are the status
150
+ // keys shown once connected. `help_url`/`help_url_label` are non-translatable
151
+ // data kept here.
149
152
  ui: {
150
153
  accent: "rose",
151
154
  configFields: [
152
155
  {
153
156
  key: "personal_access_token",
154
- label: "Personal Access Token",
155
157
  type: "password",
156
158
  placeholder: "1/1234567890abcdef:...",
157
- help: {
158
- label: "¿Cómo obtener el token?",
159
- url: "https://app.asana.com/0/my-apps",
160
- urlLabel: "app.asana.com/0/my-apps",
161
- steps: [
162
- "Abrí app.asana.com/0/my-apps en el navegador.",
163
- 'Bajá hasta la sección "Personal access tokens" (no tus apps OAuth).',
164
- 'Hacé clic en "+ New access token".',
165
- "Dale un nombre y confirmá.",
166
- 'Copiá el token completo — empieza con "1/..." y tiene un ":" en el medio.',
167
- "Pegalo en el campo de abajo.",
168
- ],
169
- },
159
+ help_url: "https://app.asana.com/0/my-apps",
160
+ help_url_label: "app.asana.com/0/my-apps",
170
161
  },
171
162
  ],
172
- select: {
173
- key: "workspace_gid",
174
- label: "Seleccioná el workspace a usar",
175
- action: "workspaces",
176
- listKey: "workspaces",
177
- valueKey: "gid",
178
- labelKey: "name",
179
- },
180
- connectedFields: [
181
- { key: "user_name", label: "Conectado como" },
182
- { key: "user_email", label: "Email" },
183
- { key: "workspace_name", label: "Workspace" },
184
- ],
163
+ select: { key: "workspace_gid", action: "workspaces", listKey: "workspaces", valueKey: "gid", labelKey: "name" },
164
+ connectedFields: ["user_name", "user_email", "workspace_name"],
185
165
  },
186
166
 
187
167
  // Save the PAT and/or the target workspace. Returns a patch to persist.
@@ -90,31 +90,19 @@ export const githubPlugin = {
90
90
  { slug: "github_list_repos", desc: "Listar repositorios accesibles" },
91
91
  { slug: "github_create_issue", desc: "Crear un issue en un repo" },
92
92
  ],
93
+ // Structure only — display text lives in web i18n (integrations.github.*).
93
94
  ui: {
94
95
  accent: "slate",
95
96
  configFields: [
96
97
  {
97
98
  key: "token",
98
- label: "Personal Access Token",
99
99
  type: "password",
100
100
  placeholder: "ghp_... o github_pat_...",
101
- help: {
102
- label: "¿Cómo obtener el token?",
103
- url: "https://github.com/settings/tokens",
104
- urlLabel: "github.com/settings/tokens",
105
- steps: [
106
- "Abrí github.com/settings/tokens.",
107
- 'Generá un token (classic o fine-grained) con scope "repo".',
108
- "Copiá el token — empieza con ghp_ o github_pat_.",
109
- "Pegalo en el campo de abajo.",
110
- ],
111
- },
101
+ help_url: "https://github.com/settings/tokens",
102
+ help_url_label: "github.com/settings/tokens",
112
103
  },
113
104
  ],
114
- connectedFields: [
115
- { key: "user_login", label: "Conectado como" },
116
- { key: "user_name", label: "Nombre" },
117
- ],
105
+ connectedFields: ["user_login", "user_name"],
118
106
  },
119
107
 
120
108
  configure(record, body = {}) {