sessix-server 0.4.3 → 0.4.5

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.
Files changed (3) hide show
  1. package/dist/index.js +533 -68
  2. package/dist/server.js +531 -66
  3. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -311,7 +311,7 @@ var import_uuid9 = require("uuid");
311
311
  var import_promises7 = require("fs/promises");
312
312
  var import_node_os9 = require("os");
313
313
  var import_node_path9 = require("path");
314
- var import_node_child_process11 = require("child_process");
314
+ var import_node_child_process12 = require("child_process");
315
315
  var import_node_util3 = require("util");
316
316
 
317
317
  // src/providers/ProcessProvider.ts
@@ -667,7 +667,24 @@ var ProcessProvider = class {
667
667
  writeUserMessage(proc, message, sessionId, images) {
668
668
  const content = [];
669
669
  if (images?.length) {
670
- for (const img of images) {
670
+ const ALLOWED_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
671
+ const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
672
+ for (let i = 0; i < images.length; i++) {
673
+ const img = images[i];
674
+ if (!ALLOWED_TYPES.has(img.media_type)) {
675
+ if (sessionId) {
676
+ this.emitWriteError(sessionId, `Image #${i + 1} rejected: unsupported media_type "${img.media_type}". Only JPEG/PNG/WebP/GIF are accepted.`);
677
+ }
678
+ return;
679
+ }
680
+ const sizeBytes = Math.floor(img.data.length * 0.75);
681
+ if (sizeBytes > MAX_IMAGE_BYTES) {
682
+ if (sessionId) {
683
+ const sizeMb = (sizeBytes / (1024 * 1024)).toFixed(1);
684
+ this.emitWriteError(sessionId, `Image #${i + 1} rejected: ${sizeMb}MB exceeds 5MB per-image limit.`);
685
+ }
686
+ return;
687
+ }
671
688
  content.push({
672
689
  type: "image",
673
690
  source: { type: "base64", media_type: img.media_type, data: img.data }
@@ -694,6 +711,14 @@ var ProcessProvider = class {
694
711
  this.emitWriteError(sessionId, `Failed to send message: ${err.message}`);
695
712
  }
696
713
  });
714
+ if (sessionId) {
715
+ const syntheticUser = {
716
+ type: "user",
717
+ session_id: sessionId,
718
+ message: { role: "user", content }
719
+ };
720
+ this.emitter.emit(this.getEventName(sessionId), syntheticUser);
721
+ }
697
722
  }
698
723
  /**
699
724
  * 发出写入失败的合成错误事件
@@ -3715,6 +3740,8 @@ var HookInstaller = class {
3715
3740
 
3716
3741
  // src/notification/NotificationService.ts
3717
3742
  var import_node_path5 = require("path");
3743
+ var RECENT_ACTIVITY_MAX = 6;
3744
+ var ACTIVITY_PUSH_THROTTLE_MS = 2500;
3718
3745
  var NotificationService = class {
3719
3746
  constructor(sessionManager, expoChannel = null) {
3720
3747
  this.sessionManager = sessionManager;
@@ -3733,6 +3760,14 @@ var NotificationService = class {
3733
3760
  latestAssistantText = /* @__PURE__ */ new Map();
3734
3761
  /** 获取全局待审批总数的回调(跨所有会话) */
3735
3762
  globalPendingCountProvider = null;
3763
+ /** sessionId → 最近活动状态(用于 LA content push) */
3764
+ recentActivityState = /* @__PURE__ */ new Map();
3765
+ /** sessionId → 节流定时器(LA content push) */
3766
+ activityPushTimers = /* @__PURE__ */ new Map();
3767
+ /** 上次推送 LA content 的时间戳(用于节流;首次立即推送) */
3768
+ lastActivityPushAt = /* @__PURE__ */ new Map();
3769
+ /** 提供器:根据 sessionId 取该会话的待审批列表(由 server.ts 注入) */
3770
+ pendingApprovalsProvider = null;
3736
3771
  /** 添加通知渠道(id 唯一,可用于后续动态开关) */
3737
3772
  addChannel(id, channel, enabled = true) {
3738
3773
  this.channelMap.set(id, { channel, enabled });
@@ -3760,11 +3795,24 @@ var NotificationService = class {
3760
3795
  }
3761
3796
  /** 注册 ActivityKit push token(由手机端启动 Live Activity 后上报) */
3762
3797
  addActivityPushToken(sessionId, token) {
3763
- this.activityPushChannel?.addToken(sessionId, token);
3798
+ if (!this.activityPushChannel) {
3799
+ console.warn(`[NotificationService] \u26A0\uFE0F \u6536\u5230 LA push token \u4F46 ActivityPushChannel \u672A\u521D\u59CB\u5316 (session=${sessionId.slice(0, 8)}\u2026) \u2014 \u68C0\u67E5 ~/.sessix/apns.json`);
3800
+ return;
3801
+ }
3802
+ this.activityPushChannel.addToken(sessionId, token);
3803
+ console.log(`[NotificationService] \u2705 LA push token \u5DF2\u6CE8\u518C (session=${sessionId.slice(0, 8)}\u2026, token=${token.slice(0, 16)}\u2026)`);
3804
+ this.scheduleActivityPush(sessionId, true);
3764
3805
  }
3765
3806
  /** 移除 ActivityKit push token */
3766
3807
  removeActivityPushToken(sessionId) {
3767
3808
  this.activityPushChannel?.removeToken(sessionId);
3809
+ this.clearActivityPushTimer(sessionId);
3810
+ this.recentActivityState.delete(sessionId);
3811
+ this.lastActivityPushAt.delete(sessionId);
3812
+ }
3813
+ /** 注入"会话 → 待审批列表"提供器(server.ts 启动时调用) */
3814
+ setPendingApprovalsProvider(fn) {
3815
+ this.pendingApprovalsProvider = fn;
3768
3816
  }
3769
3817
  /** 设置全局待审批总数提供者 */
3770
3818
  setGlobalPendingCountProvider(provider) {
@@ -3787,12 +3835,15 @@ var NotificationService = class {
3787
3835
  if (this.activityPushChannel?.hasToken(request.sessionId)) {
3788
3836
  const dangerLevel2 = this.getDangerLevel(request.toolName);
3789
3837
  const isYoloMode = this.getYoloMode(request.sessionId);
3838
+ const recentActivity = this.getRecentActivity(request.sessionId);
3839
+ const latestMessage = recentActivity[recentActivity.length - 1] ?? "";
3790
3840
  this.activityPushChannel.updateActivityWithAlert(
3791
3841
  request.sessionId,
3792
3842
  {
3793
3843
  status: "waitingApproval",
3794
3844
  sessionTitle,
3795
- latestMessage: "",
3845
+ latestMessage,
3846
+ recentActivity,
3796
3847
  approvalInfo: {
3797
3848
  requestId: request.id,
3798
3849
  toolName: request.toolName,
@@ -3805,6 +3856,7 @@ var NotificationService = class {
3805
3856
  },
3806
3857
  { title, body }
3807
3858
  );
3859
+ this.lastActivityPushAt.set(request.sessionId, Date.now());
3808
3860
  return;
3809
3861
  }
3810
3862
  const dangerLevel = this.getDangerLevel(request.toolName);
@@ -3838,17 +3890,20 @@ var NotificationService = class {
3838
3890
  const body = `\u2753 ${request.question.slice(0, 80)}`;
3839
3891
  if (this.activityPushChannel?.hasToken(request.sessionId)) {
3840
3892
  const isYoloMode = this.getYoloMode(request.sessionId);
3893
+ const recentActivity = this.getRecentActivity(request.sessionId);
3841
3894
  this.activityPushChannel.updateActivityWithAlert(
3842
3895
  request.sessionId,
3843
3896
  {
3844
3897
  status: "waitingApproval",
3845
3898
  sessionTitle,
3846
3899
  latestMessage: request.question.slice(0, 80),
3900
+ recentActivity,
3847
3901
  isYoloMode,
3848
3902
  updatedAt: Date.now()
3849
3903
  },
3850
3904
  { title: sessionTitle, body }
3851
3905
  );
3906
+ this.lastActivityPushAt.set(request.sessionId, Date.now());
3852
3907
  return;
3853
3908
  }
3854
3909
  this.notify({
@@ -3882,6 +3937,10 @@ var NotificationService = class {
3882
3937
  this.unsubscribe = null;
3883
3938
  this.yoloModeState.clear();
3884
3939
  this.latestAssistantText.clear();
3940
+ for (const timer of this.activityPushTimers.values()) clearTimeout(timer);
3941
+ this.activityPushTimers.clear();
3942
+ this.recentActivityState.clear();
3943
+ this.lastActivityPushAt.clear();
3885
3944
  }
3886
3945
  // ============================================
3887
3946
  // 内部方法
@@ -3890,15 +3949,20 @@ var NotificationService = class {
3890
3949
  switch (event.type) {
3891
3950
  case "claude_event": {
3892
3951
  this.trackAssistantText(event.sessionId, event.event);
3952
+ this.updateRecentActivity(event.sessionId, event.event);
3953
+ this.scheduleActivityPush(event.sessionId);
3893
3954
  break;
3894
3955
  }
3895
3956
  case "claude_events": {
3896
3957
  for (const e of event.events) {
3897
3958
  this.trackAssistantText(event.sessionId, e);
3959
+ this.updateRecentActivity(event.sessionId, e);
3898
3960
  }
3961
+ this.scheduleActivityPush(event.sessionId);
3899
3962
  break;
3900
3963
  }
3901
3964
  case "status_change": {
3965
+ this.clearActivityPushTimer(event.sessionId);
3902
3966
  if (event.status === "idle") {
3903
3967
  const sessionTitle = this.getSessionTitle(event.sessionId);
3904
3968
  const latestMsg = this.latestAssistantText.get(event.sessionId);
@@ -3909,9 +3973,12 @@ var NotificationService = class {
3909
3973
  status: "idle",
3910
3974
  sessionTitle,
3911
3975
  latestMessage: body,
3976
+ recentActivity: this.getRecentActivity(event.sessionId),
3912
3977
  isYoloMode,
3913
3978
  updatedAt: Date.now()
3914
3979
  });
3980
+ this.recentActivityState.delete(event.sessionId);
3981
+ this.lastActivityPushAt.delete(event.sessionId);
3915
3982
  } else {
3916
3983
  this.notify({
3917
3984
  title: sessionTitle,
@@ -3931,9 +3998,12 @@ var NotificationService = class {
3931
3998
  status: "error",
3932
3999
  sessionTitle,
3933
4000
  latestMessage: body,
4001
+ recentActivity: this.getRecentActivity(event.sessionId),
3934
4002
  isYoloMode,
3935
4003
  updatedAt: Date.now()
3936
4004
  });
4005
+ this.recentActivityState.delete(event.sessionId);
4006
+ this.lastActivityPushAt.delete(event.sessionId);
3937
4007
  } else {
3938
4008
  this.notify({
3939
4009
  title: sessionTitle,
@@ -3975,6 +4045,229 @@ var NotificationService = class {
3975
4045
  getYoloMode(sessionId) {
3976
4046
  return this.yoloModeState.get(sessionId) ?? false;
3977
4047
  }
4048
+ // ============================================
4049
+ // Live Activity 内容推送(后台 LA 实时刷新)
4050
+ // ============================================
4051
+ /**
4052
+ * 把一个 ClaudeStreamEvent 折算到 recentActivity 列表里。
4053
+ * 同一 message.id 内多次 assistant 事件视为流式更新,整段重建 currentEntries;
4054
+ * 切换 message.id 视为新 turn,旧条目沉淀到 history。
4055
+ */
4056
+ updateRecentActivity(sessionId, event) {
4057
+ if (event.type === "result") {
4058
+ const state2 = this.recentActivityState.get(sessionId);
4059
+ if (state2 && state2.currentEntries.length > 0) {
4060
+ state2.history.push(...state2.currentEntries);
4061
+ while (state2.history.length > RECENT_ACTIVITY_MAX) state2.history.shift();
4062
+ state2.currentEntries = [];
4063
+ state2.currentMessageId = null;
4064
+ }
4065
+ return;
4066
+ }
4067
+ if (event.type !== "assistant") return;
4068
+ const msg = event.message;
4069
+ if (!Array.isArray(msg.content)) return;
4070
+ let state = this.recentActivityState.get(sessionId);
4071
+ if (!state) {
4072
+ state = { history: [], currentMessageId: null, currentEntries: [] };
4073
+ this.recentActivityState.set(sessionId, state);
4074
+ }
4075
+ if (state.currentMessageId !== msg.id) {
4076
+ if (state.currentEntries.length > 0) {
4077
+ state.history.push(...state.currentEntries);
4078
+ while (state.history.length > RECENT_ACTIVITY_MAX) state.history.shift();
4079
+ }
4080
+ state.currentEntries = [];
4081
+ state.currentMessageId = msg.id;
4082
+ }
4083
+ const next = [];
4084
+ for (const block of msg.content) {
4085
+ if (block.type === "text") {
4086
+ const line = this.summarizeText(block.text);
4087
+ if (line.length >= 4) next.push(line);
4088
+ } else if (block.type === "tool_use") {
4089
+ const line = this.summarizeToolCall(block.name, block.input ?? {});
4090
+ if (line) next.push(line);
4091
+ }
4092
+ }
4093
+ state.currentEntries = next;
4094
+ }
4095
+ /** 取该会话当前的 recentActivity(history + currentEntries),保留末尾 N 条 */
4096
+ getRecentActivity(sessionId) {
4097
+ const state = this.recentActivityState.get(sessionId);
4098
+ if (!state) return [];
4099
+ const combined = [...state.history, ...state.currentEntries];
4100
+ return combined.slice(-RECENT_ACTIVITY_MAX);
4101
+ }
4102
+ /** 节流调度 LA content push;首次立即推,后续合并到 throttle window 末尾 */
4103
+ scheduleActivityPush(sessionId, force = false) {
4104
+ if (!this.activityPushChannel?.hasToken(sessionId)) return;
4105
+ const now = Date.now();
4106
+ const last = this.lastActivityPushAt.get(sessionId) ?? 0;
4107
+ const elapsed = now - last;
4108
+ if (force || elapsed >= ACTIVITY_PUSH_THROTTLE_MS) {
4109
+ this.clearActivityPushTimer(sessionId);
4110
+ this.flushActivityPush(sessionId);
4111
+ return;
4112
+ }
4113
+ if (this.activityPushTimers.has(sessionId)) return;
4114
+ const wait = ACTIVITY_PUSH_THROTTLE_MS - elapsed;
4115
+ this.activityPushTimers.set(
4116
+ sessionId,
4117
+ setTimeout(() => {
4118
+ this.activityPushTimers.delete(sessionId);
4119
+ this.flushActivityPush(sessionId);
4120
+ }, wait)
4121
+ );
4122
+ }
4123
+ clearActivityPushTimer(sessionId) {
4124
+ const timer = this.activityPushTimers.get(sessionId);
4125
+ if (timer) {
4126
+ clearTimeout(timer);
4127
+ this.activityPushTimers.delete(sessionId);
4128
+ }
4129
+ }
4130
+ /** 真正发送一次 LA content push(无 alert) */
4131
+ flushActivityPush(sessionId) {
4132
+ const channel = this.activityPushChannel;
4133
+ if (!channel?.hasToken(sessionId)) return;
4134
+ const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
4135
+ if (!session) return;
4136
+ const recentActivity = this.getRecentActivity(sessionId);
4137
+ const latestMessage = recentActivity[recentActivity.length - 1] ?? this.latestAssistantText.get(sessionId) ?? "";
4138
+ const sessionTitle = this.getSessionTitle(sessionId);
4139
+ const isYoloMode = this.getYoloMode(sessionId);
4140
+ const pendingApprovals = this.pendingApprovalsProvider?.(sessionId) ?? [];
4141
+ const latestApproval = pendingApprovals[pendingApprovals.length - 1];
4142
+ const status = latestApproval ? "waitingApproval" : this.mapSessionStatus(session.status);
4143
+ const contentState = {
4144
+ status,
4145
+ sessionTitle,
4146
+ latestMessage,
4147
+ recentActivity,
4148
+ isYoloMode,
4149
+ updatedAt: Date.now()
4150
+ };
4151
+ if (latestApproval) {
4152
+ contentState.approvalInfo = {
4153
+ requestId: latestApproval.id,
4154
+ toolName: latestApproval.toolName,
4155
+ description: String(latestApproval.description ?? "").slice(0, 80),
4156
+ dangerLevel: this.getDangerLevel(latestApproval.toolName),
4157
+ pendingCount: pendingApprovals.length
4158
+ };
4159
+ }
4160
+ if (session.stats) {
4161
+ contentState.stats = {
4162
+ totalInputTokens: session.stats.totalInputTokens,
4163
+ totalOutputTokens: session.stats.totalOutputTokens,
4164
+ totalCostUsd: session.stats.totalCostUsd
4165
+ };
4166
+ }
4167
+ this.lastActivityPushAt.set(sessionId, Date.now());
4168
+ const lineCount = recentActivity.length;
4169
+ channel.updateActivity(sessionId, contentState).then(() => {
4170
+ console.log(`[NotificationService] \u{1F4E1} LA push \u2713 session=${sessionId.slice(0, 8)}\u2026 status=${status} lines=${lineCount}`);
4171
+ }).catch((err) => {
4172
+ console.warn(`[NotificationService] \u{1F4E1} LA push \u2717 session=${sessionId.slice(0, 8)}\u2026:`, err instanceof Error ? err.message : err);
4173
+ });
4174
+ }
4175
+ /** SessionStatus → LiveActivity status 字符串映射(与客户端 mapStatus 一致) */
4176
+ mapSessionStatus(status) {
4177
+ switch (status) {
4178
+ case "running":
4179
+ return "running";
4180
+ case "waiting_approval":
4181
+ return "waitingApproval";
4182
+ case "waiting_question":
4183
+ return "waitingQuestion";
4184
+ case "idle":
4185
+ return "completed";
4186
+ case "completed":
4187
+ return "completed";
4188
+ case "error":
4189
+ return "error";
4190
+ default:
4191
+ return "idle";
4192
+ }
4193
+ }
4194
+ /** 文本块清洗:去多余空白 + 截断到 70 字符 */
4195
+ summarizeText(raw) {
4196
+ if (typeof raw !== "string") return "";
4197
+ const cleaned = raw.replace(/\s+/g, " ").trim();
4198
+ return cleaned.length > 70 ? cleaned.slice(0, 70) + "\u2026" : cleaned;
4199
+ }
4200
+ /** 工具调用摘要(与客户端 summarizeToolCall 行为对齐,简化版只输出中文) */
4201
+ summarizeToolCall(name, input) {
4202
+ const str = (v) => typeof v === "string" ? v : "";
4203
+ const baseName = (p) => {
4204
+ const cleaned = p.split(/[?#]/)[0];
4205
+ const parts = cleaned.split("/");
4206
+ return parts[parts.length - 1] || cleaned;
4207
+ };
4208
+ const trunc = (s, n) => s.length > n ? s.slice(0, n) + "\u2026" : s;
4209
+ switch (name) {
4210
+ case "Bash": {
4211
+ const cmd = str(input.command).split("\n")[0];
4212
+ return cmd ? `\u8FD0\u884C: ${trunc(cmd, 60)}` : "\u6267\u884C\u547D\u4EE4";
4213
+ }
4214
+ case "Edit": {
4215
+ const fp = baseName(str(input.file_path));
4216
+ return fp ? `\u7F16\u8F91 ${fp}` : "\u7F16\u8F91\u6587\u4EF6";
4217
+ }
4218
+ case "MultiEdit": {
4219
+ const fp = baseName(str(input.file_path));
4220
+ return fp ? `\u6279\u91CF\u7F16\u8F91 ${fp}` : "\u6279\u91CF\u7F16\u8F91\u6587\u4EF6";
4221
+ }
4222
+ case "Write": {
4223
+ const fp = baseName(str(input.file_path));
4224
+ return fp ? `\u5199\u5165 ${fp}` : "\u5199\u5165\u6587\u4EF6";
4225
+ }
4226
+ case "Read":
4227
+ case "NotebookEdit": {
4228
+ const fp = baseName(str(input.file_path) || str(input.notebook_path));
4229
+ return fp ? `\u9605\u8BFB ${fp}` : "\u9605\u8BFB\u6587\u4EF6";
4230
+ }
4231
+ case "Grep": {
4232
+ const p = str(input.pattern);
4233
+ return p ? `\u641C\u7D22: ${trunc(p, 50)}` : "\u641C\u7D22\u4EE3\u7801";
4234
+ }
4235
+ case "Glob": {
4236
+ const p = str(input.pattern);
4237
+ return p ? `\u67E5\u627E: ${trunc(p, 50)}` : "\u67E5\u627E\u6587\u4EF6";
4238
+ }
4239
+ case "WebFetch": {
4240
+ const url = str(input.url);
4241
+ let host = url;
4242
+ try {
4243
+ host = new URL(url).hostname;
4244
+ } catch {
4245
+ }
4246
+ return host ? `\u8BF7\u6C42 ${trunc(host, 50)}` : "\u8BF7\u6C42\u7F51\u9875";
4247
+ }
4248
+ case "WebSearch": {
4249
+ const q = str(input.query);
4250
+ return q ? `\u641C\u7D22\u7F51\u9875: ${trunc(q, 50)}` : "\u641C\u7D22\u7F51\u9875";
4251
+ }
4252
+ case "TodoWrite":
4253
+ return "\u66F4\u65B0\u4EFB\u52A1\u6E05\u5355";
4254
+ case "Task":
4255
+ case "Agent": {
4256
+ const desc = str(input.description) || str(input.subagent_type);
4257
+ return desc ? `\u6D3E\u53D1\u4EFB\u52A1: ${trunc(desc, 50)}` : "\u6D3E\u53D1\u5B50\u4EFB\u52A1";
4258
+ }
4259
+ case "ExitPlanMode":
4260
+ return "\u63D0\u4EA4\u8BA1\u5212";
4261
+ case "Skill": {
4262
+ const skill = str(input.skill);
4263
+ return skill ? `\u8C03\u7528\u6280\u80FD: ${trunc(skill, 40)}` : "\u8C03\u7528\u6280\u80FD";
4264
+ }
4265
+ default: {
4266
+ const summary = trunc(JSON.stringify(input), 50);
4267
+ return name ? `${name}: ${summary}` : summary;
4268
+ }
4269
+ }
4270
+ }
3978
4271
  };
3979
4272
 
3980
4273
  // src/notification/DesktopNotificationChannel.ts
@@ -4088,62 +4381,82 @@ var ExpoNotificationChannel = class {
4088
4381
  var http2 = __toESM(require("http2"));
4089
4382
  var fs2 = __toESM(require("fs"));
4090
4383
  var crypto = __toESM(require("crypto"));
4384
+ var APNS_HOSTS = {
4385
+ production: "api.push.apple.com",
4386
+ sandbox: "api.sandbox.push.apple.com"
4387
+ };
4091
4388
  var ActivityPushChannel = class {
4092
4389
  /** sessionId -> activityPushToken */
4093
4390
  tokens = /* @__PURE__ */ new Map();
4391
+ /**
4392
+ * 每个 token 已确认工作的 APNs 环境。
4393
+ * Debug build (aps-environment=development) 的 token 仅在 sandbox 端有效;
4394
+ * Release build (aps-environment=production) 的 token 仅在 production 端有效。
4395
+ * 同时维护两个连接 + 探测机制,避免环境配错时静默失败。
4396
+ */
4397
+ tokenEnv = /* @__PURE__ */ new Map();
4398
+ /** 首次探测顺序(来自配置 hint),未配置则先试 sandbox(开发场景占多数) */
4399
+ probeOrder;
4094
4400
  teamId;
4095
4401
  keyId;
4096
4402
  authKey;
4097
- apnsHost;
4098
4403
  /** 缓存的 JWT token + 过期时间 */
4099
4404
  cachedJwt = null;
4100
- /** 复用的 HTTP/2 长连接 */
4101
- http2Client = null;
4405
+ /** 每个环境一条 HTTP/2 长连接 */
4406
+ http2Clients = {};
4102
4407
  constructor(config) {
4103
4408
  this.teamId = config.teamId;
4104
4409
  this.keyId = config.keyId;
4105
4410
  this.authKey = fs2.readFileSync(config.authKeyPath, "utf-8");
4106
- this.apnsHost = config.sandbox ? "api.sandbox.push.apple.com" : "api.push.apple.com";
4107
- console.log(`[ActivityPushChannel] Initialized (${config.sandbox ? "sandbox" : "production"} mode)`);
4108
- }
4109
- /** 获取或新建 HTTP/2 长连接 */
4110
- getHttp2Client() {
4111
- if (this.http2Client && !this.http2Client.destroyed && !this.http2Client.closed) {
4112
- return this.http2Client;
4113
- }
4114
- this.http2Client = http2.connect(`https://${this.apnsHost}`);
4115
- this.http2Client.on("error", (err) => {
4116
- console.warn("[ActivityPushChannel] HTTP/2 connection error, will reconnect on next request:", err.message);
4117
- this.http2Client?.destroy();
4118
- this.http2Client = null;
4411
+ this.probeOrder = config.sandbox === false ? ["production", "sandbox"] : ["sandbox", "production"];
4412
+ console.log(`[ActivityPushChannel] Initialized (probe order: ${this.probeOrder.join(" \u2192 ")})`);
4413
+ }
4414
+ /** 获取或新建指定环境的 HTTP/2 长连接 */
4415
+ getHttp2Client(env) {
4416
+ const existing = this.http2Clients[env];
4417
+ if (existing && !existing.destroyed && !existing.closed) {
4418
+ return existing;
4419
+ }
4420
+ const client = http2.connect(`https://${APNS_HOSTS[env]}`);
4421
+ client.on("error", (err) => {
4422
+ console.warn(`[ActivityPushChannel] HTTP/2 (${env}) error, will reconnect on next request:`, err.message);
4423
+ client.destroy();
4424
+ if (this.http2Clients[env] === client) delete this.http2Clients[env];
4119
4425
  });
4120
- this.http2Client.on("close", () => {
4121
- this.http2Client = null;
4426
+ client.on("close", () => {
4427
+ if (this.http2Clients[env] === client) delete this.http2Clients[env];
4122
4428
  });
4123
- return this.http2Client;
4429
+ this.http2Clients[env] = client;
4430
+ return client;
4124
4431
  }
4125
4432
  /** 注册 Activity push token */
4126
4433
  addToken(sessionId, token) {
4434
+ const existed = this.tokens.has(sessionId);
4127
4435
  this.tokens.set(sessionId, token);
4128
- console.log(`[ActivityPushChannel] Token registered: session=${sessionId}`);
4436
+ console.log(`[ActivityPushChannel] Token ${existed ? "updated" : "registered"}: session=${sessionId.slice(0, 8)}\u2026 token=${token.slice(0, 16)}\u2026`);
4129
4437
  }
4130
4438
  /** 移除 Activity push token */
4131
4439
  removeToken(sessionId) {
4440
+ const tok = this.tokens.get(sessionId);
4132
4441
  this.tokens.delete(sessionId);
4442
+ if (tok) this.tokenEnv.delete(tok);
4133
4443
  }
4134
- /** 发送 content-state 更新到指定会话的 Live Activity */
4444
+ /** 发送 content-state 更新到指定会话的 Live Activity(纯内容刷新,不响通知) */
4135
4445
  async updateActivity(sessionId, contentState) {
4136
4446
  const token = this.tokens.get(sessionId);
4137
4447
  if (!token) return;
4448
+ const now = Math.floor(Date.now() / 1e3);
4138
4449
  const payload = {
4139
4450
  aps: {
4140
- timestamp: Math.floor(Date.now() / 1e3),
4451
+ timestamp: now,
4141
4452
  event: "update",
4142
- "content-state": contentState
4453
+ "content-state": contentState,
4454
+ // 2 分钟没新内容就让 LA 进入 stale 状态(系统自动灰化),避免显示陈旧数据
4455
+ "stale-date": now + 120
4143
4456
  }
4144
4457
  };
4145
4458
  try {
4146
- await this.sendToAPNs(token, payload);
4459
+ await this.sendToAPNs(token, payload, { priority: "5" });
4147
4460
  } catch (err) {
4148
4461
  console.warn(`[ActivityPushChannel] Update failed session=${sessionId}:`, err);
4149
4462
  }
@@ -4152,17 +4465,19 @@ var ActivityPushChannel = class {
4152
4465
  async updateActivityWithAlert(sessionId, contentState, alert) {
4153
4466
  const token = this.tokens.get(sessionId);
4154
4467
  if (!token) return;
4468
+ const now = Math.floor(Date.now() / 1e3);
4155
4469
  const payload = {
4156
4470
  aps: {
4157
- timestamp: Math.floor(Date.now() / 1e3),
4471
+ timestamp: now,
4158
4472
  event: "update",
4159
4473
  "content-state": contentState,
4474
+ "stale-date": now + 120,
4160
4475
  alert,
4161
4476
  sound: "default"
4162
4477
  }
4163
4478
  };
4164
4479
  try {
4165
- await this.sendToAPNs(token, payload);
4480
+ await this.sendToAPNs(token, payload, { priority: "10" });
4166
4481
  } catch (err) {
4167
4482
  console.warn(`[ActivityPushChannel] Alert update failed session=${sessionId}:`, err);
4168
4483
  }
@@ -4171,15 +4486,16 @@ var ActivityPushChannel = class {
4171
4486
  async endActivity(sessionId, contentState) {
4172
4487
  const token = this.tokens.get(sessionId);
4173
4488
  if (!token) return;
4489
+ const now = Math.floor(Date.now() / 1e3);
4174
4490
  const payload = {
4175
4491
  aps: {
4176
- timestamp: Math.floor(Date.now() / 1e3),
4492
+ timestamp: now,
4177
4493
  event: "end",
4178
4494
  "content-state": contentState
4179
4495
  }
4180
4496
  };
4181
4497
  try {
4182
- await this.sendToAPNs(token, payload);
4498
+ await this.sendToAPNs(token, payload, { priority: "10" });
4183
4499
  } catch (err) {
4184
4500
  console.warn(`[ActivityPushChannel] End failed session=${sessionId}:`, err);
4185
4501
  }
@@ -4189,15 +4505,44 @@ var ActivityPushChannel = class {
4189
4505
  hasToken(sessionId) {
4190
4506
  return this.tokens.has(sessionId);
4191
4507
  }
4192
- /** 发送 APNs HTTP/2 请求 */
4193
- async sendToAPNs(deviceToken, payload) {
4508
+ /**
4509
+ * 发送 APNs,自动处理环境探测。
4510
+ * 对每个 token:先用已确认的环境(如有);否则按 probeOrder 顺序探测,
4511
+ * 收到 BadDeviceToken 自动切到另一个环境,并把成功的环境绑定到该 token。
4512
+ */
4513
+ async sendToAPNs(deviceToken, payload, opts = {}) {
4514
+ const known = this.tokenEnv.get(deviceToken);
4515
+ if (known) {
4516
+ return this.sendToAPNsOnce(deviceToken, payload, opts, known);
4517
+ }
4518
+ let lastErr = null;
4519
+ for (const env of this.probeOrder) {
4520
+ try {
4521
+ await this.sendToAPNsOnce(deviceToken, payload, opts, env);
4522
+ this.tokenEnv.set(deviceToken, env);
4523
+ if (env !== this.probeOrder[0]) {
4524
+ console.log(`[ActivityPushChannel] Token bound to ${env} after probe (token=${deviceToken.slice(0, 16)}\u2026)`);
4525
+ }
4526
+ return;
4527
+ } catch (err) {
4528
+ lastErr = err;
4529
+ if (!isBadDeviceTokenError(err)) {
4530
+ throw err;
4531
+ }
4532
+ }
4533
+ }
4534
+ throw lastErr ?? new Error("APNs send failed: all environments rejected token");
4535
+ }
4536
+ /** 单次 APNs HTTP/2 请求(内部 helper,不做探测) */
4537
+ async sendToAPNsOnce(deviceToken, payload, opts, env) {
4194
4538
  const topic = "com.kachun.sessix.push-type.liveactivity";
4195
4539
  const jwt = this.getJWT();
4196
4540
  const payloadStr = JSON.stringify(payload);
4541
+ const priority = opts.priority ?? "10";
4197
4542
  return new Promise((resolve, reject) => {
4198
4543
  let client;
4199
4544
  try {
4200
- client = this.getHttp2Client();
4545
+ client = this.getHttp2Client(env);
4201
4546
  } catch (err) {
4202
4547
  return reject(err);
4203
4548
  }
@@ -4207,7 +4552,7 @@ var ActivityPushChannel = class {
4207
4552
  "authorization": `bearer ${jwt}`,
4208
4553
  "apns-topic": topic,
4209
4554
  "apns-push-type": "liveactivity",
4210
- "apns-priority": "10",
4555
+ "apns-priority": priority,
4211
4556
  "apns-expiration": String(Math.floor(Date.now() / 1e3) + 30),
4212
4557
  "content-type": "application/json",
4213
4558
  "content-length": Buffer.byteLength(payloadStr)
@@ -4225,10 +4570,11 @@ var ActivityPushChannel = class {
4225
4570
  resolve();
4226
4571
  } else {
4227
4572
  if (statusCode === 0) {
4228
- this.http2Client?.destroy();
4229
- this.http2Client = null;
4573
+ const c = this.http2Clients[env];
4574
+ c?.destroy();
4575
+ delete this.http2Clients[env];
4230
4576
  }
4231
- reject(new Error(`APNs returned ${statusCode}: ${responseData}`));
4577
+ reject(new ApnsError(statusCode, responseData));
4232
4578
  }
4233
4579
  });
4234
4580
  req.on("error", (err) => {
@@ -4261,6 +4607,24 @@ var ActivityPushChannel = class {
4261
4607
  return token;
4262
4608
  }
4263
4609
  };
4610
+ var ApnsError = class extends Error {
4611
+ constructor(statusCode, responseBody) {
4612
+ super(`APNs returned ${statusCode}: ${responseBody}`);
4613
+ this.statusCode = statusCode;
4614
+ this.responseBody = responseBody;
4615
+ this.name = "ApnsError";
4616
+ }
4617
+ };
4618
+ function isBadDeviceTokenError(err) {
4619
+ if (!(err instanceof ApnsError)) return false;
4620
+ if (err.statusCode !== 400 && err.statusCode !== 410) return false;
4621
+ try {
4622
+ const parsed = JSON.parse(err.responseBody);
4623
+ return parsed.reason === "BadDeviceToken" || parsed.reason === "Unregistered";
4624
+ } catch {
4625
+ return false;
4626
+ }
4627
+ }
4264
4628
 
4265
4629
  // src/session/ProjectReader.ts
4266
4630
  var import_promises3 = require("fs/promises");
@@ -4660,6 +5024,45 @@ var PairingManager = class {
4660
5024
  }
4661
5025
  };
4662
5026
 
5027
+ // src/utils/shellPath.ts
5028
+ var import_node_child_process7 = require("child_process");
5029
+ var fixed = false;
5030
+ function fixShellPath() {
5031
+ if (fixed || isWindows) {
5032
+ fixed = true;
5033
+ return;
5034
+ }
5035
+ fixed = true;
5036
+ const shell = process.env.SHELL || "/bin/zsh";
5037
+ const isFish = /\/fish$/.test(shell);
5038
+ const printPathCmd = isFish ? "string join : $PATH" : 'printf "%s" "$PATH"';
5039
+ let raw;
5040
+ try {
5041
+ raw = (0, import_node_child_process7.execFileSync)(shell, ["-l", "-c", printPathCmd], {
5042
+ encoding: "utf8",
5043
+ timeout: 3e3,
5044
+ stdio: ["ignore", "pipe", "ignore"]
5045
+ });
5046
+ } catch (err) {
5047
+ console.warn("[fixShellPath] failed to read login shell PATH:", err);
5048
+ return;
5049
+ }
5050
+ const fromShell = raw.trim();
5051
+ if (!fromShell) return;
5052
+ process.env.PATH = mergePath(fromShell, process.env.PATH || "");
5053
+ }
5054
+ function mergePath(primary, secondary) {
5055
+ const seen = /* @__PURE__ */ new Set();
5056
+ const out = [];
5057
+ for (const seg of primary.split(":").concat(secondary.split(":"))) {
5058
+ if (!seg) continue;
5059
+ if (seen.has(seg)) continue;
5060
+ seen.add(seg);
5061
+ out.push(seg);
5062
+ }
5063
+ return out.join(":");
5064
+ }
5065
+
4663
5066
  // src/auth/AuthManager.ts
4664
5067
  var import_child_process3 = require("child_process");
4665
5068
  var import_child_process4 = require("child_process");
@@ -4782,9 +5185,9 @@ var AuthManager = class extends import_events3.EventEmitter {
4782
5185
  var import_promises8 = require("fs/promises");
4783
5186
 
4784
5187
  // src/terminal/TerminalExecutor.ts
4785
- var import_node_child_process7 = require("child_process");
5188
+ var import_node_child_process8 = require("child_process");
4786
5189
  var import_uuid5 = require("uuid");
4787
- var EXEC_TIMEOUT_MS = 5 * 60 * 1e3;
5190
+ var EXEC_TIMEOUT_MS = 30 * 60 * 1e3;
4788
5191
  var TerminalExecutor = class {
4789
5192
  processes = /* @__PURE__ */ new Map();
4790
5193
  eventCallbacks = [];
@@ -4806,9 +5209,9 @@ var TerminalExecutor = class {
4806
5209
  }
4807
5210
  exec(sessionId, command, cwd) {
4808
5211
  const execId = (0, import_uuid5.v4)();
4809
- const shell = isWindows ? "powershell" : "bash";
4810
- const args = isWindows ? ["-Command", command] : ["-c", command];
4811
- const proc = (0, import_node_child_process7.spawn)(shell, args, {
5212
+ const shell = isWindows ? "powershell" : process.env.SHELL || "/bin/zsh";
5213
+ const args = isWindows ? ["-Command", command] : ["-l", "-c", command];
5214
+ const proc = (0, import_node_child_process8.spawn)(shell, args, {
4812
5215
  cwd,
4813
5216
  stdio: ["ignore", "pipe", "pipe"],
4814
5217
  env: { ...process.env }
@@ -4845,6 +5248,14 @@ var TerminalExecutor = class {
4845
5248
  });
4846
5249
  const timer = setTimeout(() => {
4847
5250
  if (this.processes.has(execId)) {
5251
+ this.emit({
5252
+ type: "terminal_output",
5253
+ sessionId,
5254
+ execId,
5255
+ stream: "stderr",
5256
+ data: `[killed: timeout ${Math.round(EXEC_TIMEOUT_MS / 6e4)}m]
5257
+ `
5258
+ });
4848
5259
  killProcessCrossPlatform(proc);
4849
5260
  }
4850
5261
  }, EXEC_TIMEOUT_MS);
@@ -4869,13 +5280,13 @@ var TerminalExecutor = class {
4869
5280
  };
4870
5281
 
4871
5282
  // src/xcode/XcodeBuildExecutor.ts
4872
- var import_node_child_process8 = require("child_process");
5283
+ var import_node_child_process9 = require("child_process");
4873
5284
  var import_node_util = require("util");
4874
5285
  var import_promises4 = require("fs/promises");
4875
5286
  var import_node_path6 = require("path");
4876
5287
  var import_node_os7 = require("os");
4877
5288
  var import_uuid6 = require("uuid");
4878
- var execAsync = (0, import_node_util.promisify)(import_node_child_process8.exec);
5289
+ var execAsync = (0, import_node_util.promisify)(import_node_child_process9.exec);
4879
5290
  var BUILD_TIMEOUT_MS = 30 * 60 * 1e3;
4880
5291
  var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
4881
5292
  var CONFIG_FILE = (0, import_node_path6.join)((0, import_node_os7.homedir)(), ".sessix", "xcode-config.json");
@@ -5064,7 +5475,7 @@ ${e.stderr ?? ""}`);
5064
5475
  if (override) await this.saveConfig(projectPath, override);
5065
5476
  const buildId = (0, import_uuid6.v4)();
5066
5477
  const args = buildArgs(config);
5067
- const proc = (0, import_node_child_process8.spawn)("xcodebuild", args, {
5478
+ const proc = (0, import_node_child_process9.spawn)("xcodebuild", args, {
5068
5479
  cwd: projectPath,
5069
5480
  stdio: ["ignore", "pipe", "pipe"],
5070
5481
  env: { ...process.env, NSUnbufferedIO: "YES" }
@@ -5160,7 +5571,7 @@ ${e.stderr ?? ""}`);
5160
5571
 
5161
5572
  `
5162
5573
  });
5163
- const proc = (0, import_node_child_process8.spawn)(installCmd[0], installCmd.slice(1), {
5574
+ const proc = (0, import_node_child_process9.spawn)(installCmd[0], installCmd.slice(1), {
5164
5575
  cwd: projectPath,
5165
5576
  stdio: ["ignore", "pipe", "pipe"]
5166
5577
  });
@@ -5567,15 +5978,16 @@ var CommandDiscovery = class {
5567
5978
  const cmd = sanitizeBashLine(rawLine);
5568
5979
  if (!cmd) continue;
5569
5980
  const { command: cleanCmd, inlineComment } = splitInlineComment(cmd);
5570
- const title = synthesizeTitle(cleanCmd);
5981
+ const { cwd: cdCwd, command: finalCmd } = splitCdPrefix(cleanCmd);
5982
+ const title = synthesizeTitle(finalCmd);
5571
5983
  out.push(makeCommand({
5572
5984
  title,
5573
- command: cleanCmd,
5574
- cwd: "",
5985
+ command: finalCmd,
5986
+ cwd: cdCwd,
5575
5987
  source,
5576
5988
  sourceFile: fileName,
5577
5989
  description: inlineComment ?? blockHeading,
5578
- category: classifyByCommand(cleanCmd)
5990
+ category: classifyByCommand(finalCmd)
5579
5991
  }));
5580
5992
  }
5581
5993
  }
@@ -5620,6 +6032,19 @@ function synthesizeTitle(cmd) {
5620
6032
  const head = tokens.slice(0, 3).join(" ");
5621
6033
  return head.length > 60 ? head.slice(0, 60) + "\u2026" : head;
5622
6034
  }
6035
+ function splitCdPrefix(cmd) {
6036
+ const m = /^cd\s+(\S+)\s*&&\s*(.+)$/.exec(cmd);
6037
+ if (!m) return { cwd: "", command: cmd };
6038
+ const path2 = m[1];
6039
+ if (!path2) return { cwd: "", command: cmd };
6040
+ if (path2.startsWith("/") || path2.startsWith("~") || path2.startsWith("-")) {
6041
+ return { cwd: "", command: cmd };
6042
+ }
6043
+ if (path2.split("/").some((seg) => seg === "..")) {
6044
+ return { cwd: "", command: cmd };
6045
+ }
6046
+ return { cwd: path2, command: m[2].trim() };
6047
+ }
5623
6048
  function splitInlineComment(line) {
5624
6049
  let inSingle = false;
5625
6050
  let inDouble = false;
@@ -5681,10 +6106,10 @@ function sourceWeight(s) {
5681
6106
  }
5682
6107
 
5683
6108
  // src/git/GitExecutor.ts
5684
- var import_node_child_process9 = require("child_process");
6109
+ var import_node_child_process10 = require("child_process");
5685
6110
  var import_node_util2 = require("util");
5686
6111
  var import_uuid7 = require("uuid");
5687
- var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process9.exec);
6112
+ var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process10.exec);
5688
6113
  var STATUS_TIMEOUT_MS = 15e3;
5689
6114
  var COMMIT_TIMEOUT_MS = 6e4;
5690
6115
  var PUSH_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -5857,7 +6282,7 @@ var GitExecutor = class {
5857
6282
  });
5858
6283
  let proc;
5859
6284
  try {
5860
- proc = (0, import_node_child_process9.spawn)(cmd[0], cmd.slice(1), {
6285
+ proc = (0, import_node_child_process10.spawn)(cmd[0], cmd.slice(1), {
5861
6286
  cwd: projectPath,
5862
6287
  stdio: ["ignore", "pipe", "pipe"],
5863
6288
  env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
@@ -6037,9 +6462,15 @@ function isValidTask(value) {
6037
6462
  }
6038
6463
 
6039
6464
  // src/utils/cliCapabilities.ts
6040
- var import_node_child_process10 = require("child_process");
6465
+ var import_node_child_process11 = require("child_process");
6466
+ var DEFAULT_MODELS = [
6467
+ { value: "opus", label: "Opus 4.7", sublabel: "Most capable for ambitious work" },
6468
+ { value: "sonnet", label: "Sonnet 4.6", sublabel: "Most efficient for everyday tasks" },
6469
+ { value: "haiku", label: "Haiku 4.5", sublabel: "Fastest for quick answers" }
6470
+ ];
6041
6471
  var DEFAULT_CAPABILITIES = {
6042
- effortLevels: ["low", "medium", "high", "xhigh", "max"]
6472
+ effortLevels: ["low", "medium", "high", "xhigh", "max"],
6473
+ models: DEFAULT_MODELS
6043
6474
  };
6044
6475
  async function parseCliCapabilities() {
6045
6476
  const claudePath = findClaudePath();
@@ -6065,7 +6496,7 @@ async function parseCliCapabilities() {
6065
6496
  }
6066
6497
  function runCli(path2, args) {
6067
6498
  return new Promise((resolve) => {
6068
- (0, import_node_child_process10.execFile)(path2, args, { timeout: 5e3 }, (err, stdout) => {
6499
+ (0, import_node_child_process11.execFile)(path2, args, { timeout: 5e3 }, (err, stdout) => {
6069
6500
  if (err) {
6070
6501
  console.warn(`[CliCapabilities] Failed to run ${path2} ${args.join(" ")}:`, err.message);
6071
6502
  resolve(null);
@@ -6079,7 +6510,7 @@ function runCli(path2, args) {
6079
6510
  // src/server.ts
6080
6511
  var WS_PORT = 3745;
6081
6512
  var HTTP_PORT = 3746;
6082
- var execAsync3 = (0, import_node_util3.promisify)(import_node_child_process11.exec);
6513
+ var execAsync3 = (0, import_node_util3.promisify)(import_node_child_process12.exec);
6083
6514
  async function killPortProcess(port) {
6084
6515
  try {
6085
6516
  if (isWindows) {
@@ -6107,6 +6538,39 @@ async function killPortProcess(port) {
6107
6538
  } catch {
6108
6539
  }
6109
6540
  }
6541
+ async function loadApnsConfigFromFile() {
6542
+ const path2 = (0, import_node_path9.join)((0, import_node_os9.homedir)(), ".sessix", "apns.json");
6543
+ try {
6544
+ const raw = await (0, import_promises7.readFile)(path2, "utf8");
6545
+ const cfg = JSON.parse(raw);
6546
+ if (typeof cfg.teamId !== "string" || typeof cfg.keyId !== "string" || typeof cfg.authKeyPath !== "string") {
6547
+ console.warn(`[Server] \u26A0\uFE0F ${path2} \u7F3A\u5C11\u5FC5\u9700\u5B57\u6BB5 (teamId / keyId / authKeyPath)\uFF0CLA \u540E\u53F0\u63A8\u9001\u5DF2\u7981\u7528`);
6548
+ return null;
6549
+ }
6550
+ try {
6551
+ await (0, import_promises7.readFile)(cfg.authKeyPath, "utf8");
6552
+ } catch (err) {
6553
+ console.warn(`[Server] \u26A0\uFE0F \u65E0\u6CD5\u8BFB\u53D6 APNs Auth Key: ${cfg.authKeyPath}`, err);
6554
+ return null;
6555
+ }
6556
+ console.log(`[Server] \u2705 \u5DF2\u52A0\u8F7D APNs \u914D\u7F6E (${path2})`);
6557
+ console.log(`[Server] teamId=${cfg.teamId} keyId=${cfg.keyId} sandbox=${cfg.sandbox === true}`);
6558
+ return {
6559
+ teamId: cfg.teamId,
6560
+ keyId: cfg.keyId,
6561
+ authKeyPath: cfg.authKeyPath,
6562
+ sandbox: cfg.sandbox === true
6563
+ };
6564
+ } catch (err) {
6565
+ const code = err.code;
6566
+ if (code === "ENOENT") {
6567
+ console.log(`[Server] \u2139\uFE0F ${path2} \u4E0D\u5B58\u5728\uFF0CLA \u540E\u53F0\u63A8\u9001\u672A\u542F\u7528\uFF08\u524D\u53F0 App \u4ECD\u80FD\u7528\u672C\u5730 Activity.update\uFF09`);
6568
+ } else {
6569
+ console.warn(`[Server] \u26A0\uFE0F \u8BFB\u53D6 ${path2} \u5931\u8D25:`, err);
6570
+ }
6571
+ return null;
6572
+ }
6573
+ }
6110
6574
  async function createWithRetry(label, port, factory) {
6111
6575
  try {
6112
6576
  return await factory();
@@ -6121,6 +6585,7 @@ async function createWithRetry(label, port, factory) {
6121
6585
  }
6122
6586
  }
6123
6587
  async function start(opts = {}) {
6588
+ fixShellPath();
6124
6589
  const configDir = (0, import_node_path9.join)((0, import_node_os9.homedir)(), ".sessix");
6125
6590
  const tokenFile = (0, import_node_path9.join)(configDir, "token");
6126
6591
  let token;
@@ -6169,9 +6634,10 @@ async function start(opts = {}) {
6169
6634
  const notificationService = new NotificationService(sessionManager, expoChannel);
6170
6635
  notificationService.addChannel("expo", expoChannel, opts.enableExpoPush !== false);
6171
6636
  notificationService.addChannel("mac", new DesktopNotificationChannel(), opts.enableMacNotification !== false);
6172
- if (opts.activityPush) {
6637
+ const activityPushOpts = opts.activityPush ?? await loadApnsConfigFromFile();
6638
+ if (activityPushOpts) {
6173
6639
  try {
6174
- const activityChannel = new ActivityPushChannel(opts.activityPush);
6640
+ const activityChannel = new ActivityPushChannel(activityPushOpts);
6175
6641
  notificationService.setActivityPushChannel(activityChannel);
6176
6642
  console.log(`[Server] ${t("server.activityPushEnabled")}`);
6177
6643
  } catch (err) {
@@ -6206,6 +6672,9 @@ async function start(opts = {}) {
6206
6672
  notificationService.setGlobalPendingCountProvider(
6207
6673
  () => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
6208
6674
  );
6675
+ notificationService.setPendingApprovalsProvider(
6676
+ (sessionId) => approvalProxy.getPendingRequestsForSession(sessionId)
6677
+ );
6209
6678
  let cliCapabilities = null;
6210
6679
  parseCliCapabilities().then((caps) => {
6211
6680
  cliCapabilities = caps;
@@ -6276,7 +6745,7 @@ async function start(opts = {}) {
6276
6745
  wsBridge.send(ws, { type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
6277
6746
  }
6278
6747
  if (cliCapabilities) {
6279
- wsBridge.send(ws, { type: "cli_info", effortLevels: cliCapabilities.effortLevels, version: cliCapabilities.version });
6748
+ wsBridge.send(ws, { type: "cli_info", effortLevels: cliCapabilities.effortLevels, version: cliCapabilities.version, models: cliCapabilities.models });
6280
6749
  }
6281
6750
  if (scheduledManager) {
6282
6751
  wsBridge.send(ws, { type: "scheduled_session_list", tasks: scheduledManager.list() });
@@ -6755,14 +7224,12 @@ async function start(opts = {}) {
6755
7224
  setTimeout(() => {
6756
7225
  if (!approvalProxy.isPending(request.id)) return;
6757
7226
  if (wsBridge.isViewingSession(request.sessionId)) return;
6758
- if (wsBridge.getConnectionCount() > 0) return;
6759
7227
  const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
6760
7228
  notificationService.notifyApproval(request, pendingCount);
6761
7229
  }, 5e3);
6762
7230
  setTimeout(() => {
6763
7231
  if (!approvalProxy.isPending(request.id)) return;
6764
7232
  if (wsBridge.isViewingSession(request.sessionId)) return;
6765
- if (wsBridge.getConnectionCount() > 0) return;
6766
7233
  console.log(`[Server] ${t("server.approvalRetry", { id: request.id })}`);
6767
7234
  const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
6768
7235
  notificationService.notifyApproval(request, pendingCount);
@@ -6774,13 +7241,11 @@ async function start(opts = {}) {
6774
7241
  setTimeout(() => {
6775
7242
  if (!sessionManager.isQuestionPending(request.id)) return;
6776
7243
  if (wsBridge.isViewingSession(request.sessionId)) return;
6777
- if (wsBridge.getConnectionCount() > 0) return;
6778
7244
  notificationService.notifyQuestion(request);
6779
7245
  }, 5e3);
6780
7246
  setTimeout(() => {
6781
7247
  if (!sessionManager.isQuestionPending(request.id)) return;
6782
7248
  if (wsBridge.isViewingSession(request.sessionId)) return;
6783
- if (wsBridge.getConnectionCount() > 0) return;
6784
7249
  console.log(`[Server] Question ${request.id} not answered in 60s, retrying push`);
6785
7250
  notificationService.notifyQuestion(request);
6786
7251
  }, 6e4);