@agentconnect/host 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/host.js CHANGED
@@ -5,11 +5,11 @@ import fs from 'fs';
5
5
  import { promises as fsp } from 'fs';
6
6
  import net from 'net';
7
7
  import path from 'path';
8
- import { listModels, listRecentModels, providers, resolveProviderForModel } from './providers/index.js';
8
+ import { listModels, listRecentModels, providers, resolveProviderForModel, } from './providers/index.js';
9
9
  import { debugLog, setSpawnLogging } from './providers/utils.js';
10
10
  import { createObservedTracker } from './observed.js';
11
11
  import { createStorage } from './storage.js';
12
- import { buildSummaryPrompt, getSummaryModel, pollClaudeSummary, runSummaryPrompt, } from './summary.js';
12
+ import { buildSummaryPrompt, buildSummaryPromptWithOverride, getSummaryModel, runSummaryPrompt, } from './summary.js';
13
13
  function send(socket, payload) {
14
14
  socket.send(JSON.stringify(payload));
15
15
  }
@@ -33,8 +33,7 @@ function buildProviderList(statuses) {
33
33
  });
34
34
  }
35
35
  function resolveLoginExperience(mode) {
36
- const raw = process.env.AGENTCONNECT_LOGIN_EXPERIENCE ||
37
- process.env.AGENTCONNECT_CLAUDE_LOGIN_EXPERIENCE;
36
+ const raw = process.env.AGENTCONNECT_LOGIN_EXPERIENCE || process.env.AGENTCONNECT_CLAUDE_LOGIN_EXPERIENCE;
38
37
  if (raw) {
39
38
  const normalized = raw.trim().toLowerCase();
40
39
  if (normalized === 'terminal' || normalized === 'manual')
@@ -127,6 +126,29 @@ function createHostRuntime(options) {
127
126
  return null;
128
127
  }
129
128
  }
129
+ function normalizeSummaryMode(value) {
130
+ const raw = typeof value === 'string' ? value.trim().toLowerCase() : '';
131
+ if (raw === 'auto' || raw === 'off' || raw === 'force')
132
+ return raw;
133
+ return undefined;
134
+ }
135
+ function normalizeSummaryPrompt(value) {
136
+ if (typeof value !== 'string')
137
+ return undefined;
138
+ const trimmed = value.trim();
139
+ return trimmed ? trimmed : undefined;
140
+ }
141
+ function resolveSummaryMode(session, requested) {
142
+ if (requested)
143
+ return requested;
144
+ return session.summaryMode ?? 'auto';
145
+ }
146
+ function resolveEffectiveSummaryMode(session, requested) {
147
+ const mode = resolveSummaryMode(session, requested);
148
+ if (mode === 'auto' && session.summaryAutoUsed)
149
+ return 'off';
150
+ return mode;
151
+ }
130
152
  function recordCapability(capability) {
131
153
  observedTracker.record(capability);
132
154
  }
@@ -139,6 +161,17 @@ function createHostRuntime(options) {
139
161
  function recordProviderCapability(providerId) {
140
162
  recordCapability(`model.${providerId}`);
141
163
  }
164
+ function resolveSystemPrompt(providerId, input) {
165
+ if (typeof input === 'string') {
166
+ const trimmed = input.trim();
167
+ return trimmed ? trimmed : undefined;
168
+ }
169
+ const envKey = `AGENTCONNECT_SYSTEM_PROMPT_${providerId.toUpperCase()}`;
170
+ const envValue = process.env[envKey];
171
+ if (envValue && envValue.trim())
172
+ return envValue.trim();
173
+ return undefined;
174
+ }
142
175
  const SUMMARY_REASONING_MAX_LINES = 3;
143
176
  const SUMMARY_REASONING_MAX_CHARS = 280;
144
177
  function appendSummaryReasoning(session, text) {
@@ -173,62 +206,57 @@ function createHostRuntime(options) {
173
206
  function clearSummarySeed(session) {
174
207
  session.summarySeed = undefined;
175
208
  session.summaryReasoning = undefined;
209
+ session.summaryNextMode = undefined;
210
+ session.summaryNextPrompt = undefined;
176
211
  }
177
212
  async function startPromptSummary(options) {
178
- const { sessionId, session, message, reasoning, emit } = options;
179
- if (session.providerId === 'claude')
180
- return;
213
+ const { sessionId, session, message, reasoning, summaryPrompt, mode, emit } = options;
181
214
  if (session.summaryRequested)
182
215
  return;
183
216
  session.summaryRequested = true;
184
- const provider = providers[session.providerId];
185
- if (!provider)
186
- return;
187
- const prompt = buildSummaryPrompt(message, reasoning);
188
- const summaryModel = getSummaryModel(session.providerId);
189
- const cwd = session.cwd || basePath;
190
- const repoRoot = session.repoRoot || basePath;
191
- const result = await runSummaryPrompt({
192
- provider,
193
- prompt,
194
- model: summaryModel,
195
- cwd,
196
- repoRoot,
197
- });
198
- if (!result)
199
- return;
200
- persistSummary(emit, sessionId, {
201
- summary: result.summary,
202
- source: 'prompt',
203
- provider: session.providerId,
204
- model: result.model ?? null,
205
- createdAt: new Date().toISOString(),
206
- }, session);
207
- }
208
- async function startClaudeLogSummary(options) {
209
- const { sessionId, session, emit } = options;
210
- if (session.providerId !== 'claude')
211
- return;
212
- if (session.claudeSummaryWatch)
213
- return;
214
- const providerSessionId = session.providerSessionId;
215
- if (!providerSessionId)
216
- return;
217
- session.claudeSummaryWatch = true;
218
- const projectRoot = session.repoRoot || session.cwd || basePath;
219
- const summary = await pollClaudeSummary({
220
- basePath: projectRoot,
221
- sessionId: providerSessionId,
222
- });
223
- if (!summary)
224
- return;
225
- persistSummary(emit, sessionId, {
226
- summary,
227
- source: 'claude-log',
228
- provider: 'claude',
229
- model: session.model ?? null,
230
- createdAt: new Date().toISOString(),
231
- }, session);
217
+ let completed = false;
218
+ try {
219
+ const provider = providers[session.providerId];
220
+ if (!provider)
221
+ return;
222
+ const prompt = summaryPrompt
223
+ ? buildSummaryPromptWithOverride(summaryPrompt, message, reasoning)
224
+ : buildSummaryPrompt(message, reasoning);
225
+ const summaryModel = getSummaryModel(session.providerId);
226
+ const cwd = session.cwd || basePath;
227
+ const repoRoot = session.repoRoot || basePath;
228
+ const result = await runSummaryPrompt({
229
+ provider,
230
+ prompt,
231
+ model: summaryModel,
232
+ cwd,
233
+ repoRoot,
234
+ });
235
+ if (!result)
236
+ return;
237
+ persistSummary(emit, sessionId, {
238
+ summary: result.summary,
239
+ source: 'prompt',
240
+ provider: session.providerId,
241
+ model: result.model ?? null,
242
+ createdAt: new Date().toISOString(),
243
+ }, session);
244
+ completed = true;
245
+ }
246
+ finally {
247
+ session.summaryRequested = false;
248
+ if (!completed && mode === 'auto') {
249
+ session.summaryAutoUsed = true;
250
+ }
251
+ if (session.summarySeed) {
252
+ maybeStartPromptSummary({
253
+ sessionId,
254
+ session,
255
+ emit,
256
+ trigger: 'output',
257
+ });
258
+ }
259
+ }
232
260
  }
233
261
  async function getCachedStatus(provider, options = {}) {
234
262
  if (options.force) {
@@ -346,7 +374,8 @@ function createHostRuntime(options) {
346
374
  }
347
375
  function maybeStartPromptSummary(options) {
348
376
  const { sessionId, session, emit, trigger } = options;
349
- if (session.providerId === 'claude')
377
+ const mode = resolveEffectiveSummaryMode(session, session.summaryNextMode);
378
+ if (mode === 'off')
350
379
  return;
351
380
  if (session.summaryRequested)
352
381
  return;
@@ -356,8 +385,17 @@ function createHostRuntime(options) {
356
385
  return;
357
386
  const message = session.summarySeed;
358
387
  const reasoning = session.summaryReasoning;
388
+ const summaryPrompt = session.summaryNextPrompt ?? session.summaryPrompt;
359
389
  clearSummarySeed(session);
360
- void startPromptSummary({ sessionId, session, message, reasoning, emit });
390
+ void startPromptSummary({
391
+ sessionId,
392
+ session,
393
+ message,
394
+ reasoning,
395
+ summaryPrompt,
396
+ mode,
397
+ emit,
398
+ });
361
399
  }
362
400
  function sessionSummaryKey(sessionId) {
363
401
  return `session:${sessionId}:summary`;
@@ -366,9 +404,6 @@ function createHostRuntime(options) {
366
404
  if (!payload.summary)
367
405
  return;
368
406
  if (session) {
369
- if (session.summarySource === 'claude-log' && payload.source === 'prompt') {
370
- return;
371
- }
372
407
  if (session.summary === payload.summary && session.summarySource === payload.source) {
373
408
  return;
374
409
  }
@@ -376,6 +411,7 @@ function createHostRuntime(options) {
376
411
  session.summarySource = payload.source;
377
412
  session.summaryModel = payload.model ?? null;
378
413
  session.summaryCreatedAt = payload.createdAt;
414
+ session.summaryAutoUsed = true;
379
415
  }
380
416
  emitSessionEvent(emit, sessionId, 'summary', payload);
381
417
  storage.set(sessionSummaryKey(sessionId), payload);
@@ -598,6 +634,7 @@ function createHostRuntime(options) {
598
634
  model = await pickDefaultModel(rawProvider);
599
635
  }
600
636
  const reasoningEffort = params.reasoningEffort || null;
637
+ const systemPrompt = resolveSystemPrompt(providerId, params.system);
601
638
  const cwd = params.cwd ? resolveAppPathInternal(params.cwd) : undefined;
602
639
  const repoRoot = params.repoRoot ? resolveAppPathInternal(params.repoRoot) : undefined;
603
640
  const providerDetailLevel = params.providerDetailLevel || undefined;
@@ -607,6 +644,8 @@ function createHostRuntime(options) {
607
644
  else {
608
645
  recordProviderCapability(providerId);
609
646
  }
647
+ const summaryMode = normalizeSummaryMode(params.summary?.mode);
648
+ const summaryPrompt = normalizeSummaryPrompt(params.summary?.prompt);
610
649
  sessions.set(sessionId, {
611
650
  id: sessionId,
612
651
  providerId,
@@ -615,9 +654,13 @@ function createHostRuntime(options) {
615
654
  reasoningEffort,
616
655
  cwd,
617
656
  repoRoot,
657
+ systemPrompt,
618
658
  providerDetailLevel: providerDetailLevel === 'raw' || providerDetailLevel === 'minimal'
619
659
  ? providerDetailLevel
620
660
  : undefined,
661
+ summaryMode: summaryMode === 'force' ? 'auto' : summaryMode ?? 'auto',
662
+ summaryPrompt,
663
+ summaryAutoUsed: false,
621
664
  });
622
665
  responder.reply(id, { sessionId });
623
666
  return;
@@ -638,12 +681,25 @@ function createHostRuntime(options) {
638
681
  if (params.repoRoot) {
639
682
  existing.repoRoot = resolveAppPathInternal(params.repoRoot);
640
683
  }
684
+ if ('system' in params) {
685
+ existing.systemPrompt = resolveSystemPrompt(existing.providerId, params.system);
686
+ }
641
687
  if (params.providerDetailLevel) {
642
688
  const level = String(params.providerDetailLevel);
643
689
  if (level === 'raw' || level === 'minimal') {
644
690
  existing.providerDetailLevel = level;
645
691
  }
646
692
  }
693
+ if ('summary' in params) {
694
+ const summaryMode = normalizeSummaryMode(params.summary?.mode);
695
+ const summaryPrompt = normalizeSummaryPrompt(params.summary?.prompt);
696
+ if (summaryMode) {
697
+ existing.summaryMode = summaryMode === 'force' ? 'auto' : summaryMode;
698
+ }
699
+ if (summaryPrompt !== undefined) {
700
+ existing.summaryPrompt = summaryPrompt;
701
+ }
702
+ }
647
703
  if (!existing.model && typeof params.model === 'string' && params.model.trim()) {
648
704
  const candidate = params.model.trim();
649
705
  const matches = await isModelForProvider(candidate, existing.providerId);
@@ -699,10 +755,13 @@ function createHostRuntime(options) {
699
755
  return;
700
756
  }
701
757
  }
702
- if (message.trim() &&
703
- session.providerId !== 'claude' &&
704
- !session.summaryRequested &&
705
- !session.summarySeed) {
758
+ const summaryMode = resolveEffectiveSummaryMode(session, normalizeSummaryMode(params.summary?.mode));
759
+ const summaryPrompt = normalizeSummaryPrompt(params.summary?.prompt);
760
+ session.summaryNextMode = summaryMode;
761
+ if (summaryPrompt !== undefined) {
762
+ session.summaryNextPrompt = summaryPrompt;
763
+ }
764
+ if (summaryMode !== 'off' && message.trim()) {
706
765
  session.summarySeed = message;
707
766
  session.summaryReasoning = '';
708
767
  }
@@ -726,6 +785,7 @@ function createHostRuntime(options) {
726
785
  repoRoot,
727
786
  cwd,
728
787
  providerDetailLevel,
788
+ system: session.systemPrompt,
729
789
  signal: controller.signal,
730
790
  onEvent: (event) => {
731
791
  const current = activeRuns.get(sessionId);
@@ -746,9 +806,7 @@ function createHostRuntime(options) {
746
806
  trigger: 'reasoning',
747
807
  });
748
808
  }
749
- if (event.type === 'delta' ||
750
- event.type === 'message' ||
751
- event.type === 'final') {
809
+ if (event.type === 'delta' || event.type === 'message' || event.type === 'final') {
752
810
  maybeStartPromptSummary({
753
811
  sessionId,
754
812
  session,
@@ -765,7 +823,6 @@ function createHostRuntime(options) {
765
823
  return;
766
824
  if (result?.sessionId) {
767
825
  session.providerSessionId = result.sessionId;
768
- void startClaudeLogSummary({ sessionId, session, emit: responder.emit });
769
826
  }
770
827
  })
771
828
  .catch((err) => {
@@ -9,4 +9,4 @@ export declare function updateClaude(): Promise<ProviderStatus>;
9
9
  export declare function loginClaude(options?: ProviderLoginOptions): Promise<{
10
10
  loggedIn: boolean;
11
11
  }>;
12
- export declare function runClaudePrompt({ prompt, resumeSessionId, model, cwd, providerDetailLevel, onEvent, signal, }: RunPromptOptions): Promise<RunPromptResult>;
12
+ export declare function runClaudePrompt({ prompt, system, resumeSessionId, model, cwd, providerDetailLevel, onEvent, signal, }: RunPromptOptions): Promise<RunPromptResult>;
@@ -184,7 +184,10 @@ function formatClaudeDisplayName(modelId) {
184
184
  const value = modelId.trim();
185
185
  if (!value.startsWith('claude-'))
186
186
  return value;
187
- const parts = value.replace(/^claude-/, '').split('-').filter(Boolean);
187
+ const parts = value
188
+ .replace(/^claude-/, '')
189
+ .split('-')
190
+ .filter(Boolean);
188
191
  if (!parts.length)
189
192
  return value;
190
193
  const family = parts[0];
@@ -560,8 +563,7 @@ async function checkClaudeCliStatus() {
560
563
  : typeof message?.error === 'string'
561
564
  ? message.error
562
565
  : '';
563
- if (isClaudeAuthErrorText(text) ||
564
- (errorText && isClaudeAuthErrorText(errorText))) {
566
+ if (isClaudeAuthErrorText(text) || (errorText && isClaudeAuthErrorText(errorText))) {
565
567
  authError = authError ?? (text || errorText);
566
568
  }
567
569
  else if (text.trim()) {
@@ -901,6 +903,180 @@ function mapClaudeModel(model) {
901
903
  return value.replace('claude-', '');
902
904
  return model;
903
905
  }
906
+ function extractUsageFromValue(value) {
907
+ if (!value || typeof value !== 'object')
908
+ return null;
909
+ const usage = value;
910
+ const toNumber = (v) => {
911
+ if (typeof v === 'number' && Number.isFinite(v))
912
+ return v;
913
+ if (typeof v === 'string' && v.trim()) {
914
+ const parsed = Number(v);
915
+ return Number.isFinite(parsed) ? parsed : undefined;
916
+ }
917
+ return undefined;
918
+ };
919
+ const input = toNumber(usage.input_tokens ?? usage.prompt_tokens ?? usage.inputTokens ?? usage.promptTokens);
920
+ const output = toNumber(usage.output_tokens ?? usage.completion_tokens ?? usage.outputTokens ?? usage.completionTokens);
921
+ const total = toNumber(usage.total_tokens ?? usage.totalTokens);
922
+ let cached = toNumber(usage.cached_input_tokens ?? usage.cachedInputTokens);
923
+ const cacheCreation = toNumber(usage.cache_creation_input_tokens ?? usage.cacheCreationInputTokens);
924
+ const cacheRead = toNumber(usage.cache_read_input_tokens ?? usage.cacheReadInputTokens);
925
+ if (cached === undefined) {
926
+ const sum = (cacheCreation ?? 0) + (cacheRead ?? 0);
927
+ if (sum > 0)
928
+ cached = sum;
929
+ }
930
+ const reasoning = toNumber(usage.reasoning_tokens ?? usage.reasoningTokens);
931
+ const out = {};
932
+ if (input !== undefined)
933
+ out.input_tokens = input;
934
+ if (output !== undefined)
935
+ out.output_tokens = output;
936
+ if (total !== undefined)
937
+ out.total_tokens = total;
938
+ if (cached !== undefined)
939
+ out.cached_input_tokens = cached;
940
+ if (reasoning !== undefined)
941
+ out.reasoning_tokens = reasoning;
942
+ return Object.keys(out).length ? out : null;
943
+ }
944
+ function pickModelUsage(value) {
945
+ if (!value || typeof value !== 'object')
946
+ return null;
947
+ const entries = Object.values(value);
948
+ for (const entry of entries) {
949
+ if (entry && typeof entry === 'object')
950
+ return entry;
951
+ }
952
+ return null;
953
+ }
954
+ function extractClaudeUsage(msg) {
955
+ const usage = msg.usage ??
956
+ msg.message?.usage ??
957
+ msg.event?.usage ??
958
+ msg.delta?.usage ??
959
+ msg.token_usage ??
960
+ msg.tokenUsage;
961
+ if (usage)
962
+ return extractUsageFromValue(usage);
963
+ const resultRecord = msg.result && typeof msg.result === 'object' ? msg.result : null;
964
+ const nestedUsage = resultRecord?.usage ??
965
+ resultRecord?.token_usage ??
966
+ resultRecord?.tokenUsage ??
967
+ resultRecord?.token_usage;
968
+ if (nestedUsage)
969
+ return extractUsageFromValue(nestedUsage);
970
+ const modelUsage = pickModelUsage(msg.modelUsage ?? msg.message?.modelUsage ?? resultRecord?.modelUsage);
971
+ return extractUsageFromValue(modelUsage);
972
+ }
973
+ function extractClaudeContextUsage(msg, usage) {
974
+ const context = msg.context_usage ??
975
+ msg.contextUsage ??
976
+ msg.message?.context_usage ??
977
+ msg.message?.contextUsage ??
978
+ msg.event?.context_usage ??
979
+ msg.event?.contextUsage;
980
+ const resultRecord = msg.result && typeof msg.result === 'object' ? msg.result : null;
981
+ const modelUsage = pickModelUsage(msg.modelUsage ?? msg.message?.modelUsage ?? resultRecord?.modelUsage);
982
+ const toNumber = (v) => {
983
+ if (typeof v === 'number' && Number.isFinite(v))
984
+ return v;
985
+ if (typeof v === 'string' && v.trim()) {
986
+ const parsed = Number(v);
987
+ return Number.isFinite(parsed) ? parsed : undefined;
988
+ }
989
+ return undefined;
990
+ };
991
+ const toBoolean = (v) => typeof v === 'boolean' ? v : undefined;
992
+ const contextWindow = toNumber(context?.context_window ??
993
+ context?.contextWindow ??
994
+ modelUsage?.contextWindow ??
995
+ modelUsage?.context_window ??
996
+ msg.context_window ??
997
+ msg.contextWindow);
998
+ let contextTokens = toNumber(context?.context_tokens ??
999
+ context?.contextTokens ??
1000
+ msg.context_tokens ??
1001
+ msg.contextTokens);
1002
+ let contextCachedTokens = toNumber(context?.context_cached_tokens ??
1003
+ context?.contextCachedTokens ??
1004
+ msg.context_cached_tokens ??
1005
+ msg.contextCachedTokens);
1006
+ let contextRemainingTokens = toNumber(context?.context_remaining_tokens ??
1007
+ context?.contextRemainingTokens ??
1008
+ msg.context_remaining_tokens ??
1009
+ msg.contextRemainingTokens);
1010
+ const contextTruncated = toBoolean(context?.context_truncated ??
1011
+ context?.contextTruncated ??
1012
+ msg.context_truncated ??
1013
+ msg.contextTruncated);
1014
+ if (contextCachedTokens === undefined) {
1015
+ const cached = toNumber(context?.cache_creation_input_tokens) ??
1016
+ toNumber(context?.cacheCreationInputTokens) ??
1017
+ toNumber(context?.cache_read_input_tokens) ??
1018
+ toNumber(context?.cacheReadInputTokens);
1019
+ if (cached !== undefined)
1020
+ contextCachedTokens = cached;
1021
+ }
1022
+ if (contextTokens === undefined) {
1023
+ if (usage?.input_tokens !== undefined &&
1024
+ usage?.cached_input_tokens !== undefined) {
1025
+ contextTokens = usage.input_tokens + usage.cached_input_tokens;
1026
+ }
1027
+ else if (usage?.input_tokens !== undefined) {
1028
+ contextTokens = usage.input_tokens;
1029
+ }
1030
+ }
1031
+ if (contextCachedTokens === undefined && usage?.cached_input_tokens !== undefined) {
1032
+ contextCachedTokens = usage.cached_input_tokens;
1033
+ }
1034
+ if (contextRemainingTokens === undefined &&
1035
+ contextWindow !== undefined &&
1036
+ contextTokens !== undefined) {
1037
+ contextRemainingTokens = Math.max(0, contextWindow - contextTokens);
1038
+ }
1039
+ const out = {};
1040
+ if (contextWindow !== undefined)
1041
+ out.context_window = contextWindow;
1042
+ if (contextTokens !== undefined)
1043
+ out.context_tokens = contextTokens;
1044
+ if (contextCachedTokens !== undefined)
1045
+ out.context_cached_tokens = contextCachedTokens;
1046
+ if (contextRemainingTokens !== undefined)
1047
+ out.context_remaining_tokens = contextRemainingTokens;
1048
+ if (contextTruncated !== undefined)
1049
+ out.context_truncated = contextTruncated;
1050
+ return Object.keys(out).length ? out : null;
1051
+ }
1052
+ function mergeUsage(current, next) {
1053
+ const out = { ...(current ?? {}) };
1054
+ if (next.input_tokens !== undefined)
1055
+ out.input_tokens = next.input_tokens;
1056
+ if (next.output_tokens !== undefined)
1057
+ out.output_tokens = next.output_tokens;
1058
+ if (next.total_tokens !== undefined)
1059
+ out.total_tokens = next.total_tokens;
1060
+ if (next.cached_input_tokens !== undefined)
1061
+ out.cached_input_tokens = next.cached_input_tokens;
1062
+ if (next.reasoning_tokens !== undefined)
1063
+ out.reasoning_tokens = next.reasoning_tokens;
1064
+ return out;
1065
+ }
1066
+ function mergeContextUsage(current, next) {
1067
+ const out = { ...(current ?? {}) };
1068
+ if (next.context_window !== undefined)
1069
+ out.context_window = next.context_window;
1070
+ if (next.context_tokens !== undefined)
1071
+ out.context_tokens = next.context_tokens;
1072
+ if (next.context_cached_tokens !== undefined)
1073
+ out.context_cached_tokens = next.context_cached_tokens;
1074
+ if (next.context_remaining_tokens !== undefined)
1075
+ out.context_remaining_tokens = next.context_remaining_tokens;
1076
+ if (next.context_truncated !== undefined)
1077
+ out.context_truncated = next.context_truncated;
1078
+ return out;
1079
+ }
904
1080
  function extractSessionId(msg) {
905
1081
  const direct = msg.session_id ?? msg.sessionId;
906
1082
  if (typeof direct === 'string' && direct)
@@ -960,7 +1136,7 @@ function extractResultText(msg) {
960
1136
  const text = msg.result;
961
1137
  return typeof text === 'string' && text ? text : null;
962
1138
  }
963
- export function runClaudePrompt({ prompt, resumeSessionId, model, cwd, providerDetailLevel, onEvent, signal, }) {
1139
+ export function runClaudePrompt({ prompt, system, resumeSessionId, model, cwd, providerDetailLevel, onEvent, signal, }) {
964
1140
  return new Promise((resolve) => {
965
1141
  const command = getClaudeCommand();
966
1142
  const args = [
@@ -970,6 +1146,10 @@ export function runClaudePrompt({ prompt, resumeSessionId, model, cwd, providerD
970
1146
  '--permission-mode',
971
1147
  'bypassPermissions',
972
1148
  ];
1149
+ const systemPrompt = typeof system === 'string' ? system.trim() : '';
1150
+ if (systemPrompt) {
1151
+ args.push('--append-system-prompt', systemPrompt);
1152
+ }
973
1153
  const modelValue = mapClaudeModel(model);
974
1154
  if (modelValue) {
975
1155
  args.push('--model', modelValue);
@@ -998,6 +1178,11 @@ export function runClaudePrompt({ prompt, resumeSessionId, model, cwd, providerD
998
1178
  let finalSessionId = null;
999
1179
  let didFinalize = false;
1000
1180
  let sawError = false;
1181
+ let usageEmitted = false;
1182
+ let latestUsage = null;
1183
+ let latestContextUsage = null;
1184
+ let latestUsageDetail;
1185
+ let latestContextUsageDetail;
1001
1186
  const toolBlocks = new Map();
1002
1187
  const thinkingBlocks = new Set();
1003
1188
  const includeRaw = providerDetailLevel === 'raw';
@@ -1021,11 +1206,46 @@ export function runClaudePrompt({ prompt, resumeSessionId, model, cwd, providerD
1021
1206
  if (sawError)
1022
1207
  return;
1023
1208
  sawError = true;
1209
+ emitUsageIfAvailable();
1024
1210
  emit({ type: 'error', message });
1025
1211
  };
1026
1212
  const emitFinal = (text, providerDetail) => {
1213
+ emitUsageIfAvailable();
1027
1214
  emit({ type: 'final', text, providerDetail });
1028
1215
  };
1216
+ const captureUsage = (msg, providerDetail) => {
1217
+ const usage = extractClaudeUsage(msg);
1218
+ if (usage) {
1219
+ latestUsage = mergeUsage(latestUsage, usage);
1220
+ latestUsageDetail = providerDetail;
1221
+ }
1222
+ const contextUsage = extractClaudeContextUsage(msg, latestUsage);
1223
+ if (contextUsage) {
1224
+ latestContextUsage = mergeContextUsage(latestContextUsage, contextUsage);
1225
+ latestContextUsageDetail = providerDetail;
1226
+ }
1227
+ };
1228
+ const emitUsageIfAvailable = () => {
1229
+ if (usageEmitted)
1230
+ return;
1231
+ if (!latestUsage && !latestContextUsage)
1232
+ return;
1233
+ if (latestUsage) {
1234
+ emit({
1235
+ type: 'usage',
1236
+ usage: latestUsage,
1237
+ providerDetail: latestUsageDetail,
1238
+ });
1239
+ }
1240
+ if (latestContextUsage) {
1241
+ emit({
1242
+ type: 'context_usage',
1243
+ contextUsage: latestContextUsage,
1244
+ providerDetail: latestContextUsageDetail ?? latestUsageDetail,
1245
+ });
1246
+ }
1247
+ usageEmitted = true;
1248
+ };
1029
1249
  const handleLine = (line) => {
1030
1250
  const parsed = safeJsonParse(line);
1031
1251
  if (!parsed || typeof parsed !== 'object') {
@@ -1040,6 +1260,8 @@ export function runClaudePrompt({ prompt, resumeSessionId, model, cwd, providerD
1040
1260
  finalSessionId = sid;
1041
1261
  const msgType = String(msg.type ?? '');
1042
1262
  let handled = false;
1263
+ const detail = buildProviderDetail(msgType || 'unknown', {}, msg);
1264
+ captureUsage(msg, detail);
1043
1265
  if (msgType === 'assistant' || msgType === 'user' || msgType === 'system') {
1044
1266
  const role = msg.message?.role === 'assistant' ||
1045
1267
  msg.message?.role === 'user' ||
@@ -1054,7 +1276,7 @@ export function runClaudePrompt({ prompt, resumeSessionId, model, cwd, providerD
1054
1276
  role,
1055
1277
  content,
1056
1278
  contentParts: rawContent ?? null,
1057
- providerDetail: buildProviderDetail(msgType, {}, msg),
1279
+ providerDetail: detail,
1058
1280
  });
1059
1281
  handled = true;
1060
1282
  }
@@ -1063,7 +1285,9 @@ export function runClaudePrompt({ prompt, resumeSessionId, model, cwd, providerD
1063
1285
  const index = typeof msg.event.index === 'number' ? msg.event.index : undefined;
1064
1286
  const block = msg.event.content_block;
1065
1287
  if (evType === 'content_block_start' && block && typeof index === 'number') {
1066
- if (block.type === 'tool_use' || block.type === 'server_tool_use' || block.type === 'mcp_tool_use') {
1288
+ if (block.type === 'tool_use' ||
1289
+ block.type === 'server_tool_use' ||
1290
+ block.type === 'mcp_tool_use') {
1067
1291
  toolBlocks.set(index, { id: block.id, name: block.name });
1068
1292
  emit({
1069
1293
  type: 'tool_call',
@@ -1144,7 +1368,7 @@ export function runClaudePrompt({ prompt, resumeSessionId, model, cwd, providerD
1144
1368
  emit({
1145
1369
  type: 'delta',
1146
1370
  text: delta,
1147
- providerDetail: buildProviderDetail(msgType || 'delta', {}, msg),
1371
+ providerDetail: detail,
1148
1372
  });
1149
1373
  return;
1150
1374
  }
@@ -1154,21 +1378,21 @@ export function runClaudePrompt({ prompt, resumeSessionId, model, cwd, providerD
1154
1378
  emit({
1155
1379
  type: 'delta',
1156
1380
  text: assistant,
1157
- providerDetail: buildProviderDetail(msgType || 'assistant', {}, msg),
1381
+ providerDetail: detail,
1158
1382
  });
1159
1383
  return;
1160
1384
  }
1161
1385
  const result = extractResultText(msg);
1162
1386
  if (result && !didFinalize && !sawError) {
1163
1387
  didFinalize = true;
1164
- emitFinal(aggregated || result, buildProviderDetail('result', {}, msg));
1388
+ emitFinal(aggregated || result, detail);
1165
1389
  handled = true;
1166
1390
  }
1167
1391
  if (!handled) {
1168
1392
  emit({
1169
1393
  type: 'detail',
1170
1394
  provider: 'claude',
1171
- providerDetail: buildProviderDetail(msgType || 'unknown', {}, msg),
1395
+ providerDetail: detail,
1172
1396
  });
1173
1397
  }
1174
1398
  };
@@ -1185,6 +1409,9 @@ export function runClaudePrompt({ prompt, resumeSessionId, model, cwd, providerD
1185
1409
  emitFinal(aggregated);
1186
1410
  }
1187
1411
  }
1412
+ if (!usageEmitted) {
1413
+ emitUsageIfAvailable();
1414
+ }
1188
1415
  resolve({ sessionId: finalSessionId });
1189
1416
  });
1190
1417
  child.on('error', (err) => {
@@ -8,4 +8,4 @@ export declare function loginCodex(): Promise<{
8
8
  loggedIn: boolean;
9
9
  }>;
10
10
  export declare function listCodexModels(): Promise<ModelInfo[]>;
11
- export declare function runCodexPrompt({ prompt, resumeSessionId, model, reasoningEffort, repoRoot, cwd, providerDetailLevel, onEvent, signal, }: RunPromptOptions): Promise<RunPromptResult>;
11
+ export declare function runCodexPrompt({ prompt, system, resumeSessionId, model, reasoningEffort, repoRoot, cwd, providerDetailLevel, onEvent, signal, }: RunPromptOptions): Promise<RunPromptResult>;
@@ -3,7 +3,7 @@ import { readFile } from 'fs/promises';
3
3
  import https from 'https';
4
4
  import os from 'os';
5
5
  import path from 'path';
6
- import { buildInstallCommandAuto, buildLoginCommand, buildStatusCommand, checkCommandVersion, commandExists, createLineParser, debugLog, logProviderSpawn, resolveWindowsCommand, resolveCommandPath, resolveCommandRealPath, runCommand, } from './utils.js';
6
+ import { buildInstallCommandAuto, buildLoginCommand, buildStatusCommand, checkCommandVersion, commandExists, createLineParser, applySystemPrompt, debugLog, logProviderSpawn, resolveWindowsCommand, resolveCommandPath, resolveCommandRealPath, runCommand, } from './utils.js';
7
7
  const CODEX_PACKAGE = '@openai/codex';
8
8
  const DEFAULT_LOGIN = 'codex login';
9
9
  const DEFAULT_STATUS = 'codex login status';
@@ -237,9 +237,7 @@ function buildCodexExecArgs(options) {
237
237
  }
238
238
  args.push('--yolo');
239
239
  const summarySetting = process.env.AGENTCONNECT_CODEX_REASONING_SUMMARY;
240
- const summary = summarySetting && summarySetting.trim()
241
- ? summarySetting.trim()
242
- : 'detailed';
240
+ const summary = summarySetting && summarySetting.trim() ? summarySetting.trim() : 'detailed';
243
241
  const summaryDisabled = ['0', 'false', 'off', 'none'].includes(summary.toLowerCase());
244
242
  if (!summaryDisabled) {
245
243
  args.push('--config', `model_reasoning_summary=${summary}`);
@@ -329,7 +327,9 @@ export async function getCodexStatus() {
329
327
  const status = buildStatusCommand('AGENTCONNECT_CODEX_STATUS', DEFAULT_STATUS);
330
328
  if (status.command) {
331
329
  const statusCommand = resolveWindowsCommand(status.command);
332
- const result = await runCommand(statusCommand, status.args, { env: { ...process.env, CI: '1' } });
330
+ const result = await runCommand(statusCommand, status.args, {
331
+ env: { ...process.env, CI: '1' },
332
+ });
333
333
  const output = `${result.stdout}\n${result.stderr}`.toLowerCase();
334
334
  if (output.includes('not logged in') ||
335
335
  output.includes('not logged') ||
@@ -430,11 +430,20 @@ function extractUsage(ev) {
430
430
  const usage = ev.usage ?? ev.token_usage ?? ev.tokens ?? ev.tokenUsage;
431
431
  if (!usage || typeof usage !== 'object')
432
432
  return null;
433
- const toNumber = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
433
+ const toNumber = (v) => {
434
+ if (typeof v === 'number' && Number.isFinite(v))
435
+ return v;
436
+ if (typeof v === 'string' && v.trim()) {
437
+ const parsed = Number(v);
438
+ return Number.isFinite(parsed) ? parsed : undefined;
439
+ }
440
+ return undefined;
441
+ };
434
442
  const input = toNumber(usage.input_tokens ?? usage.prompt_tokens ?? usage.inputTokens ?? usage.promptTokens);
435
443
  const output = toNumber(usage.output_tokens ?? usage.completion_tokens ?? usage.outputTokens ?? usage.completionTokens);
436
444
  const total = toNumber(usage.total_tokens ?? usage.totalTokens);
437
445
  const cached = toNumber(usage.cached_input_tokens ?? usage.cachedInputTokens);
446
+ const reasoning = toNumber(usage.reasoning_tokens ?? usage.reasoningTokens);
438
447
  const out = {};
439
448
  if (input !== undefined)
440
449
  out.input_tokens = input;
@@ -444,6 +453,61 @@ function extractUsage(ev) {
444
453
  out.total_tokens = total;
445
454
  if (cached !== undefined)
446
455
  out.cached_input_tokens = cached;
456
+ if (reasoning !== undefined)
457
+ out.reasoning_tokens = reasoning;
458
+ return Object.keys(out).length ? out : null;
459
+ }
460
+ function extractContextUsage(ev, usage) {
461
+ const context = ev.context_usage ?? ev.contextUsage;
462
+ const toNumber = (v) => {
463
+ if (typeof v === 'number' && Number.isFinite(v))
464
+ return v;
465
+ if (typeof v === 'string' && v.trim()) {
466
+ const parsed = Number(v);
467
+ return Number.isFinite(parsed) ? parsed : undefined;
468
+ }
469
+ return undefined;
470
+ };
471
+ const toBoolean = (v) => typeof v === 'boolean' ? v : undefined;
472
+ const contextWindow = toNumber(context?.context_window ?? context?.contextWindow ?? ev.context_window ?? ev.contextWindow);
473
+ let contextTokens = toNumber(context?.context_tokens ?? context?.contextTokens ?? ev.context_tokens ?? ev.contextTokens);
474
+ let contextCachedTokens = toNumber(context?.context_cached_tokens ??
475
+ context?.contextCachedTokens ??
476
+ ev.context_cached_tokens ??
477
+ ev.contextCachedTokens);
478
+ let contextRemainingTokens = toNumber(context?.context_remaining_tokens ??
479
+ context?.contextRemainingTokens ??
480
+ ev.context_remaining_tokens ??
481
+ ev.contextRemainingTokens);
482
+ const contextTruncated = toBoolean(context?.context_truncated ?? context?.contextTruncated ?? ev.context_truncated ?? ev.contextTruncated);
483
+ if (contextTokens === undefined) {
484
+ if (usage?.input_tokens !== undefined &&
485
+ usage?.cached_input_tokens !== undefined) {
486
+ contextTokens = usage.input_tokens + usage.cached_input_tokens;
487
+ }
488
+ else if (usage?.input_tokens !== undefined) {
489
+ contextTokens = usage.input_tokens;
490
+ }
491
+ }
492
+ if (contextCachedTokens === undefined && usage?.cached_input_tokens !== undefined) {
493
+ contextCachedTokens = usage.cached_input_tokens;
494
+ }
495
+ if (contextRemainingTokens === undefined &&
496
+ contextWindow !== undefined &&
497
+ contextTokens !== undefined) {
498
+ contextRemainingTokens = Math.max(0, contextWindow - contextTokens);
499
+ }
500
+ const out = {};
501
+ if (contextWindow !== undefined)
502
+ out.context_window = contextWindow;
503
+ if (contextTokens !== undefined)
504
+ out.context_tokens = contextTokens;
505
+ if (contextCachedTokens !== undefined)
506
+ out.context_cached_tokens = contextCachedTokens;
507
+ if (contextRemainingTokens !== undefined)
508
+ out.context_remaining_tokens = contextRemainingTokens;
509
+ if (contextTruncated !== undefined)
510
+ out.context_truncated = contextTruncated;
447
511
  return Object.keys(out).length ? out : null;
448
512
  }
449
513
  function normalizeItem(raw) {
@@ -644,16 +708,17 @@ export async function listCodexModels() {
644
708
  // Fetch failed - return empty, don't cache so it retries next time
645
709
  return [];
646
710
  }
647
- export function runCodexPrompt({ prompt, resumeSessionId, model, reasoningEffort, repoRoot, cwd, providerDetailLevel, onEvent, signal, }) {
711
+ export function runCodexPrompt({ prompt, system, resumeSessionId, model, reasoningEffort, repoRoot, cwd, providerDetailLevel, onEvent, signal, }) {
648
712
  return new Promise((resolve) => {
649
713
  const command = getCodexCommand();
650
714
  const resolvedRepoRoot = repoRoot ? path.resolve(repoRoot) : null;
651
715
  const resolvedCwd = cwd ? path.resolve(cwd) : null;
652
716
  const runDir = resolvedCwd || resolvedRepoRoot || process.cwd();
653
717
  const cdTarget = resolvedRepoRoot || resolvedCwd || '.';
718
+ const composedPrompt = applySystemPrompt(system, prompt);
654
719
  const runAttempt = (mode) => new Promise((attemptResolve) => {
655
720
  const args = buildCodexExecArgs({
656
- prompt,
721
+ prompt: composedPrompt,
657
722
  cdTarget,
658
723
  resumeSessionId,
659
724
  model,
@@ -839,8 +904,16 @@ export function runCodexPrompt({ prompt, resumeSessionId, model, reasoningEffort
839
904
  if (usage) {
840
905
  emit({
841
906
  type: 'usage',
842
- inputTokens: usage.input_tokens,
843
- outputTokens: usage.output_tokens,
907
+ usage,
908
+ providerDetail,
909
+ });
910
+ handled = true;
911
+ }
912
+ const contextUsage = extractContextUsage(ev, usage);
913
+ if (contextUsage) {
914
+ emit({
915
+ type: 'context_usage',
916
+ contextUsage,
844
917
  providerDetail,
845
918
  });
846
919
  handled = true;
@@ -893,9 +966,7 @@ export function runCodexPrompt({ prompt, resumeSessionId, model, reasoningEffort
893
966
  }
894
967
  if (isTerminalEvent(ev) && !didFinalize) {
895
968
  if (ev.type === 'turn.failed') {
896
- const message = typeof ev.error?.message === 'string'
897
- ? ev.error.message
898
- : pendingError?.message;
969
+ const message = typeof ev.error?.message === 'string' ? ev.error.message : pendingError?.message;
899
970
  emitError(message ?? 'Codex run failed', providerDetail);
900
971
  didFinalize = true;
901
972
  handled = true;
@@ -921,10 +992,9 @@ export function runCodexPrompt({ prompt, resumeSessionId, model, reasoningEffort
921
992
  const hint = stderrLines.at(-1) || stdoutLines.at(-1) || '';
922
993
  const context = pendingError?.message || hint;
923
994
  const suffix = context ? `: ${context}` : '';
924
- const fallback = mode === 'modern' && !sawJson && shouldFallbackToLegacy([
925
- ...stderrLines,
926
- ...stdoutLines,
927
- ]);
995
+ const fallback = mode === 'modern' &&
996
+ !sawJson &&
997
+ shouldFallbackToLegacy([...stderrLines, ...stdoutLines]);
928
998
  debugLog('Codex', 'exit', {
929
999
  code,
930
1000
  stderr: stderrLines,
@@ -8,4 +8,4 @@ export declare function updateCursor(): Promise<ProviderStatus>;
8
8
  export declare function loginCursor(options?: ProviderLoginOptions): Promise<{
9
9
  loggedIn: boolean;
10
10
  }>;
11
- export declare function runCursorPrompt({ prompt, resumeSessionId, model, repoRoot, cwd, providerDetailLevel, onEvent, signal, }: RunPromptOptions): Promise<RunPromptResult>;
11
+ export declare function runCursorPrompt({ prompt, system, resumeSessionId, model, repoRoot, cwd, providerDetailLevel, onEvent, signal, }: RunPromptOptions): Promise<RunPromptResult>;
@@ -1,6 +1,6 @@
1
1
  import { spawn } from 'child_process';
2
2
  import path from 'path';
3
- import { buildInstallCommand, buildLoginCommand, buildStatusCommand, checkCommandVersion, commandExists, createLineParser, debugLog, logProviderSpawn, resolveWindowsCommand, resolveCommandPath, resolveCommandRealPath, runCommand, } from './utils.js';
3
+ import { buildInstallCommand, buildLoginCommand, buildStatusCommand, checkCommandVersion, commandExists, createLineParser, applySystemPrompt, debugLog, logProviderSpawn, resolveWindowsCommand, resolveCommandPath, resolveCommandRealPath, runCommand, } from './utils.js';
4
4
  const INSTALL_UNIX = 'curl https://cursor.com/install -fsS | bash';
5
5
  const DEFAULT_LOGIN = 'cursor-agent login';
6
6
  const DEFAULT_STATUS = 'cursor-agent status';
@@ -468,10 +468,20 @@ function extractUsage(ev) {
468
468
  const usage = ev.usage ?? ev.token_usage ?? ev.tokenUsage ?? ev.tokens;
469
469
  if (!usage || typeof usage !== 'object')
470
470
  return null;
471
- const toNumber = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
471
+ const toNumber = (v) => {
472
+ if (typeof v === 'number' && Number.isFinite(v))
473
+ return v;
474
+ if (typeof v === 'string' && v.trim()) {
475
+ const parsed = Number(v);
476
+ return Number.isFinite(parsed) ? parsed : undefined;
477
+ }
478
+ return undefined;
479
+ };
472
480
  const input = toNumber(usage.input_tokens ?? usage.prompt_tokens ?? usage.inputTokens ?? usage.promptTokens);
473
481
  const output = toNumber(usage.output_tokens ?? usage.completion_tokens ?? usage.outputTokens ?? usage.completionTokens);
474
482
  const total = toNumber(usage.total_tokens ?? usage.totalTokens);
483
+ const cached = toNumber(usage.cached_input_tokens ?? usage.cachedInputTokens);
484
+ const reasoning = toNumber(usage.reasoning_tokens ?? usage.reasoningTokens);
475
485
  const out = {};
476
486
  if (input !== undefined)
477
487
  out.input_tokens = input;
@@ -479,6 +489,63 @@ function extractUsage(ev) {
479
489
  out.output_tokens = output;
480
490
  if (total !== undefined)
481
491
  out.total_tokens = total;
492
+ if (cached !== undefined)
493
+ out.cached_input_tokens = cached;
494
+ if (reasoning !== undefined)
495
+ out.reasoning_tokens = reasoning;
496
+ return Object.keys(out).length ? out : null;
497
+ }
498
+ function extractContextUsage(ev, usage) {
499
+ const context = ev.context_usage ?? ev.contextUsage;
500
+ const toNumber = (v) => {
501
+ if (typeof v === 'number' && Number.isFinite(v))
502
+ return v;
503
+ if (typeof v === 'string' && v.trim()) {
504
+ const parsed = Number(v);
505
+ return Number.isFinite(parsed) ? parsed : undefined;
506
+ }
507
+ return undefined;
508
+ };
509
+ const toBoolean = (v) => typeof v === 'boolean' ? v : undefined;
510
+ const contextWindow = toNumber(context?.context_window ?? context?.contextWindow ?? ev.context_window ?? ev.contextWindow);
511
+ let contextTokens = toNumber(context?.context_tokens ?? context?.contextTokens ?? ev.context_tokens ?? ev.contextTokens);
512
+ let contextCachedTokens = toNumber(context?.context_cached_tokens ??
513
+ context?.contextCachedTokens ??
514
+ ev.context_cached_tokens ??
515
+ ev.contextCachedTokens);
516
+ let contextRemainingTokens = toNumber(context?.context_remaining_tokens ??
517
+ context?.contextRemainingTokens ??
518
+ ev.context_remaining_tokens ??
519
+ ev.contextRemainingTokens);
520
+ const contextTruncated = toBoolean(context?.context_truncated ?? context?.contextTruncated ?? ev.context_truncated ?? ev.contextTruncated);
521
+ if (contextTokens === undefined) {
522
+ if (usage?.input_tokens !== undefined &&
523
+ usage?.cached_input_tokens !== undefined) {
524
+ contextTokens = usage.input_tokens + usage.cached_input_tokens;
525
+ }
526
+ else if (usage?.input_tokens !== undefined) {
527
+ contextTokens = usage.input_tokens;
528
+ }
529
+ }
530
+ if (contextCachedTokens === undefined && usage?.cached_input_tokens !== undefined) {
531
+ contextCachedTokens = usage.cached_input_tokens;
532
+ }
533
+ if (contextRemainingTokens === undefined &&
534
+ contextWindow !== undefined &&
535
+ contextTokens !== undefined) {
536
+ contextRemainingTokens = Math.max(0, contextWindow - contextTokens);
537
+ }
538
+ const out = {};
539
+ if (contextWindow !== undefined)
540
+ out.context_window = contextWindow;
541
+ if (contextTokens !== undefined)
542
+ out.context_tokens = contextTokens;
543
+ if (contextCachedTokens !== undefined)
544
+ out.context_cached_tokens = contextCachedTokens;
545
+ if (contextRemainingTokens !== undefined)
546
+ out.context_remaining_tokens = contextRemainingTokens;
547
+ if (contextTruncated !== undefined)
548
+ out.context_truncated = contextTruncated;
482
549
  return Object.keys(out).length ? out : null;
483
550
  }
484
551
  function extractTextFromContent(content) {
@@ -611,7 +678,7 @@ function extractErrorMessage(ev) {
611
678
  return ev.result;
612
679
  return null;
613
680
  }
614
- export function runCursorPrompt({ prompt, resumeSessionId, model, repoRoot, cwd, providerDetailLevel, onEvent, signal, }) {
681
+ export function runCursorPrompt({ prompt, system, resumeSessionId, model, repoRoot, cwd, providerDetailLevel, onEvent, signal, }) {
615
682
  return new Promise((resolve) => {
616
683
  const command = getCursorCommand();
617
684
  const resolvedRepoRoot = repoRoot ? path.resolve(repoRoot) : null;
@@ -630,7 +697,8 @@ export function runCursorPrompt({ prompt, resumeSessionId, model, repoRoot, cwd,
630
697
  if (endpoint) {
631
698
  args.push('--endpoint', endpoint);
632
699
  }
633
- args.push(prompt);
700
+ const composedPrompt = applySystemPrompt(system, prompt);
701
+ args.push(composedPrompt);
634
702
  logProviderSpawn({
635
703
  provider: 'cursor',
636
704
  command,
@@ -650,7 +718,7 @@ export function runCursorPrompt({ prompt, resumeSessionId, model, repoRoot, cwd,
650
718
  endpoint: endpoint || null,
651
719
  resume: resumeSessionId || null,
652
720
  apiKeyConfigured: Boolean(getCursorApiKey().trim()),
653
- promptChars: prompt.length,
721
+ promptChars: composedPrompt.length,
654
722
  });
655
723
  const child = spawn(command, args, {
656
724
  cwd: runDir,
@@ -786,8 +854,14 @@ export function runCursorPrompt({ prompt, resumeSessionId, model, repoRoot, cwd,
786
854
  if (usage) {
787
855
  emit({
788
856
  type: 'usage',
789
- inputTokens: usage.input_tokens,
790
- outputTokens: usage.output_tokens,
857
+ usage,
858
+ });
859
+ }
860
+ const contextUsage = extractContextUsage(ev, usage);
861
+ if (contextUsage) {
862
+ emit({
863
+ type: 'context_usage',
864
+ contextUsage,
791
865
  });
792
866
  }
793
867
  if (isErrorEvent(ev)) {
@@ -6,4 +6,4 @@ export declare function loginLocal(options?: ProviderLoginOptions): Promise<{
6
6
  loggedIn: boolean;
7
7
  }>;
8
8
  export declare function listLocalModels(): Promise<ModelInfo[]>;
9
- export declare function runLocalPrompt({ prompt, model, onEvent, }: RunPromptOptions): Promise<RunPromptResult>;
9
+ export declare function runLocalPrompt({ prompt, system, model, onEvent, }: RunPromptOptions): Promise<RunPromptResult>;
@@ -76,7 +76,7 @@ export async function listLocalModels() {
76
76
  .map((entry) => ({ id: entry.id, provider: 'local', displayName: entry.id }))
77
77
  .filter((entry) => entry.id);
78
78
  }
79
- export async function runLocalPrompt({ prompt, model, onEvent, }) {
79
+ export async function runLocalPrompt({ prompt, system, model, onEvent, }) {
80
80
  const base = getLocalBaseUrl();
81
81
  const fallback = process.env.AGENTCONNECT_LOCAL_MODEL || '';
82
82
  const resolvedModel = resolveLocalModel(model, fallback);
@@ -90,9 +90,15 @@ export async function runLocalPrompt({ prompt, model, onEvent, }) {
90
90
  args: ['--base-url', base, '--model', resolvedModel, prompt],
91
91
  cwd: process.cwd(),
92
92
  });
93
+ const messages = [];
94
+ const systemPrompt = typeof system === 'string' ? system.trim() : '';
95
+ if (systemPrompt) {
96
+ messages.push({ role: 'system', content: systemPrompt });
97
+ }
98
+ messages.push({ role: 'user', content: prompt });
93
99
  const payload = {
94
100
  model: resolvedModel,
95
- messages: [{ role: 'user', content: prompt }],
101
+ messages,
96
102
  stream: false,
97
103
  };
98
104
  const headers = { 'Content-Type': 'application/json' };
@@ -10,6 +10,7 @@ export declare function logProviderSpawn(options: {
10
10
  redactIndex?: number;
11
11
  }): void;
12
12
  export declare function debugLog(scope: string, message: string, details?: Record<string, unknown>): void;
13
+ export declare function applySystemPrompt(system: string | undefined, prompt: string): string;
13
14
  export interface SplitCommandResult {
14
15
  command: string;
15
16
  args: string[];
@@ -17,9 +17,7 @@ export function logProviderSpawn(options) {
17
17
  }
18
18
  const cwd = options.cwd || process.cwd();
19
19
  const formatted = formatShellCommand(options.command, redacted);
20
- const fullCommand = cwd
21
- ? `${formatShellCommand('cd', [cwd])} && ${formatted}`
22
- : formatted;
20
+ const fullCommand = cwd ? `${formatShellCommand('cd', [cwd])} && ${formatted}` : formatted;
23
21
  console.log(`AgentConnect: ${fullCommand}`);
24
22
  }
25
23
  function formatShellCommand(command, args) {
@@ -46,6 +44,12 @@ export function debugLog(scope, message, details) {
46
44
  }
47
45
  console.log(`[AgentConnect][${scope}] ${message}${suffix}`);
48
46
  }
47
+ export function applySystemPrompt(system, prompt) {
48
+ const trimmed = typeof system === 'string' ? system.trim() : '';
49
+ if (!trimmed)
50
+ return prompt;
51
+ return `<<SYSTEM>>\n${trimmed}\n<</SYSTEM>>\n\n<<USER>>\n${prompt}`;
52
+ }
49
53
  export function splitCommand(value) {
50
54
  if (!value)
51
55
  return { command: '', args: [] };
package/dist/summary.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Provider, ProviderId } from './types.js';
2
- export type SummarySource = 'prompt' | 'claude-log';
2
+ export type SummarySource = 'prompt';
3
3
  export type SummaryPayload = {
4
4
  summary: string;
5
5
  source: SummarySource;
@@ -9,6 +9,7 @@ export type SummaryPayload = {
9
9
  };
10
10
  export declare function getSummaryModel(providerId: ProviderId): string | null;
11
11
  export declare function buildSummaryPrompt(userPrompt: string, reasoning?: string): string;
12
+ export declare function buildSummaryPromptWithOverride(template: string, userPrompt: string, reasoning?: string): string;
12
13
  export declare function sanitizeSummary(raw: string): string;
13
14
  export declare function runSummaryPrompt(options: {
14
15
  provider: Provider;
@@ -21,9 +22,3 @@ export declare function runSummaryPrompt(options: {
21
22
  summary: string;
22
23
  model?: string | null;
23
24
  } | null>;
24
- export declare function pollClaudeSummary(options: {
25
- basePath: string;
26
- sessionId: string;
27
- timeoutMs?: number;
28
- intervalMs?: number;
29
- }): Promise<string | null>;
package/dist/summary.js CHANGED
@@ -1,7 +1,3 @@
1
- import fs from 'fs';
2
- import { promises as fsp } from 'fs';
3
- import os from 'os';
4
- import path from 'path';
5
1
  const SUMMARY_MODEL_OVERRIDES = {
6
2
  claude: 'haiku',
7
3
  codex: 'gpt-5.1-codex-mini',
@@ -30,6 +26,7 @@ export function buildSummaryPrompt(userPrompt, reasoning) {
30
26
  const clippedReasoning = reasoning?.trim() ? clipText(reasoning, REASONING_MAX_CHARS) : '';
31
27
  const lines = [
32
28
  'You write ultra-short task summaries for a chat list.',
29
+ 'Do not run commands or edit files; only write the summary.',
33
30
  `Summarize the task in ${Math.max(6, SUMMARY_MAX_WORDS - 4)}-${SUMMARY_MAX_WORDS} words.`,
34
31
  'Capture the task and outcome; include key file/component/tech if present.',
35
32
  'Use a specific action verb; avoid vague verbs like "help" or "work on".',
@@ -46,8 +43,32 @@ export function buildSummaryPrompt(userPrompt, reasoning) {
46
43
  }
47
44
  return lines.join('\n');
48
45
  }
46
+ export function buildSummaryPromptWithOverride(template, userPrompt, reasoning) {
47
+ const trimmedTemplate = template.trim();
48
+ if (!trimmedTemplate)
49
+ return buildSummaryPrompt(userPrompt, reasoning);
50
+ const clipped = clipText(userPrompt.trim(), 1200);
51
+ const clippedReasoning = reasoning?.trim() ? clipText(reasoning, REASONING_MAX_CHARS) : '';
52
+ const hasMessage = trimmedTemplate.includes('{{message}}');
53
+ const hasReasoning = trimmedTemplate.includes('{{reasoning}}');
54
+ let prompt = trimmedTemplate;
55
+ if (hasMessage)
56
+ prompt = prompt.replaceAll('{{message}}', clipped);
57
+ if (hasReasoning)
58
+ prompt = prompt.replaceAll('{{reasoning}}', clippedReasoning);
59
+ if (!hasMessage) {
60
+ prompt = `${prompt}\n\nUser request:\n${clipped}`;
61
+ }
62
+ if (clippedReasoning && !hasReasoning) {
63
+ prompt = `${prompt}\n\nInitial reasoning (first lines):\n${clippedReasoning}`;
64
+ }
65
+ return prompt;
66
+ }
49
67
  export function sanitizeSummary(raw) {
50
- const normalized = raw.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim();
68
+ const normalized = raw
69
+ .replace(/[\r\n]+/g, ' ')
70
+ .replace(/\s+/g, ' ')
71
+ .trim();
51
72
  const stripped = normalized.replace(/^["']+|["']+$/g, '').trim();
52
73
  const cleaned = stripped.replace(/[.!?]+$/g, '').trim();
53
74
  if (!cleaned)
@@ -107,72 +128,3 @@ export async function runSummaryPrompt(options) {
107
128
  }
108
129
  return attempt(null);
109
130
  }
110
- function buildClaudeProjectKey(basePath) {
111
- const resolved = path.resolve(basePath);
112
- const normalized = resolved.split(path.sep).filter(Boolean).join('-');
113
- return `-${normalized}`;
114
- }
115
- function resolveClaudeSummaryPath(basePath, sessionId) {
116
- if (!sessionId)
117
- return null;
118
- const root = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
119
- const projectKey = buildClaudeProjectKey(basePath);
120
- return path.join(root, 'projects', projectKey, `${sessionId}.jsonl`);
121
- }
122
- async function readClaudeSummaryFile(filePath) {
123
- if (!fs.existsSync(filePath))
124
- return null;
125
- try {
126
- const raw = await fsp.readFile(filePath, 'utf8');
127
- let latest = null;
128
- for (const line of raw.split(/\r?\n/)) {
129
- if (!line.trim())
130
- continue;
131
- let parsed;
132
- try {
133
- parsed = JSON.parse(line);
134
- }
135
- catch {
136
- continue;
137
- }
138
- if (!parsed || typeof parsed !== 'object')
139
- continue;
140
- const record = parsed;
141
- if (record.type !== 'summary')
142
- continue;
143
- const summary = record.summary;
144
- if (typeof summary === 'string' && summary.trim()) {
145
- latest = summary.trim();
146
- }
147
- }
148
- return latest ? sanitizeSummary(latest) : null;
149
- }
150
- catch {
151
- return null;
152
- }
153
- }
154
- export async function pollClaudeSummary(options) {
155
- const { basePath, sessionId, timeoutMs = 30000, intervalMs = 1500 } = options;
156
- const filePath = resolveClaudeSummaryPath(basePath, sessionId);
157
- if (!filePath)
158
- return null;
159
- const start = Date.now();
160
- let lastMtime = 0;
161
- while (Date.now() - start < timeoutMs) {
162
- try {
163
- const stat = await fsp.stat(filePath);
164
- const mtime = stat.mtimeMs;
165
- if (mtime !== lastMtime) {
166
- lastMtime = mtime;
167
- const summary = await readClaudeSummaryFile(filePath);
168
- if (summary)
169
- return summary;
170
- }
171
- }
172
- catch {
173
- // ignore
174
- }
175
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
176
- }
177
- return null;
178
- }
package/dist/types.d.ts CHANGED
@@ -116,6 +116,20 @@ export interface ModelInfo {
116
116
  reasoningEfforts?: ReasoningEffort[];
117
117
  defaultReasoningEffort?: string;
118
118
  }
119
+ export type TokenUsage = {
120
+ input_tokens?: number;
121
+ output_tokens?: number;
122
+ total_tokens?: number;
123
+ cached_input_tokens?: number;
124
+ reasoning_tokens?: number;
125
+ };
126
+ export type ContextUsage = {
127
+ context_window?: number;
128
+ context_tokens?: number;
129
+ context_cached_tokens?: number;
130
+ context_remaining_tokens?: number;
131
+ context_truncated?: boolean;
132
+ };
119
133
  export interface ProviderLoginOptions {
120
134
  baseUrl?: string;
121
135
  apiKey?: string;
@@ -125,15 +139,15 @@ export interface ProviderLoginOptions {
125
139
  loginExperience?: 'embedded' | 'terminal';
126
140
  }
127
141
  export interface SessionEvent {
128
- type: 'delta' | 'final' | 'usage' | 'status' | 'error' | 'raw_line' | 'message' | 'thinking' | 'tool_call' | 'detail' | 'summary';
142
+ type: 'delta' | 'final' | 'usage' | 'context_usage' | 'status' | 'error' | 'raw_line' | 'message' | 'thinking' | 'tool_call' | 'detail' | 'summary';
129
143
  text?: string;
130
144
  message?: string;
131
145
  line?: string;
132
146
  provider?: ProviderId;
133
147
  providerDetail?: ProviderDetail;
134
148
  providerSessionId?: string | null;
135
- inputTokens?: number;
136
- outputTokens?: number;
149
+ usage?: TokenUsage;
150
+ contextUsage?: ContextUsage;
137
151
  role?: 'system' | 'user' | 'assistant';
138
152
  content?: string;
139
153
  contentParts?: unknown;
@@ -146,12 +160,13 @@ export interface SessionEvent {
146
160
  timestampMs?: number;
147
161
  cancelled?: boolean;
148
162
  summary?: string;
149
- source?: 'prompt' | 'claude-log';
163
+ source?: 'prompt';
150
164
  model?: string | null;
151
165
  createdAt?: string;
152
166
  }
153
167
  export interface RunPromptOptions {
154
168
  prompt: string;
169
+ system?: string;
155
170
  resumeSessionId?: string | null;
156
171
  model?: string;
157
172
  reasoningEffort?: string | null;
@@ -193,12 +208,17 @@ export interface SessionState {
193
208
  cwd?: string;
194
209
  repoRoot?: string;
195
210
  providerDetailLevel?: ProviderDetailLevel;
211
+ systemPrompt?: string;
212
+ summaryMode?: 'auto' | 'off';
213
+ summaryPrompt?: string;
214
+ summaryNextMode?: 'auto' | 'off' | 'force';
215
+ summaryNextPrompt?: string;
216
+ summaryAutoUsed?: boolean;
196
217
  summaryRequested?: boolean;
197
- claudeSummaryWatch?: boolean;
198
218
  summarySeed?: string;
199
219
  summaryReasoning?: string;
200
220
  summary?: string | null;
201
- summarySource?: 'prompt' | 'claude-log';
221
+ summarySource?: 'prompt';
202
222
  summaryModel?: string | null;
203
223
  summaryCreatedAt?: string;
204
224
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentconnect/host",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/rayzhudev/agent-connect",