@ganglion/xacpx 0.11.0 → 0.12.1

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.
@@ -68,6 +68,10 @@ function encodeBridgePromptPlanEvent(event) {
68
68
  return `${JSON.stringify(event)}
69
69
  `;
70
70
  }
71
+ function encodeBridgePromptUsageEvent(event) {
72
+ return `${JSON.stringify(event)}
73
+ `;
74
+ }
71
75
  function encodeBridgeSessionProgressEvent(event) {
72
76
  return `${JSON.stringify(event)}
73
77
  `;
@@ -424,6 +428,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
424
428
  let onToolEvent;
425
429
  let onThought;
426
430
  let onPlan;
431
+ let onUsage;
427
432
  let rawStream = false;
428
433
  if (options === undefined) {
429
434
  toolEventMode = "text";
@@ -435,6 +440,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
435
440
  onToolEvent = options.onToolEvent;
436
441
  onThought = options.onThought;
437
442
  onPlan = options.onPlan;
443
+ onUsage = options.onUsage;
438
444
  rawStream = options.rawStream ?? false;
439
445
  toolEventMode = resolveToolEventMode({
440
446
  toolEventMode: options.mode,
@@ -453,6 +459,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
453
459
  onToolEvent,
454
460
  onThought,
455
461
  onPlan,
462
+ onUsage,
456
463
  finalize() {
457
464
  if (this.pendingLine.trim().length > 0) {
458
465
  parseStreamingChunks(this, this.pendingLine);
@@ -517,6 +524,13 @@ function parseStreamingChunks(state, line) {
517
524
  state.onPlan?.(entries);
518
525
  return;
519
526
  }
527
+ if (update.sessionUpdate === "usage_update") {
528
+ const used = typeof update.used === "number" && Number.isFinite(update.used) ? update.used : undefined;
529
+ const size = typeof update.size === "number" && Number.isFinite(update.size) ? update.size : undefined;
530
+ if (used !== undefined && size !== undefined && size > 0)
531
+ state.onUsage?.({ used, size });
532
+ return;
533
+ }
520
534
  const isThoughtChunk = update.sessionUpdate === "agent_thought_chunk" && update.content?.type === "text" && typeof update.content.text === "string";
521
535
  if (isThoughtChunk) {
522
536
  const chunk2 = update.content.text;
@@ -1683,6 +1697,9 @@ var init_channel_cli = __esm(() => {
1683
1697
  channelAdded: (type) => `Channel ${type} added`,
1684
1698
  cannotRemoveLastEnabled: "Cannot remove the last enabled channel.",
1685
1699
  channelRemoved: (id) => `Channel ${id} removed`,
1700
+ channelCredentialsCleared: (id) => `Removed stored credentials for channel ${id}`,
1701
+ channelCredentialsClearFailed: (id, error) => `Channel ${id} removed, but clearing its stored credentials failed: ${error}`,
1702
+ channelCredentialsKept: (id) => `Kept stored credentials for channel ${id} (--keep-credentials)`,
1686
1703
  cannotDisableLastEnabled: "Cannot disable the last enabled channel.",
1687
1704
  channelEnabledToggled: (id, enabled) => `Channel ${id} ${enabled ? "enabled" : "disabled"}`,
1688
1705
  channelReplyModeSet: (id, mode) => `Channel ${id} default reply mode set to: ${mode}`,
@@ -2775,6 +2792,9 @@ var init_channel_cli2 = __esm(() => {
2775
2792
  channelAdded: (type) => `频道 ${type} 已添加`,
2776
2793
  cannotRemoveLastEnabled: "不能删除最后一个启用的频道。",
2777
2794
  channelRemoved: (id) => `频道 ${id} 已删除`,
2795
+ channelCredentialsCleared: (id) => `已移除频道 ${id} 的存储凭证`,
2796
+ channelCredentialsClearFailed: (id, error) => `频道 ${id} 已删除,但清除其存储凭证失败:${error}`,
2797
+ channelCredentialsKept: (id) => `已保留频道 ${id} 的存储凭证(--keep-credentials)`,
2778
2798
  cannotDisableLastEnabled: "不能禁用最后一个启用的频道。",
2779
2799
  channelEnabledToggled: (id, enabled) => `频道 ${id} 已${enabled ? "启用" : "禁用"}`,
2780
2800
  channelReplyModeSet: (id, mode) => `频道 ${id} 的默认 reply mode 已设置为:${mode}`,
@@ -3989,7 +4009,8 @@ async function runStreamingPrompt(command, args, onEvent, options = {}) {
3989
4009
  rawStream,
3990
4010
  ...onEvent && (toolEventMode === "structured" || toolEventMode === "both") ? { onToolEvent: (toolEvent) => onEvent({ type: "prompt.tool_event", event: toolEvent }) } : {},
3991
4011
  ...onEvent ? { onThought: (chunk) => onEvent({ type: "prompt.thought", text: chunk }) } : {},
3992
- ...onEvent ? { onPlan: (entries) => onEvent({ type: "prompt.plan", entries }) } : {}
4012
+ ...onEvent ? { onPlan: (entries) => onEvent({ type: "prompt.plan", entries }) } : {},
4013
+ ...onEvent ? { onUsage: (usage) => onEvent({ type: "prompt.usage", used: usage.used, size: usage.size }) } : {}
3993
4014
  });
3994
4015
  let lastReplyAt = now();
3995
4016
  const flushBuffer = () => {
@@ -4280,6 +4301,13 @@ class BridgeServer {
4280
4301
  event: "prompt.plan",
4281
4302
  entries: event.entries
4282
4303
  }));
4304
+ } else if (event.type === "prompt.usage") {
4305
+ writeLine?.(encodeBridgePromptUsageEvent({
4306
+ id: requestId,
4307
+ event: "prompt.usage",
4308
+ used: event.used,
4309
+ size: event.size
4310
+ }));
4283
4311
  }
4284
4312
  });
4285
4313
  case "resumeAgentSession":
package/dist/cli.js CHANGED
@@ -898,6 +898,9 @@ var init_channel_cli = __esm(() => {
898
898
  channelAdded: (type) => `Channel ${type} added`,
899
899
  cannotRemoveLastEnabled: "Cannot remove the last enabled channel.",
900
900
  channelRemoved: (id) => `Channel ${id} removed`,
901
+ channelCredentialsCleared: (id) => `Removed stored credentials for channel ${id}`,
902
+ channelCredentialsClearFailed: (id, error) => `Channel ${id} removed, but clearing its stored credentials failed: ${error}`,
903
+ channelCredentialsKept: (id) => `Kept stored credentials for channel ${id} (--keep-credentials)`,
901
904
  cannotDisableLastEnabled: "Cannot disable the last enabled channel.",
902
905
  channelEnabledToggled: (id, enabled) => `Channel ${id} ${enabled ? "enabled" : "disabled"}`,
903
906
  channelReplyModeSet: (id, mode) => `Channel ${id} default reply mode set to: ${mode}`,
@@ -1990,6 +1993,9 @@ var init_channel_cli2 = __esm(() => {
1990
1993
  channelAdded: (type) => `频道 ${type} 已添加`,
1991
1994
  cannotRemoveLastEnabled: "不能删除最后一个启用的频道。",
1992
1995
  channelRemoved: (id) => `频道 ${id} 已删除`,
1996
+ channelCredentialsCleared: (id) => `已移除频道 ${id} 的存储凭证`,
1997
+ channelCredentialsClearFailed: (id, error) => `频道 ${id} 已删除,但清除其存储凭证失败:${error}`,
1998
+ channelCredentialsKept: (id) => `已保留频道 ${id} 的存储凭证(--keep-credentials)`,
1993
1999
  cannotDisableLastEnabled: "不能禁用最后一个启用的频道。",
1994
2000
  channelEnabledToggled: (id, enabled) => `频道 ${id} 已${enabled ? "启用" : "禁用"}`,
1995
2001
  channelReplyModeSet: (id, mode) => `频道 ${id} 的默认 reply mode 已设置为:${mode}`,
@@ -22355,7 +22361,7 @@ async function handleSessionRemove(context, chatKey, alias) {
22355
22361
  return { text: lines.join(`
22356
22362
  `) };
22357
22363
  }
22358
- async function promptWithSession(context, session3, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan) {
22364
+ async function promptWithSession(context, session3, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage) {
22359
22365
  const effectiveReplyMode = resolveEffectiveReplyMode(context.config, chatKey, session3.replyMode);
22360
22366
  if (!session3.replyMode)
22361
22367
  session3.replyMode = effectiveReplyMode;
@@ -22387,7 +22393,7 @@ async function promptWithSession(context, session3, chatKey, text, reply, replyC
22387
22393
  const { promptText, taskIds, groupIds, claimHumanReply } = await preparePromptWithFallback(context, session3, chatKey, text, replyContextToken, accountId);
22388
22394
  try {
22389
22395
  const replyContext = transportReply && context.quota && getChannelIdFromChatKey(chatKey) === "weixin" ? { chatKey, quota: context.quota } : undefined;
22390
- const result = await context.interaction.promptTransportSession(session3, promptText, transportReply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpan, onPlan);
22396
+ const result = await context.interaction.promptTransportSession(session3, promptText, transportReply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpan, onPlan, onUsage);
22391
22397
  if (claimHumanReply) {
22392
22398
  try {
22393
22399
  await context.orchestration?.claimActiveHumanReply?.(claimHumanReply);
@@ -22407,23 +22413,23 @@ async function promptWithSession(context, session3, chatKey, text, reply, replyC
22407
22413
  throw error2;
22408
22414
  }
22409
22415
  }
22410
- async function handlePromptWithSession(context, session3, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan) {
22416
+ async function handlePromptWithSession(context, session3, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage) {
22411
22417
  try {
22412
- return await promptWithSession(context, session3, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan);
22418
+ return await promptWithSession(context, session3, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage);
22413
22419
  } catch (error2) {
22414
22420
  const recovered = await context.recovery.tryRecoverMissingSession(session3, error2);
22415
22421
  if (recovered) {
22416
- return await promptWithSession(context, recovered, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan);
22422
+ return await promptWithSession(context, recovered, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage);
22417
22423
  }
22418
22424
  return context.recovery.renderTransportError(session3, error2);
22419
22425
  }
22420
22426
  }
22421
- async function handlePrompt(context, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan) {
22427
+ async function handlePrompt(context, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage) {
22422
22428
  const session3 = metadata?.boundSessionAlias ? context.sessions.getResolvedSessionByInternalAlias(metadata.boundSessionAlias) : await context.sessions.getCurrentSession(chatKey);
22423
22429
  if (!session3) {
22424
22430
  return { text: t().session.noCurrent };
22425
22431
  }
22426
- return await handlePromptWithSession(context, session3, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan);
22432
+ return await handlePromptWithSession(context, session3, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage);
22427
22433
  }
22428
22434
  function toCoordinatorRouteChatMetadata(metadata) {
22429
22435
  if (!metadata) {
@@ -24682,7 +24688,7 @@ class CommandRouter {
24682
24688
  this.logger = logger2 ?? createNoopAppLogger();
24683
24689
  this.activeTurns = activeTurns;
24684
24690
  }
24685
- async handle(chatKey, input, reply, replyContextToken, accountId, media, metadata, abortSignal, onToolEvent, onThought, perfSpan, onPlan) {
24691
+ async handle(chatKey, input, reply, replyContextToken, accountId, media, metadata, abortSignal, onToolEvent, onThought, perfSpan, onPlan, onUsage) {
24686
24692
  const startedAt = Date.now();
24687
24693
  let command = parseCommand(input);
24688
24694
  if (metadata?.channel === "control" && command.kind !== "prompt") {
@@ -24842,16 +24848,16 @@ class CommandRouter {
24842
24848
  ...this.sessions.resolveSession(descriptor.alias, descriptor.agent, descriptor.workspace, descriptor.transportSession),
24843
24849
  transient: true
24844
24850
  };
24845
- return await handlePromptWithSession(sessionContext, transientSession, chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan);
24851
+ return await handlePromptWithSession(sessionContext, transientSession, chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage);
24846
24852
  }
24847
24853
  if (metadata?.scheduledSessionAlias) {
24848
24854
  const scheduledSession = await this.sessions.getSession(metadata.scheduledSessionAlias);
24849
24855
  if (!scheduledSession) {
24850
24856
  throw new Error(`session "${metadata.scheduledSessionAlias}" not found for scheduled prompt`);
24851
24857
  }
24852
- return await handlePromptWithSession(sessionContext, scheduledSession, chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan);
24858
+ return await handlePromptWithSession(sessionContext, scheduledSession, chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage);
24853
24859
  }
24854
- return await handlePrompt(sessionContext, chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan);
24860
+ return await handlePrompt(sessionContext, chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage);
24855
24861
  }
24856
24862
  }
24857
24863
  });
@@ -24999,7 +25005,7 @@ class CommandRouter {
24999
25005
  setModelTransportSession: (session3, modelId) => this.setModelTransportSession(session3, modelId),
25000
25006
  getModelTransportSession: (session3) => this.getModelTransportSession(session3),
25001
25007
  cancelTransportSession: (session3) => this.cancelTransportSession(session3),
25002
- promptTransportSession: (session3, text, reply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpanOverride, onPlan) => this.promptTransportSession(session3, text, reply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpanOverride ?? perfSpan, onPlan)
25008
+ promptTransportSession: (session3, text, reply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpanOverride, onPlan, onUsage) => this.promptTransportSession(session3, text, reply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpanOverride ?? perfSpan, onPlan, onUsage)
25003
25009
  };
25004
25010
  }
25005
25011
  createSessionRenderRecoveryOps() {
@@ -25178,7 +25184,7 @@ class CommandRouter {
25178
25184
  async checkTransportSession(session3) {
25179
25185
  return await this.measureTransportCall("has_session", session3, () => this.transport.hasSession(session3));
25180
25186
  }
25181
- async promptTransportSession(session3, text, reply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpan, onPlan) {
25187
+ async promptTransportSession(session3, text, reply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpan, onPlan, onUsage) {
25182
25188
  session3.mcpCoordinatorSession ??= stableCoordinatorSession(session3.transportSession);
25183
25189
  let done = false;
25184
25190
  let abortRequested = false;
@@ -25236,7 +25242,8 @@ class CommandRouter {
25236
25242
  ...reply ? { onSegment } : {},
25237
25243
  ...onToolEvent ? { onToolEvent } : {},
25238
25244
  ...onThought ? { onThought } : {},
25239
- ...onPlan ? { onPlan } : {}
25245
+ ...onPlan ? { onPlan } : {},
25246
+ ...onUsage ? { onUsage } : {}
25240
25247
  }));
25241
25248
  } catch (error2) {
25242
25249
  localOutcome = isAbortError2(error2) || abortRequested ? "aborted" : "error";
@@ -25422,7 +25429,7 @@ class ConsoleAgent {
25422
25429
  ...m.fileName ? { fileName: m.fileName } : {}
25423
25430
  })) : undefined;
25424
25431
  request.perfSpan?.mark("agent.dispatched");
25425
- return await this.router.handle(request.conversationId, request.text, request.reply, request.replyContextToken, request.accountId, promptMedia, request.metadata, request.abortSignal, request.onToolEvent, request.onThought, request.perfSpan, request.onPlan);
25432
+ return await this.router.handle(request.conversationId, request.text, request.reply, request.replyContextToken, request.accountId, promptMedia, request.metadata, request.abortSignal, request.onToolEvent, request.onThought, request.perfSpan, request.onPlan, request.onUsage);
25426
25433
  }
25427
25434
  isKnownCommand(text) {
25428
25435
  return isKnownXacpxCommandText(text);
@@ -30478,6 +30485,10 @@ function encodeBridgePromptPlanEvent(event) {
30478
30485
  return `${JSON.stringify(event)}
30479
30486
  `;
30480
30487
  }
30488
+ function encodeBridgePromptUsageEvent(event) {
30489
+ return `${JSON.stringify(event)}
30490
+ `;
30491
+ }
30481
30492
  function encodeBridgeSessionProgressEvent(event) {
30482
30493
  return `${JSON.stringify(event)}
30483
30494
  `;
@@ -30780,6 +30791,12 @@ class AcpxBridgeClient {
30780
30791
  type: "prompt.plan",
30781
30792
  entries: message.entries
30782
30793
  });
30794
+ } else if (message.event === "prompt.usage") {
30795
+ pending.onEvent?.({
30796
+ type: "prompt.usage",
30797
+ used: message.used,
30798
+ size: message.size
30799
+ });
30783
30800
  } else if (message.event === "session.progress") {
30784
30801
  pending.onEvent?.({
30785
30802
  type: "session.progress",
@@ -31239,6 +31256,8 @@ class AcpxBridgeTransport {
31239
31256
  let thoughtChain = Promise.resolve();
31240
31257
  let planError;
31241
31258
  let planChain = Promise.resolve();
31259
+ let usageError;
31260
+ let usageChain = Promise.resolve();
31242
31261
  let toolEventMode = resolveToolEventMode(options);
31243
31262
  if ((toolEventMode === "structured" || toolEventMode === "both") && !options?.onToolEvent) {
31244
31263
  toolEventMode = "text";
@@ -31291,11 +31310,22 @@ class AcpxBridgeTransport {
31291
31310
  }
31292
31311
  return;
31293
31312
  }
31313
+ if (event.type === "prompt.usage") {
31314
+ const onUsage = options?.onUsage;
31315
+ if (onUsage) {
31316
+ const usage = { used: event.used, size: event.size };
31317
+ usageChain = usageChain.then(() => onUsage(usage)).catch((error2) => {
31318
+ usageError ??= error2;
31319
+ });
31320
+ }
31321
+ return;
31322
+ }
31294
31323
  });
31295
31324
  await segmentChain;
31296
31325
  await toolEventChain;
31297
31326
  await thoughtChain;
31298
31327
  await planChain;
31328
+ await usageChain;
31299
31329
  if (sink) {
31300
31330
  const { overflowCount } = sink.finalize();
31301
31331
  await sink.drain({ timeoutMs: 30000 });
@@ -31316,6 +31346,9 @@ class AcpxBridgeTransport {
31316
31346
  if (planError) {
31317
31347
  throw planError;
31318
31348
  }
31349
+ if (usageError) {
31350
+ throw usageError;
31351
+ }
31319
31352
  return { text: summary ? `${summary}
31320
31353
 
31321
31354
  ${result.text}` : "" };
@@ -31332,6 +31365,9 @@ ${result.text}` : "" };
31332
31365
  if (planError) {
31333
31366
  throw planError;
31334
31367
  }
31368
+ if (usageError) {
31369
+ throw usageError;
31370
+ }
31335
31371
  return result;
31336
31372
  }
31337
31373
  async setMode(session3, modeId) {
@@ -31537,6 +31573,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
31537
31573
  let onToolEvent;
31538
31574
  let onThought;
31539
31575
  let onPlan;
31576
+ let onUsage;
31540
31577
  let rawStream = false;
31541
31578
  if (options === undefined) {
31542
31579
  toolEventMode = "text";
@@ -31548,6 +31585,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
31548
31585
  onToolEvent = options.onToolEvent;
31549
31586
  onThought = options.onThought;
31550
31587
  onPlan = options.onPlan;
31588
+ onUsage = options.onUsage;
31551
31589
  rawStream = options.rawStream ?? false;
31552
31590
  toolEventMode = resolveToolEventMode({
31553
31591
  toolEventMode: options.mode,
@@ -31566,6 +31604,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
31566
31604
  onToolEvent,
31567
31605
  onThought,
31568
31606
  onPlan,
31607
+ onUsage,
31569
31608
  finalize() {
31570
31609
  if (this.pendingLine.trim().length > 0) {
31571
31610
  parseStreamingChunks(this, this.pendingLine);
@@ -31630,6 +31669,13 @@ function parseStreamingChunks(state, line) {
31630
31669
  state.onPlan?.(entries);
31631
31670
  return;
31632
31671
  }
31672
+ if (update.sessionUpdate === "usage_update") {
31673
+ const used = typeof update.used === "number" && Number.isFinite(update.used) ? update.used : undefined;
31674
+ const size = typeof update.size === "number" && Number.isFinite(update.size) ? update.size : undefined;
31675
+ if (used !== undefined && size !== undefined && size > 0)
31676
+ state.onUsage?.({ used, size });
31677
+ return;
31678
+ }
31633
31679
  const isThoughtChunk = update.sessionUpdate === "agent_thought_chunk" && update.content?.type === "text" && typeof update.content.text === "string";
31634
31680
  if (isThoughtChunk) {
31635
31681
  const chunk2 = update.content.text;
@@ -33259,7 +33305,29 @@ class WorkspaceFs {
33259
33305
  }
33260
33306
  }
33261
33307
  const truncated = diff.length > DIFF_CAP;
33262
- return { workspace: workspace3, files, diff: truncated ? diff.slice(0, DIFF_CAP) : diff, truncated };
33308
+ return { workspace: workspace3, files, diff: truncated ? diff.slice(0, DIFF_CAP) : diff, truncated, ...await this.gitContext(root) };
33309
+ }
33310
+ async gitContext(root) {
33311
+ const run = async (...args) => {
33312
+ try {
33313
+ return (await execFileAsync("git", ["-C", root, ...args], { maxBuffer: GIT_MAX_BUFFER })).stdout.trim();
33314
+ } catch {
33315
+ return null;
33316
+ }
33317
+ };
33318
+ const ctx = {};
33319
+ const head = await run("rev-parse", "--abbrev-ref", "HEAD");
33320
+ if (head === "HEAD")
33321
+ ctx.detached = true;
33322
+ else if (head)
33323
+ ctx.branch = head;
33324
+ const top = await run("rev-parse", "--show-toplevel");
33325
+ if (top) {
33326
+ const gitDir = await run("rev-parse", "--absolute-git-dir");
33327
+ const commonDir = await run("rev-parse", "--path-format=absolute", "--git-common-dir");
33328
+ ctx.worktree = { root: top, linked: !!gitDir && !!commonDir && gitDir !== commonDir };
33329
+ }
33330
+ return ctx;
33263
33331
  }
33264
33332
  }
33265
33333
  var execFileAsync, MAX_ENTRIES = 2000, FILE_READ_CAP, DIFF_CAP, GIT_MAX_BUFFER, SEARCH_MAX_RESULTS = 200, SEARCH_MAX_SCAN = 20000, SEARCH_SKIP_DIRS;
@@ -33526,6 +33594,15 @@ ${chunk}` : chunk
33526
33594
  sessionAlias: params.sessionAlias,
33527
33595
  entries
33528
33596
  });
33597
+ },
33598
+ onUsage: (usage) => {
33599
+ this.deps.events.emit({
33600
+ type: "turn-usage",
33601
+ chatKey: params.chatKey,
33602
+ sessionAlias: params.sessionAlias,
33603
+ used: usage.used,
33604
+ size: usage.size
33605
+ });
33529
33606
  }
33530
33607
  });
33531
33608
  if (response.text) {
@@ -50830,6 +50907,7 @@ async function removeChannel(type, rawArgs, deps) {
50830
50907
  deps.print(restartFlags.message);
50831
50908
  return 1;
50832
50909
  }
50910
+ const keepCredentials = restartFlags.rest.includes("--keep-credentials");
50833
50911
  const config4 = await deps.loadConfig();
50834
50912
  ensureChannelsArray(config4);
50835
50913
  const channel = findChannel(config4.channels, type);
@@ -50844,6 +50922,16 @@ async function removeChannel(type, rawArgs, deps) {
50844
50922
  config4.channels = config4.channels.filter((entry) => entry.id !== channel.id);
50845
50923
  await deps.saveChannels(config4.channels);
50846
50924
  deps.print(t().channelCli.channelRemoved(channel.id));
50925
+ if (keepCredentials) {
50926
+ deps.print(t().channelCli.channelCredentialsKept(channel.id));
50927
+ } else if (deps.clearChannelCredentials) {
50928
+ try {
50929
+ await deps.clearChannelCredentials(channel);
50930
+ deps.print(t().channelCli.channelCredentialsCleared(channel.id));
50931
+ } catch (error2) {
50932
+ deps.print(t().channelCli.channelCredentialsClearFailed(channel.id, error2 instanceof Error ? error2.message : String(error2)));
50933
+ }
50934
+ }
50847
50935
  return await maybeRestartAfterMutation(restartFlags.restart, deps);
50848
50936
  }
50849
50937
  async function setChannelEnabled(type, enabled, rawArgs, deps) {
@@ -52569,7 +52657,11 @@ async function createChannelCliDeps(input) {
52569
52657
  return { state: "indeterminate", pid: status.pid, reason: status.reason };
52570
52658
  return { state: "stopped" };
52571
52659
  },
52572
- restartDaemon: async () => await restartDaemonCli(controller, input.print)
52660
+ restartDaemon: async () => await restartDaemonCli(controller, input.print),
52661
+ clearChannelCredentials: async (channel) => {
52662
+ const { createMessageChannel: createMessageChannel2 } = await Promise.resolve().then(() => (init_create_channel(), exports_create_channel));
52663
+ createMessageChannel2(channel.type, channel).logout();
52664
+ }
52573
52665
  };
52574
52666
  return { ...base, ...input.overrides };
52575
52667
  }
@@ -38,5 +38,11 @@ export declare function handleCancel(context: SessionHandlerContext, chatKey: st
38
38
  export declare function handleSessionReset(context: SessionHandlerContext, chatKey: string): Promise<RouterResponse>;
39
39
  export declare function handleSessionTail(context: SessionHandlerContext, chatKey: string, lines?: number): Promise<RouterResponse>;
40
40
  export declare function handleSessionRemove(context: SessionHandlerContext, chatKey: string, alias: string): Promise<RouterResponse>;
41
- export declare function handlePromptWithSession(context: SessionHandlerContext, session: ResolvedSession, chatKey: string, text: string, reply?: (text: string) => Promise<void>, replyContextToken?: string, accountId?: string, media?: PromptMediaInput, abortSignal?: AbortSignal, onToolEvent?: (event: ToolUseEvent) => void | Promise<void>, onThought?: (chunk: string) => void | Promise<void>, perfSpan?: PerfSpan, metadata?: ChatRequestMetadata, onPlan?: (entries: PlanEntry[]) => void | Promise<void>): Promise<RouterResponse>;
42
- export declare function handlePrompt(context: SessionHandlerContext, chatKey: string, text: string, reply?: (text: string) => Promise<void>, replyContextToken?: string, accountId?: string, media?: PromptMediaInput, abortSignal?: AbortSignal, onToolEvent?: (event: ToolUseEvent) => void | Promise<void>, onThought?: (chunk: string) => void | Promise<void>, perfSpan?: PerfSpan, metadata?: ChatRequestMetadata, onPlan?: (entries: PlanEntry[]) => void | Promise<void>): Promise<RouterResponse>;
41
+ export declare function handlePromptWithSession(context: SessionHandlerContext, session: ResolvedSession, chatKey: string, text: string, reply?: (text: string) => Promise<void>, replyContextToken?: string, accountId?: string, media?: PromptMediaInput, abortSignal?: AbortSignal, onToolEvent?: (event: ToolUseEvent) => void | Promise<void>, onThought?: (chunk: string) => void | Promise<void>, perfSpan?: PerfSpan, metadata?: ChatRequestMetadata, onPlan?: (entries: PlanEntry[]) => void | Promise<void>, onUsage?: (usage: {
42
+ used: number;
43
+ size: number;
44
+ }) => void | Promise<void>): Promise<RouterResponse>;
45
+ export declare function handlePrompt(context: SessionHandlerContext, chatKey: string, text: string, reply?: (text: string) => Promise<void>, replyContextToken?: string, accountId?: string, media?: PromptMediaInput, abortSignal?: AbortSignal, onToolEvent?: (event: ToolUseEvent) => void | Promise<void>, onThought?: (chunk: string) => void | Promise<void>, perfSpan?: PerfSpan, metadata?: ChatRequestMetadata, onPlan?: (entries: PlanEntry[]) => void | Promise<void>, onUsage?: (usage: {
46
+ used: number;
47
+ size: number;
48
+ }) => void | Promise<void>): Promise<RouterResponse>;
@@ -113,7 +113,10 @@ export interface SessionInteractionOps {
113
113
  cancelled: boolean;
114
114
  message: string;
115
115
  }>;
116
- promptTransportSession: (session: import("../transport/types").ResolvedSession, text: string, reply?: (text: string) => Promise<void>, replyContext?: ReplyQuotaContext, media?: PromptMediaInput, abortSignal?: AbortSignal, onToolEvent?: (event: ToolUseEvent) => void | Promise<void>, onThought?: (chunk: string) => void | Promise<void>, perfSpan?: PerfSpan, onPlan?: (entries: PlanEntry[]) => void | Promise<void>) => Promise<{
116
+ promptTransportSession: (session: import("../transport/types").ResolvedSession, text: string, reply?: (text: string) => Promise<void>, replyContext?: ReplyQuotaContext, media?: PromptMediaInput, abortSignal?: AbortSignal, onToolEvent?: (event: ToolUseEvent) => void | Promise<void>, onThought?: (chunk: string) => void | Promise<void>, perfSpan?: PerfSpan, onPlan?: (entries: PlanEntry[]) => void | Promise<void>, onUsage?: (usage: {
117
+ used: number;
118
+ size: number;
119
+ }) => void | Promise<void>) => Promise<{
117
120
  text: string;
118
121
  }>;
119
122
  }
@@ -31,6 +31,12 @@ export type ControlEvent = {
31
31
  chatKey: string;
32
32
  sessionAlias: string;
33
33
  entries: PlanEntry[];
34
+ } | {
35
+ type: "turn-usage";
36
+ chatKey: string;
37
+ sessionAlias: string;
38
+ used: number;
39
+ size: number;
34
40
  } | {
35
41
  type: "turn-finished";
36
42
  chatKey: string;
@@ -31,6 +31,15 @@ export interface WorkspaceDiff {
31
31
  files: DiffFile[];
32
32
  diff: string;
33
33
  truncated: boolean;
34
+ /** Symbolic branch name (abbrev-ref HEAD); omitted when HEAD is detached. */
35
+ branch?: string;
36
+ /** True when HEAD is detached (no branch). */
37
+ detached?: boolean;
38
+ /** Working-tree context: its top-level root, and whether it's a linked (non-primary) worktree. */
39
+ worktree?: {
40
+ root: string;
41
+ linked: boolean;
42
+ };
34
43
  }
35
44
  export interface WorkspaceRef {
36
45
  name: string;
@@ -49,4 +58,7 @@ export declare class WorkspaceFs {
49
58
  * (so it stays contained), bounded by a scan budget and a result cap. */
50
59
  search(workspace: string, query: string): Promise<SearchResult>;
51
60
  gitDiff(workspace: string, relPath?: string): Promise<WorkspaceDiff>;
61
+ /** Branch + worktree context for a repo root. Best-effort: any git hiccup just
62
+ * omits the fields so the diff itself still returns. */
63
+ private gitContext;
52
64
  }
@@ -672,6 +672,9 @@ export interface ChannelCliMessages {
672
672
  channelAdded: (type: string) => string;
673
673
  cannotRemoveLastEnabled: string;
674
674
  channelRemoved: (id: string) => string;
675
+ channelCredentialsCleared: (id: string) => string;
676
+ channelCredentialsClearFailed: (id: string, error: string) => string;
677
+ channelCredentialsKept: (id: string) => string;
675
678
  cannotDisableLastEnabled: string;
676
679
  channelEnabledToggled: (id: string, enabled: boolean) => string;
677
680
  channelReplyModeSet: (id: string, mode: string) => string;
@@ -873,6 +873,9 @@ var init_channel_cli = __esm(() => {
873
873
  channelAdded: (type) => `Channel ${type} added`,
874
874
  cannotRemoveLastEnabled: "Cannot remove the last enabled channel.",
875
875
  channelRemoved: (id) => `Channel ${id} removed`,
876
+ channelCredentialsCleared: (id) => `Removed stored credentials for channel ${id}`,
877
+ channelCredentialsClearFailed: (id, error) => `Channel ${id} removed, but clearing its stored credentials failed: ${error}`,
878
+ channelCredentialsKept: (id) => `Kept stored credentials for channel ${id} (--keep-credentials)`,
876
879
  cannotDisableLastEnabled: "Cannot disable the last enabled channel.",
877
880
  channelEnabledToggled: (id, enabled) => `Channel ${id} ${enabled ? "enabled" : "disabled"}`,
878
881
  channelReplyModeSet: (id, mode) => `Channel ${id} default reply mode set to: ${mode}`,
@@ -1965,6 +1968,9 @@ var init_channel_cli2 = __esm(() => {
1965
1968
  channelAdded: (type) => `频道 ${type} 已添加`,
1966
1969
  cannotRemoveLastEnabled: "不能删除最后一个启用的频道。",
1967
1970
  channelRemoved: (id) => `频道 ${id} 已删除`,
1971
+ channelCredentialsCleared: (id) => `已移除频道 ${id} 的存储凭证`,
1972
+ channelCredentialsClearFailed: (id, error) => `频道 ${id} 已删除,但清除其存储凭证失败:${error}`,
1973
+ channelCredentialsKept: (id) => `已保留频道 ${id} 的存储凭证(--keep-credentials)`,
1968
1974
  cannotDisableLastEnabled: "不能禁用最后一个启用的频道。",
1969
1975
  channelEnabledToggled: (id, enabled) => `频道 ${id} 已${enabled ? "启用" : "禁用"}`,
1970
1976
  channelReplyModeSet: (id, mode) => `频道 ${id} 的默认 reply mode 已设置为:${mode}`,
@@ -114,6 +114,16 @@ export interface PromptOptions {
114
114
  * re-sent on every update (REPLACE, not append). Optional — text channels omit it.
115
115
  */
116
116
  onPlan?: (entries: PlanEntry[]) => void | Promise<void>;
117
+ /**
118
+ * Context-usage side-channel: the agent's ACP `usage_update` — `used` tokens
119
+ * currently in context and `size`, the model's total context window. Replace-latest
120
+ * scalar (re-sent during a turn). Optional — only agents that report it fire this
121
+ * (e.g. claude does, codex does not), and text channels omit the handler.
122
+ */
123
+ onUsage?: (usage: {
124
+ used: number;
125
+ size: number;
126
+ }) => void | Promise<void>;
117
127
  /**
118
128
  * How tool_call / tool_call_update events are surfaced for this prompt.
119
129
  *
@@ -50,6 +50,11 @@ export interface ChatRequest {
50
50
  onThought?: (chunk: string) => void | Promise<void>;
51
51
  /** Structured plan/todo side-channel; see PromptOptions.onPlan. */
52
52
  onPlan?: (entries: PlanEntry[]) => void | Promise<void>;
53
+ /** Context-usage side-channel; see PromptOptions.onUsage. */
54
+ onUsage?: (usage: {
55
+ used: number;
56
+ size: number;
57
+ }) => void | Promise<void>;
53
58
  /**
54
59
  * Optional per-turn performance tracing span. When `logging.perf.enabled` is
55
60
  * true, the channel handler attaches a `PerfSpan` so downstream layers can
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ganglion/xacpx",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",