@staix/agent-hub 0.12.38 → 0.12.39

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-hub",
3
- "version": "0.12.38",
3
+ "version": "0.12.39",
4
4
  "description": "Channel between Claude Code and the agent-hub daemon: peer messages from Codex, Kimi and the local worker arrive as channel events; hub_send replies.",
5
5
  "author": {
6
6
  "name": "Young Joon Lee",
@@ -456,12 +456,12 @@ class BasePeer {
456
456
  this._state = next;
457
457
  this.onState?.(next);
458
458
  }
459
- touch() {
459
+ touch(ms = this.watchdogMs) {
460
460
  this.clearWatchdog();
461
461
  this.timer = setTimeout(() => {
462
462
  if (this._state === "busy")
463
463
  this.onWatchdog();
464
- }, this.watchdogMs);
464
+ }, ms);
465
465
  this.timer.unref?.();
466
466
  }
467
467
  onWatchdog() {
@@ -2551,13 +2551,14 @@ function jsonObject(text) {
2551
2551
  return;
2552
2552
  }
2553
2553
  }
2554
- var HANDSHAKE_MS = 30000, TOOL_INPUT_CAP = 64, RpcTimeoutError, AcpPeer;
2554
+ var HANDSHAKE_MS = 30000, TOOL_INPUT_CAP = 64, TOOL_CEILING_MS, RpcTimeoutError, AcpPeer;
2555
2555
  var init_acp = __esm(() => {
2556
2556
  init_envelope();
2557
2557
  init_kimi_native();
2558
2558
  init_qwen_native();
2559
2559
  init_qwen_native();
2560
2560
  init_child_process();
2561
+ TOOL_CEILING_MS = 30 * 60000;
2561
2562
  RpcTimeoutError = class RpcTimeoutError extends Error {
2562
2563
  };
2563
2564
  AcpPeer = class AcpPeer extends BasePeer {
@@ -2575,6 +2576,8 @@ var init_acp = __esm(() => {
2575
2576
  toolInputs = new Map;
2576
2577
  toolText = new Map;
2577
2578
  toolTitles = new Map;
2579
+ openTools = new Map;
2580
+ lastSignAt = 0;
2578
2581
  primed = false;
2579
2582
  turn = 0;
2580
2583
  stopping = false;
@@ -2793,6 +2796,7 @@ var init_acp = __esm(() => {
2793
2796
  this.deliveryAccepted = false;
2794
2797
  this.promptActivity = false;
2795
2798
  this.chunks = [];
2799
+ this.openTools.clear();
2796
2800
  this.activityOpen = true;
2797
2801
  this.activityPrivate = envs.some((env) => env.private === true);
2798
2802
  try {
@@ -2894,6 +2898,21 @@ ${renderDigest(envs, false)}` }] });
2894
2898
  this.clearWatchdog();
2895
2899
  return;
2896
2900
  }
2901
+ if (this.openTools.size) {
2902
+ const ceiling = Math.max(this.watchdogMs, this.opts.toolCeilingMs ?? TOOL_CEILING_MS);
2903
+ const remaining = ceiling - (Date.now() - this.lastSignAt);
2904
+ if (remaining > 0) {
2905
+ this.touch(Math.min(this.watchdogMs, remaining));
2906
+ return;
2907
+ }
2908
+ try {
2909
+ this.activityPrivate ||= this.opts.isPrivateTurn?.() === true;
2910
+ } catch {
2911
+ this.activityPrivate = true;
2912
+ }
2913
+ const tools = this.activityPrivate ? "a tool call" : [...new Set(this.openTools.values())].map((title) => JSON.stringify(activityText(title).replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, " ").slice(0, 80))).join(", ");
2914
+ this.opts.log?.(`[${this.id}] open tool call (${tools}) silent past the ${Math.round(ceiling / 60000)} min tool ceiling; cancelling the turn`);
2915
+ }
2897
2916
  this.endActivity("watchdog cancelled", true);
2898
2917
  const cancelledTurn = this.turn;
2899
2918
  this.cancelPermissions();
@@ -2967,8 +2986,10 @@ ${renderDigest(envs, false)}` }] });
2967
2986
  } catch {
2968
2987
  return;
2969
2988
  }
2970
- if (this.state === "busy" && !this.cancelWait)
2989
+ if (this.state === "busy" && !this.cancelWait) {
2990
+ this.lastSignAt = Date.now();
2971
2991
  this.touch();
2992
+ }
2972
2993
  if (msg.method === "session/update") {
2973
2994
  if (msg.params?.sessionId && msg.params.sessionId !== this.sessionId)
2974
2995
  return;
@@ -2999,7 +3020,11 @@ ${renderDigest(envs, false)}` }] });
2999
3020
  if (text)
3000
3021
  this.toolText.set(u.toolCallId, text);
3001
3022
  if (u.status === "completed" || u.status === "failed")
3002
- this.toolInputs.delete(u.toolCallId), this.toolText.delete(u.toolCallId), this.toolTitles.delete(u.toolCallId);
3023
+ this.toolInputs.delete(u.toolCallId), this.toolText.delete(u.toolCallId), this.toolTitles.delete(u.toolCallId), this.openTools.delete(u.toolCallId);
3024
+ else if (u.sessionUpdate === "tool_call" || this.openTools.has(u.toolCallId))
3025
+ this.openTools.set(u.toolCallId, this.toolTitles.get(u.toolCallId) ?? "tool");
3026
+ while (this.openTools.size > TOOL_INPUT_CAP)
3027
+ this.openTools.delete(this.openTools.keys().next().value);
3003
3028
  for (const map of [this.toolInputs, this.toolText, this.toolTitles])
3004
3029
  while (map.size > TOOL_INPUT_CAP)
3005
3030
  map.delete(map.keys().next().value);
@@ -3104,7 +3129,7 @@ ${renderDigest(envs, false)}` }] });
3104
3129
 
3105
3130
  // src/adapters/jcode.ts
3106
3131
  import { spawn as spawn6, spawnSync as spawnSync2 } from "child_process";
3107
- import { existsSync as existsSync4, lstatSync as lstatSync4, mkdtempSync as mkdtempSync3, rmSync as rmSync3 } from "fs";
3132
+ import { closeSync, constants, existsSync as existsSync4, fstatSync, lstatSync as lstatSync4, mkdtempSync as mkdtempSync3, openSync, readSync, rmSync as rmSync3 } from "fs";
3108
3133
  import { tmpdir as tmpdir3 } from "os";
3109
3134
  import { isAbsolute as isAbsolute3, join as join5 } from "path";
3110
3135
  function jcodeCommand(command, kind, socket, cwd, model, profile) {
@@ -3124,6 +3149,72 @@ function jcodeCommand(command, kind, socket, cwd, model, profile) {
3124
3149
  ...kind === "serve" ? ["--temporary-server", "--owner-pid", String(process.pid), "--server-name", "agent-hub-jcode"] : ["--mcp-tools", "eager"]
3125
3150
  ];
3126
3151
  }
3152
+ function jcodeTuiCommand(command, socket, sessionId, model, profile) {
3153
+ if (!command.length || command.some((word) => typeof word !== "string" || !word))
3154
+ throw new Error("jcode_cmd must name an executable argv");
3155
+ return [
3156
+ ...command,
3157
+ "--socket",
3158
+ socket,
3159
+ "--resume",
3160
+ sessionId,
3161
+ "--no-update",
3162
+ "--no-selfdev",
3163
+ ...model ? ["--model", model] : [],
3164
+ ...profile ? ["--provider-profile", profile] : []
3165
+ ];
3166
+ }
3167
+ function savedSession(home, id) {
3168
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(id))
3169
+ throw new Error("invalid native session identity");
3170
+ const directory = join5(home, "sessions"), info = lstatSync4(directory, { throwIfNoEntry: false });
3171
+ if (!info)
3172
+ return false;
3173
+ if (!info.isDirectory() || info.isSymbolicLink())
3174
+ throw new Error("unsafe native session directory");
3175
+ const file = join5(directory, `${id}.json`), entry = lstatSync4(file, { throwIfNoEntry: false });
3176
+ if (!entry)
3177
+ return false;
3178
+ if (!entry.isFile() || entry.isSymbolicLink())
3179
+ throw new Error("unsafe native session proof");
3180
+ let fd;
3181
+ try {
3182
+ fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
3183
+ } catch (error) {
3184
+ if (error.code === "ENOENT")
3185
+ return false;
3186
+ throw error;
3187
+ }
3188
+ try {
3189
+ const before = fstatSync(fd);
3190
+ if (!before.isFile() || before.nlink !== 1 || before.size > 32 * 1024 * 1024)
3191
+ throw new Error("unsafe native session proof");
3192
+ if (!before.size)
3193
+ return false;
3194
+ const bytes = Buffer.alloc(before.size);
3195
+ let offset = 0;
3196
+ while (offset < bytes.length) {
3197
+ const n = readSync(fd, bytes, offset, bytes.length - offset, offset);
3198
+ if (!n)
3199
+ return false;
3200
+ offset += n;
3201
+ }
3202
+ const after = fstatSync(fd);
3203
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs)
3204
+ return false;
3205
+ let data;
3206
+ try {
3207
+ data = JSON.parse(bytes.toString("utf8"));
3208
+ } catch {
3209
+ return false;
3210
+ }
3211
+ if (!data || typeof data !== "object" || data.id !== id || !Array.isArray(data.messages))
3212
+ throw new Error("native saved session identity is unproven");
3213
+ return true;
3214
+ } finally {
3215
+ closeSync(fd);
3216
+ }
3217
+ }
3127
3218
  function jcodeVersionSupported(version) {
3128
3219
  const v = typeof version === "string" ? release(version) : undefined;
3129
3220
  return !!v && compare(v, release(JCODE_MIN_VERSION)) >= 0 && compare(v, release(JCODE_BREAKING_VERSION)) < 0;
@@ -3135,6 +3226,8 @@ var init_jcode = __esm(() => {
3135
3226
  JCODE_OWNED_ENV = { JCODE_DISABLE_CLAUDE_MCP: "1", JCODE_NO_TELEMETRY: "1" };
3136
3227
  JcodePeer = class JcodePeer extends AcpPeer {
3137
3228
  jcode;
3229
+ launchId = crypto.randomUUID();
3230
+ socketIdentity;
3138
3231
  root;
3139
3232
  socket;
3140
3233
  server;
@@ -3187,6 +3280,8 @@ var init_jcode = __esm(() => {
3187
3280
  if (!this.ending && this.server === server)
3188
3281
  this.stop("Jcode owned daemon disconnected").catch(() => {});
3189
3282
  });
3283
+ const identity = lstatSync4(this.socket);
3284
+ this.socketIdentity = { dev: identity.dev, ino: identity.ino };
3190
3285
  await super.start();
3191
3286
  } catch (error) {
3192
3287
  await this.stop().catch(() => {});
@@ -3252,6 +3347,21 @@ var init_jcode = __esm(() => {
3252
3347
  if (failure)
3253
3348
  throw failure;
3254
3349
  }
3350
+ tuiLaunch(expected, options = {}) {
3351
+ const sessionId = super.recoveryMetadata().sessionId;
3352
+ const socket = lstatSync4(this.socket, { throwIfNoEntry: false });
3353
+ if (this.ending || !this.server || this.server.exitCode !== null || this.server.signalCode !== null || this.state === "offline" || !socket?.isSocket() || socket.dev !== this.socketIdentity?.dev || socket.ino !== this.socketIdentity.ino || typeof sessionId !== "string")
3354
+ throw new Error("owned Jcode launch is unavailable");
3355
+ const binding = { launchId: this.launchId, sessionId };
3356
+ if (expected && (expected.launchId !== binding.launchId || expected.sessionId !== binding.sessionId))
3357
+ throw new Error("Jcode launch changed");
3358
+ if (options.sessionId !== undefined && options.sessionId !== sessionId || options.model !== undefined && options.model !== this.jcode.launchModel || options.providerProfile !== undefined && options.providerProfile !== this.jcode.providerProfile)
3359
+ throw new Error("Jcode launch options differ from its running session");
3360
+ if (!savedSession(this.jcode.home, sessionId))
3361
+ return { state: "waiting", binding };
3362
+ const [cmd, ...args] = jcodeTuiCommand(this.jcode.command, this.socket, sessionId, this.jcode.launchModel, this.jcode.providerProfile);
3363
+ return { state: "ready", binding, launch: { cmd, args, env: { JCODE_HOME: this.jcode.home, JCODE_SOCKET: this.socket, ...JCODE_OWNED_ENV } } };
3364
+ }
3255
3365
  recoveryMetadata() {
3256
3366
  return { ...super.recoveryMetadata(), launch: { kind: "jcode", peer: "jcode", mode: "headless", ...this.jcode.launchModel ? { model: this.jcode.launchModel } : {}, ...this.jcode.providerProfile ? { providerProfile: this.jcode.providerProfile } : {} } };
3257
3367
  }
@@ -3299,7 +3409,7 @@ var init_recovery_store = __esm(() => {
3299
3409
  });
3300
3410
 
3301
3411
  // src/pi/session.ts
3302
- import { readFileSync as readFileSync4, openSync, closeSync, fsyncSync, mkdirSync as mkdirSync3 } from "fs";
3412
+ import { readFileSync as readFileSync4, openSync as openSync2, closeSync as closeSync2, fsyncSync, mkdirSync as mkdirSync3 } from "fs";
3303
3413
  import { join as join7, relative as relative2, resolve as resolve5, dirname as dirname5 } from "path";
3304
3414
  function sessionReadFailure(error) {
3305
3415
  if (error.code === "ENOENT")
@@ -3368,7 +3478,7 @@ var init_permission_mode = __esm(() => {
3368
3478
  });
3369
3479
 
3370
3480
  // src/hub/path-policy.ts
3371
- import { accessSync, constants, lstatSync as lstatSync5, opendirSync, readlinkSync, statSync as statSync2 } from "fs";
3481
+ import { accessSync, constants as constants2, lstatSync as lstatSync5, opendirSync, readlinkSync, statSync as statSync2 } from "fs";
3372
3482
  import { dirname as dirname6, isAbsolute as isAbsolute4, join as join8, relative as relative3, resolve as resolve6, sep as sep2 } from "path";
3373
3483
  function existing(path) {
3374
3484
  try {
@@ -3423,7 +3533,7 @@ function directAliasTarget(path) {
3423
3533
  return;
3424
3534
  }
3425
3535
  function aliasScope(dir, lexical, actual, includeParents, budget) {
3426
- accessSync(dir, constants.R_OK);
3536
+ accessSync(dir, constants2.R_OK);
3427
3537
  const handle = opendirSync(dir);
3428
3538
  let matched = false;
3429
3539
  try {
@@ -7243,7 +7353,7 @@ var init_shipped_roles = __esm(() => {
7243
7353
 
7244
7354
  // src/hub/roles.ts
7245
7355
  import { createHash as createHash5 } from "crypto";
7246
- import { constants as constants2, closeSync as closeSync2, fstatSync, lstatSync as lstatSync7, openSync as openSync2, readSync, readdirSync as readdirSync2 } from "fs";
7356
+ import { constants as constants3, closeSync as closeSync3, fstatSync as fstatSync2, lstatSync as lstatSync7, openSync as openSync3, readSync as readSync2, readdirSync as readdirSync2 } from "fs";
7247
7357
  import { join as join12 } from "path";
7248
7358
  function parseRole(text, stem, layer) {
7249
7359
  if (Buffer.byteLength(text) > 8192 || text.includes("\r") || !ID.test(stem))
@@ -7291,24 +7401,24 @@ function parseRole(text, stem, layer) {
7291
7401
  return { id: stem, name: header.name, group: header.group, summary: header.summary, source: header.source, base: list("base", BASES2), classes: list("classes", CLASSES2), body, review, layer, hash: createHash5("sha256").update(text).digest("hex") };
7292
7402
  }
7293
7403
  function projectRole(file) {
7294
- const fd = openSync2(file, constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK);
7404
+ const fd = openSync3(file, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
7295
7405
  try {
7296
- const before = fstatSync(fd);
7406
+ const before = fstatSync2(fd);
7297
7407
  if (!before.isFile() || before.nlink !== 1 || before.size > 8192)
7298
7408
  throw new Error("role must be a bounded regular file");
7299
7409
  const bytes = Buffer.alloc(8193);
7300
7410
  let length = 0, n = 0;
7301
- while ((n = readSync(fd, bytes, length, bytes.length - length, null)) > 0) {
7411
+ while ((n = readSync2(fd, bytes, length, bytes.length - length, null)) > 0) {
7302
7412
  length += n;
7303
7413
  if (length > 8192)
7304
7414
  throw new Error("role exceeds file cap");
7305
7415
  }
7306
- const after = fstatSync(fd);
7416
+ const after = fstatSync2(fd);
7307
7417
  if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || length !== after.size)
7308
7418
  throw new Error("role changed during read");
7309
7419
  return bytes.subarray(0, length).toString("utf8");
7310
7420
  } finally {
7311
- closeSync2(fd);
7421
+ closeSync3(fd);
7312
7422
  }
7313
7423
  }
7314
7424
  function loadRoleCatalog(cwd) {
@@ -8254,7 +8364,7 @@ var init_reviewer_variants = __esm(() => {
8254
8364
  });
8255
8365
 
8256
8366
  // src/adapters/agy/hooks.ts
8257
- import { closeSync as closeSync3, constants as constants3, fstatSync as fstatSync2, lstatSync as lstatSync8, openSync as openSync3, readSync as readSync2 } from "fs";
8367
+ import { closeSync as closeSync4, constants as constants4, fstatSync as fstatSync3, lstatSync as lstatSync8, openSync as openSync4, readSync as readSync3 } from "fs";
8258
8368
  import { homedir as homedir4 } from "os";
8259
8369
  import { isAbsolute as isAbsolute5, join as join13, relative as relative5 } from "path";
8260
8370
  import { createHash as createHash6 } from "crypto";
@@ -8303,8 +8413,8 @@ function readAgyTranscript(session, transcript) {
8303
8413
  if (lstatSync8(cursor).isSymbolicLink())
8304
8414
  return empty;
8305
8415
  }
8306
- fd = openSync3(transcript, constants3.O_RDONLY | constants3.O_NOFOLLOW);
8307
- const stat = fstatSync2(fd);
8416
+ fd = openSync4(transcript, constants4.O_RDONLY | constants4.O_NOFOLLOW);
8417
+ const stat = fstatSync3(fd);
8308
8418
  if (!stat.isFile() || stat.nlink !== 1)
8309
8419
  return empty;
8310
8420
  const cap = 4 * 1024 * 1024;
@@ -8313,7 +8423,7 @@ function readAgyTranscript(session, transcript) {
8313
8423
  const buffer = Buffer.alloc(stat.size - from);
8314
8424
  let bytes = 0;
8315
8425
  while (bytes < buffer.length) {
8316
- const count = readSync2(fd, buffer, bytes, buffer.length - bytes, from + bytes);
8426
+ const count = readSync3(fd, buffer, bytes, buffer.length - bytes, from + bytes);
8317
8427
  if (!count)
8318
8428
  break;
8319
8429
  bytes += count;
@@ -8346,7 +8456,7 @@ function readAgyTranscript(session, transcript) {
8346
8456
  return empty;
8347
8457
  } finally {
8348
8458
  if (fd !== undefined)
8349
- closeSync3(fd);
8459
+ closeSync4(fd);
8350
8460
  }
8351
8461
  }
8352
8462
  var EVENTS, UUID, record3 = (value) => !!value && typeof value === "object" && !Array.isArray(value), AGY_FACTS_HOOK;
@@ -8461,7 +8571,7 @@ function bundleInStatus(status) {
8461
8571
 
8462
8572
  // src/cli/agy-mcp.ts
8463
8573
  import { randomUUID as randomUUID5 } from "crypto";
8464
- import { closeSync as closeSync4, fstatSync as fstatSync3, lstatSync as lstatSync9, mkdirSync as mkdirSync6, openSync as openSync4, readFileSync as readFileSync6, renameSync, rmdirSync, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
8574
+ import { closeSync as closeSync5, fstatSync as fstatSync4, lstatSync as lstatSync9, mkdirSync as mkdirSync6, openSync as openSync5, readFileSync as readFileSync6, renameSync, rmdirSync, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
8465
8575
  import { isAbsolute as isAbsolute6, join as join14, resolve as resolve10 } from "path";
8466
8576
  import { isDeepStrictEqual } from "util";
8467
8577
  function object4(value) {
@@ -8712,11 +8822,11 @@ function atomicWrite(path, bytes, expected, plan) {
8712
8822
  let tempIdentity;
8713
8823
  try {
8714
8824
  verifyPluginDirectory(plan, path);
8715
- fd = openSync4(temporary, "wx", 384);
8716
- const tempStat = fstatSync3(fd);
8825
+ fd = openSync5(temporary, "wx", 384);
8826
+ const tempStat = fstatSync4(fd);
8717
8827
  tempIdentity = { dev: tempStat.dev, ino: tempStat.ino };
8718
8828
  writeFileSync4(fd, bytes);
8719
- closeSync4(fd);
8829
+ closeSync5(fd);
8720
8830
  fd = undefined;
8721
8831
  verifyPluginDirectory(plan, path);
8722
8832
  if (!identityMatches(path, expected))
@@ -8728,7 +8838,7 @@ function atomicWrite(path, bytes, expected, plan) {
8728
8838
  return { dev: stat.dev, ino: stat.ino };
8729
8839
  } finally {
8730
8840
  if (fd !== undefined)
8731
- closeSync4(fd);
8841
+ closeSync5(fd);
8732
8842
  try {
8733
8843
  const current = lstatSync9(temporary, { throwIfNoEntry: false });
8734
8844
  if (current && tempIdentity && current.dev === tempIdentity.dev && current.ino === tempIdentity.ino)
@@ -9108,22 +9218,89 @@ var init_native_cli_entry_codex = () => {};
9108
9218
 
9109
9219
  // src/adapters/definitions/native-cli-entry-jcode.ts
9110
9220
  import { resolve as resolve12 } from "path";
9221
+ async function waitForJcodeTui(hub, fail, signal, options, first) {
9222
+ let binding = first?.binding, announced = false, current = first;
9223
+ while (!signal.aborted) {
9224
+ if (!current) {
9225
+ const reply = await hub.request({ t: "native_tui_launch", peer: "jcode", binding, args: options });
9226
+ if (signal.aborted)
9227
+ return;
9228
+ if (!reply.ok || !reply.binding || !["waiting", "ready"].includes(reply.state))
9229
+ fail("Jcode launch changed or its native saved session is unavailable; retry from a terminal");
9230
+ current = reply;
9231
+ }
9232
+ if (binding && (binding.launchId !== current.binding.launchId || binding.sessionId !== current.binding.sessionId))
9233
+ fail("Jcode launch changed while waiting");
9234
+ binding = current.binding;
9235
+ if (current.state === "ready")
9236
+ return current;
9237
+ if (!announced) {
9238
+ console.log(WAITING);
9239
+ announced = true;
9240
+ }
9241
+ await new Promise((resolve) => {
9242
+ const finish = () => {
9243
+ clearTimeout(timer);
9244
+ signal.removeEventListener("abort", finish);
9245
+ resolve();
9246
+ };
9247
+ const timer = setTimeout(finish, 200);
9248
+ signal.addEventListener("abort", finish, { once: true });
9249
+ if (signal.aborted)
9250
+ finish();
9251
+ });
9252
+ current = undefined;
9253
+ }
9254
+ }
9111
9255
  async function runJcodeEntry(ctx) {
9112
9256
  const { args, cwd, stateDir, cliDir, unattendedEnv, connect, fail, takeFlags } = ctx;
9113
- const { launcherPreview, qwenSessionId, configureJcodeMcp } = ctx.helpers;
9114
- if (args.includes("--print-command") || args.includes("--dry-run")) {
9257
+ const { launcherPreview, qwenSessionId, configureJcodeMcp, readControl, startModeOf } = ctx.helpers;
9258
+ const print = args.includes("--print-command");
9259
+ if (args.includes("--dry-run") || print) {
9260
+ if (!print || args.includes("--dry-run") || !ctx.person) {
9261
+ try {
9262
+ return console.log(JSON.stringify(launcherPreview("jcode", args, cwd, stateDir, unattendedEnv), null, 2));
9263
+ } catch {
9264
+ fail("cannot preview Jcode launch: invalid arguments or configuration (details withheld)");
9265
+ }
9266
+ }
9267
+ }
9268
+ const { one, rest } = takeFlags(args.filter((arg) => arg !== "--print-command"), ["--mode", "--model", "--provider-profile", "--session-id"], []);
9269
+ if (rest.length || one["--mode"] !== undefined && !["headless", "tui"].includes(one["--mode"]))
9270
+ fail("usage: ahub jcode [--mode tui|headless] [--model <alias>] [--provider-profile <name>] [--session-id <id>]");
9271
+ if (one["--session-id"] !== undefined && !qwenSessionId(one["--session-id"]))
9272
+ fail("invalid Jcode session identity");
9273
+ const control = readControl(stateDir);
9274
+ const mode = one["--mode"] ?? (ctx.instance ? "headless" : control?.startModes?.jcode ?? startModeOf(ctx.projectConfig().peers, "jcode"));
9275
+ if (print && (mode === "headless" || !control?.features?.includes("native_tui_launch"))) {
9115
9276
  try {
9116
9277
  return console.log(JSON.stringify(launcherPreview("jcode", args, cwd, stateDir, unattendedEnv), null, 2));
9117
9278
  } catch {
9118
- fail("cannot preview Jcode launch: invalid arguments or configuration (details withheld)");
9279
+ return fail("cannot preview Jcode launch: invalid arguments or configuration (details withheld)");
9119
9280
  }
9120
9281
  }
9121
- const { one, rest } = takeFlags(args, ["--mode", "--model", "--provider-profile", "--session-id"], []);
9122
- if (rest.length || one["--mode"] !== undefined && one["--mode"] !== "headless")
9123
- fail("usage: ahub jcode [--mode headless] [--model <alias>] [--provider-profile <name>] [--session-id <id>]");
9124
- if (one["--session-id"] !== undefined && !qwenSessionId(one["--session-id"]))
9125
- fail("invalid Jcode session identity");
9126
- const hub = await connect();
9282
+ if (mode === "tui" && !ctx.person)
9283
+ fail("Jcode's private TUI is person-only; run ahub jcode in your terminal");
9284
+ const hub = await connect(), abort = new AbortController;
9285
+ let interrupted = false;
9286
+ const interruptedWait = () => {
9287
+ if (abort.signal.aborted && !interrupted)
9288
+ fail("hub connection closed while waiting for Jcode; retry from a terminal");
9289
+ return abort.signal.aborted;
9290
+ };
9291
+ const interrupt = () => {
9292
+ interrupted = true;
9293
+ abort.abort();
9294
+ hub.close();
9295
+ process.exitCode = 130;
9296
+ };
9297
+ process.once("SIGINT", interrupt);
9298
+ const oldClose = hub.onClose;
9299
+ hub.onClose = (code, reason) => {
9300
+ if (!abort.signal.aborted)
9301
+ abort.abort();
9302
+ oldClose?.(code, reason);
9303
+ };
9127
9304
  try {
9128
9305
  const snapshot = (await hub.request({ t: "status" })).status;
9129
9306
  const rostered = rosterInStatus(snapshot, "jcode");
@@ -9134,17 +9311,60 @@ async function runJcodeEntry(ctx) {
9134
9311
  const warning = retainedLaunchWarning(snapshot, "jcode", ctx.definition);
9135
9312
  if (warning)
9136
9313
  console.error(warning);
9314
+ const options = { model: one["--model"], providerProfile: one["--provider-profile"], sessionId: one["--session-id"] };
9315
+ if (print) {
9316
+ if (!hub.features.includes("native_tui_launch") || mode !== "tui")
9317
+ return console.log(JSON.stringify(launcherPreview("jcode", args, cwd, stateDir, unattendedEnv), null, 2));
9318
+ const reply = await hub.request({ t: "native_tui_launch", peer: "jcode", args: options });
9319
+ if (!reply.ok || reply.state !== "ready")
9320
+ fail("Jcode's selected native session is not saved and ready; run ahub jcode to wait for its first hub turn");
9321
+ return console.log(JSON.stringify(reply.launch, null, 2));
9322
+ }
9323
+ if (mode === "tui") {
9324
+ if (!hub.features.includes("native_tui_launch"))
9325
+ fail("the running hub does not support Jcode TUI launch; upgrade it before retrying");
9326
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
9327
+ if (one["--mode"] || Object.values(options).some((value) => value !== undefined))
9328
+ fail("run ahub jcode with these options in a terminal");
9329
+ const reply = await hub.request({ t: "peer_start", peer: "jcode" });
9330
+ if (!reply.ok)
9331
+ fail(reply.error);
9332
+ console.log(reply.command ?? "Jcode terminal requested");
9333
+ return;
9334
+ }
9335
+ }
9137
9336
  const written = configureJcodeMcp(cwd, stateDir, bundleInStatus(snapshot) ?? resolve12(cliDir, "../../plugins/agent-hub/server.js"));
9138
9337
  if (written.tracked)
9139
9338
  console.error(".jcode/mcp.json is tracked; review this local machine-path change before committing it");
9140
- const reply = await hub.request({ t: "start", peer: "jcode", args: { mode: "headless", model: one["--model"], providerProfile: one["--provider-profile"], sessionId: one["--session-id"] }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION }, 65000);
9339
+ const reply = await hub.request({ t: "start", peer: "jcode", args: { mode, ...options }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION }, 65000);
9340
+ if (interruptedWait())
9341
+ return;
9141
9342
  if (!reply.ok)
9142
9343
  fail(reply.error);
9143
- console.log(reply.already ? "jcode is already attached" : "jcode attached (headless); native policy unmanaged, NO hub sandbox");
9344
+ if (mode === "headless") {
9345
+ console.log(reply.already ? "jcode is already attached" : "jcode attached (headless); native policy unmanaged, NO hub sandbox");
9346
+ return;
9347
+ }
9348
+ const ready = await waitForJcodeTui(hub, fail, abort.signal, options);
9349
+ if (interruptedWait() || !ready)
9350
+ return;
9351
+ const instanceId = typeof snapshot?.instanceId === "string" ? snapshot.instanceId : undefined;
9352
+ if (!instanceId)
9353
+ return fail("cannot bind Jcode's terminal to the running hub");
9354
+ await ctx.helpers.recordTerminalLaunch("jcode", cwd, stateDir, instanceId);
9355
+ const confirmed = await hub.request({ t: "native_tui_launch", peer: "jcode", binding: ready.binding, args: options });
9356
+ if (interruptedWait())
9357
+ return;
9358
+ if (!confirmed.ok || confirmed.state !== "ready")
9359
+ fail("Jcode launch changed before its terminal opened");
9360
+ ctx.execWithEnv(confirmed.launch.cmd, confirmed.launch.args, { ...ctx.helpers.childEnv(), ...confirmed.launch.env });
9144
9361
  } finally {
9362
+ process.removeListener("SIGINT", interrupt);
9363
+ hub.onClose = oldClose;
9145
9364
  hub.close();
9146
9365
  }
9147
9366
  }
9367
+ var WAITING = "jcode's TUI opens after its first hub turn; send it work, or Ctrl-C";
9148
9368
  var init_native_cli_entry_jcode = () => {};
9149
9369
 
9150
9370
  // src/adapters/definitions/native-cli-entry-qwen.ts
@@ -9605,23 +9825,23 @@ var init_native_cli_preview_qwen = () => {};
9605
9825
  // src/adapters/definitions/native-cli-preview-jcode.ts
9606
9826
  function previewJcode(ctx) {
9607
9827
  const { args, config } = ctx;
9608
- const { jcodeCommand, qwenSessionId } = ctx.helpers;
9828
+ const { jcodeCommand, jcodeTuiCommand, qwenSessionId, startModeOf } = ctx.helpers;
9609
9829
  const state = previewState(args, ctx.baseEnvNames), { unresolved, value } = state;
9610
9830
  let envNames = state.envNames;
9611
9831
  let launch;
9612
- const mode = value("--mode") ?? "headless", model = value("--model"), profile = value("--provider-profile"), session = value("--session-id");
9613
- if (mode !== "headless")
9614
- throw new Error("Jcode TUI driving is unverified");
9832
+ const mode = value("--mode") ?? startModeOf(config.peers, "jcode"), model = value("--model"), profile = value("--provider-profile"), session = value("--session-id");
9833
+ if (mode !== "headless" && mode !== "tui")
9834
+ throw new Error("invalid Jcode start mode");
9615
9835
  for (let i = 0;i < args.length; i += 2)
9616
9836
  if (!["--model", "--mode", "--provider-profile", "--session-id"].includes(args[i]) || !args[i + 1])
9617
9837
  throw new Error("unknown Jcode option");
9618
9838
  if (session !== undefined && !qwenSessionId(session))
9619
9839
  throw new Error("invalid Jcode session identity");
9620
- const cmd = jcodeCommand(nativeCommand(config.jcode_cmd, ["jcode"]), "acp", UNRESOLVED, UNRESOLVED, model ? REDACTED : undefined, profile || config.jcode_provider_profile ? REDACTED : undefined);
9840
+ const cmd = mode === "tui" ? jcodeTuiCommand(nativeCommand(config.jcode_cmd, ["jcode"]), UNRESOLVED, UNRESOLVED, model ? REDACTED : undefined, profile || config.jcode_provider_profile ? REDACTED : undefined) : jcodeCommand(nativeCommand(config.jcode_cmd, ["jcode"]), "acp", UNRESOLVED, UNRESOLVED, model ? REDACTED : undefined, profile || config.jcode_provider_profile ? REDACTED : undefined);
9621
9841
  launch = { cmd: cmd[0], args: cmd.slice(1) };
9622
9842
  unresolved.push({ field: "ownedDaemon", reason: "private daemon/socket and source-pinned native identity verified at start; preview starts nothing" });
9623
9843
  unresolved.push({ field: "hubTools", reason: "project MCP bridge required; native tool-call receipt remains unverified" });
9624
- return finishPreview(ctx, launch, envNames, unresolved, undefined);
9844
+ return { ...finishPreview(ctx, launch, envNames, unresolved), mode };
9625
9845
  }
9626
9846
  var init_native_cli_preview_jcode = () => {};
9627
9847
 
@@ -9651,7 +9871,7 @@ function kimiAcpCommand(command, model) {
9651
9871
  }
9652
9872
 
9653
9873
  // src/adapters/definitions/native-daemon-policy.ts
9654
- var claudeRecoveryLaunch = (instance) => ({ kind: "claude-channel", peer: instance }), kimiRecoveryLaunch = (instance) => ({ kind: "kimi-tui", peer: instance, mode: "tui" }), claudeRetainedTerminalNotice = (instance) => `Stop ${instance}'s existing work in its retained terminal; removal detaches the hub connection and does not stop that terminal's work.`, piRetainedTerminalNotice = (instance, form) => form === "tui" ? claudeRetainedTerminalNotice(instance) : undefined, codexRetainedTerminalNotice = (_instance, _form, resumeCommand) => `Check codex's existing terminal and stop any remaining work. Its app-server and proxy stay available, but Codex 0.146.0 exits its TUI on disconnect and does not reconnect automatically; after ahub roster add codex, resume the previous session with ${resumeCommand}.`, codexRosterReturnNotice = (resumeCommand) => `Codex 0.146.0 needs a manual return: resume the previous native session with ${resumeCommand}.`, jcodeStartPreflight = (request) => request.form !== undefined && request.form !== "headless" ? "Jcode native TUI driving is unverified; use headless" : undefined, claudeObservedVersion = (context) => context.observedVersion, codexObservedVersion = (context) => context.peer?.version ?? context.observedVersion;
9874
+ var claudeRecoveryLaunch = (instance) => ({ kind: "claude-channel", peer: instance }), kimiRecoveryLaunch = (instance) => ({ kind: "kimi-tui", peer: instance, mode: "tui" }), claudeRetainedTerminalNotice = (instance) => `Stop ${instance}'s existing work in its retained terminal; removal detaches the hub connection and does not stop that terminal's work.`, piRetainedTerminalNotice = (instance, form) => form === "tui" ? claudeRetainedTerminalNotice(instance) : undefined, codexRetainedTerminalNotice = (_instance, _form, resumeCommand) => `Check codex's existing terminal and stop any remaining work. Its app-server and proxy stay available, but Codex 0.146.0 exits its TUI on disconnect and does not reconnect automatically; after ahub roster add codex, resume the previous session with ${resumeCommand}.`, codexRosterReturnNotice = (resumeCommand) => `Codex 0.146.0 needs a manual return: resume the previous native session with ${resumeCommand}.`, jcodeStartPreflight = (request) => request.form !== undefined && request.form !== "headless" && request.form !== "tui" ? "invalid Jcode start mode" : undefined, claudeObservedVersion = (context) => context.observedVersion, codexObservedVersion = (context) => context.peer?.version ?? context.observedVersion;
9655
9875
 
9656
9876
  // src/adapters/definitions/native-config-policy.ts
9657
9877
  function piConfigSchema(v) {
@@ -9697,7 +9917,7 @@ var init_native_config_policy = __esm(() => {
9697
9917
  });
9698
9918
 
9699
9919
  // src/adapters/definitions/native-policy-aux.ts
9700
- import { closeSync as closeSync5, constants as constants4, fstatSync as fstatSync4, openSync as openSync5, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
9920
+ import { closeSync as closeSync6, constants as constants5, fstatSync as fstatSync5, openSync as openSync6, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
9701
9921
  import { isAbsolute as isAbsolute7, join as join20, normalize } from "path";
9702
9922
  function claudeCrashPlan(_meta) {
9703
9923
  return { how: "claude: the Claude Code plugin reconnects by itself while that session is still open" };
@@ -9802,8 +10022,8 @@ function absoluteContinuityPath(value) {
9802
10022
  var str3 = (value) => typeof value === "string" && value ? value : undefined, PLUGINS, CLAUDE_ONLY, text = (file) => {
9803
10023
  let fd;
9804
10024
  try {
9805
- fd = openSync5(file, constants4.O_RDONLY | constants4.O_NONBLOCK);
9806
- const st = fstatSync4(fd);
10025
+ fd = openSync6(file, constants5.O_RDONLY | constants5.O_NONBLOCK);
10026
+ const st = fstatSync5(fd);
9807
10027
  if (!st.isFile() || st.size > 1024 * 1024)
9808
10028
  return;
9809
10029
  return readFileSync9(fd, "utf8");
@@ -9811,7 +10031,7 @@ var str3 = (value) => typeof value === "string" && value ? value : undefined, PL
9811
10031
  return;
9812
10032
  } finally {
9813
10033
  if (fd !== undefined)
9814
- closeSync5(fd);
10034
+ closeSync6(fd);
9815
10035
  }
9816
10036
  }, json = (file) => {
9817
10037
  try {
@@ -10009,7 +10229,7 @@ var init_native_cli_codex_launch = __esm(() => {
10009
10229
  });
10010
10230
 
10011
10231
  // src/hub/events.ts
10012
- import { appendFileSync, closeSync as closeSync6, existsSync as existsSync7, fstatSync as fstatSync5, openSync as openSync6, readFileSync as readFileSync11, readSync as readSync3 } from "fs";
10232
+ import { appendFileSync, closeSync as closeSync7, existsSync as existsSync7, fstatSync as fstatSync6, openSync as openSync7, readFileSync as readFileSync11, readSync as readSync4 } from "fs";
10013
10233
  function readEvents(file, since = 0) {
10014
10234
  if (!existsSync7(file))
10015
10235
  return [];
@@ -10171,8 +10391,8 @@ function claudeWindows(rateLimits) {
10171
10391
  }
10172
10392
 
10173
10393
  // src/adapters/definitions/builtin-metadata.ts
10174
- function compatibilityLaunch(kind, ctx) {
10175
- const mode = kind === "kimi" || kind === "pi" ? ["--mode", ctx.form] : [];
10394
+ function compatibilityLaunch(kind, ctx, explicitMode = false) {
10395
+ const mode = explicitMode || kind === "kimi" || kind === "pi" ? ["--mode", ctx.form] : [];
10176
10396
  const unattended = ctx.unattended && (kind === "claude" || kind === "codex") ? ["--unattended"] : [];
10177
10397
  const args = [
10178
10398
  ctx.entrypoint,
@@ -10408,7 +10628,7 @@ var init_builtin_metadata = __esm(() => {
10408
10628
  optional: true,
10409
10629
  defaultOnRoster: false,
10410
10630
  trust: { onPrem: false, hubSandboxed: false },
10411
- start: { create: createJcode, preflight: jcodeStartPreflight, daemonPort: "owned-acp", forms: headlessForms, defaultForm: "headless", ownsTerminal: headless, launch: (ctx) => compatibilityLaunch("jcode", ctx), conductorStartable: true, hubSwitches: [], ownedFlags: [] },
10631
+ start: { create: createJcode, preflight: jcodeStartPreflight, daemonPort: "owned-acp", forms: dualForms, defaultForm: "tui", ownsTerminal: terminal, explicitHeadlessMode: true, launch: (ctx) => compatibilityLaunch("jcode", ctx, true), conductorStartable: true, hubSwitches: [], ownedFlags: [] },
10412
10632
  identity: { mcp: "project-file" },
10413
10633
  permission: { modes: {}, unmanaged: "Jcode native policy is unmanaged; no hub mode mapping", grants: {
10414
10634
  ask: "no verified hub mode mapping; native policy only",
@@ -10418,7 +10638,7 @@ var init_builtin_metadata = __esm(() => {
10418
10638
  session: { idField: "sessionId", currentId: sessionId("sessionId"), launchKind: "jcode", ...JCODE_RECOVERY_SESSION, crashPlan: jcodeCrashPlan },
10419
10639
  turnEnd: "adapter",
10420
10640
  budget: { account: "jcode" },
10421
- cli: { command: "jcode", authority: "person", launchConfigKeys: ["jcode_cmd", "jcode_home", "jcode_provider_profile"], startWords: () => ["jcode"] },
10641
+ cli: { command: "jcode", authority: "person", launchConfigKeys: ["jcode_cmd", "jcode_home", "jcode_provider_profile"], startWords: (mode) => ["jcode", "--mode", mode] },
10422
10642
  display: JCODE_DISPLAY,
10423
10643
  hooks: { recallPlatform: "jcode" }
10424
10644
  }
@@ -17993,7 +18213,7 @@ var STDOUT_LIMIT = 512 * 1024;
17993
18213
  // package.json
17994
18214
  var package_default = {
17995
18215
  name: "@staix/agent-hub",
17996
- version: "0.12.38",
18216
+ version: "0.12.39",
17997
18217
  description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
17998
18218
  license: "MIT",
17999
18219
  type: "module",