ctx-switch 2.0.4 → 2.0.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.
- package/dist/index.mjs +299 -45
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import path from "node:path";
|
|
|
14
14
|
// src/types.ts
|
|
15
15
|
var SUPPORTED_COMMANDS = ["continue", "doctor", "sessions"];
|
|
16
16
|
var SUPPORTED_PROVIDERS = ["openrouter"];
|
|
17
|
-
var SUPPORTED_TARGETS = ["generic", "codex", "cursor", "chatgpt"];
|
|
17
|
+
var SUPPORTED_TARGETS = ["generic", "claude", "codex", "cursor", "chatgpt"];
|
|
18
18
|
var SUPPORTED_SOURCES = ["claude", "codex", "opencode"];
|
|
19
19
|
|
|
20
20
|
// src/args.ts
|
|
@@ -142,7 +142,7 @@ function getHelpText({ name, version }) {
|
|
|
142
142
|
" -o, --output <file> Write the final prompt to a file",
|
|
143
143
|
" --source <name> Session source: claude, codex, opencode (interactive if omitted)",
|
|
144
144
|
" --session <id|path> Use a specific session file or session id",
|
|
145
|
-
" --target <name> Prompt target: generic, codex, cursor, chatgpt",
|
|
145
|
+
" --target <name> Prompt target: generic, claude, codex, cursor, chatgpt",
|
|
146
146
|
" -n, --limit <count> Limit rows for the sessions command (default: 10)",
|
|
147
147
|
"",
|
|
148
148
|
"Refinement (optional)",
|
|
@@ -160,6 +160,7 @@ function getHelpText({ name, version }) {
|
|
|
160
160
|
"Examples",
|
|
161
161
|
` ${name} # interactive source picker`,
|
|
162
162
|
` ${name} --source claude # use Claude Code sessions`,
|
|
163
|
+
` ${name} --source codex --target claude`,
|
|
163
164
|
` ${name} --source codex --target codex`,
|
|
164
165
|
` ${name} --source opencode`,
|
|
165
166
|
` ${name} --refine --model openrouter/free`,
|
|
@@ -600,6 +601,60 @@ import path5 from "node:path";
|
|
|
600
601
|
var CODEX_DIR = path5.join(os3.homedir(), ".codex");
|
|
601
602
|
var CODEX_SESSIONS_DIR = path5.join(CODEX_DIR, "sessions");
|
|
602
603
|
var CODEX_INDEX_PATH = path5.join(CODEX_DIR, "session_index.jsonl");
|
|
604
|
+
function parseJsonRecord(line) {
|
|
605
|
+
try {
|
|
606
|
+
return JSON.parse(line);
|
|
607
|
+
} catch {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function parseFunctionArguments(raw) {
|
|
612
|
+
if (!raw) return {};
|
|
613
|
+
try {
|
|
614
|
+
const parsed = JSON.parse(raw);
|
|
615
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
616
|
+
} catch {
|
|
617
|
+
return {};
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function parseCustomToolInput(toolName, raw) {
|
|
621
|
+
if (!raw) return {};
|
|
622
|
+
if (typeof raw !== "string") return raw;
|
|
623
|
+
const trimmed = raw.trim();
|
|
624
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
625
|
+
try {
|
|
626
|
+
const parsed = JSON.parse(trimmed);
|
|
627
|
+
if (parsed && typeof parsed === "object") {
|
|
628
|
+
return parsed;
|
|
629
|
+
}
|
|
630
|
+
} catch {
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (toolName === "apply_patch") {
|
|
634
|
+
return { patch: raw };
|
|
635
|
+
}
|
|
636
|
+
return { input: raw };
|
|
637
|
+
}
|
|
638
|
+
function parseToolOutput(raw) {
|
|
639
|
+
if (!raw) return { text: "" };
|
|
640
|
+
let text = raw;
|
|
641
|
+
let exitCode;
|
|
642
|
+
try {
|
|
643
|
+
const parsed = JSON.parse(raw);
|
|
644
|
+
if (typeof parsed.output === "string") {
|
|
645
|
+
text = parsed.output;
|
|
646
|
+
}
|
|
647
|
+
if (typeof parsed.metadata?.exit_code === "number") {
|
|
648
|
+
exitCode = parsed.metadata.exit_code;
|
|
649
|
+
}
|
|
650
|
+
} catch {
|
|
651
|
+
}
|
|
652
|
+
const isError = exitCode !== void 0 ? exitCode !== 0 : /Process exited with code [^0]/.test(text) || /error|Error|ERROR/.test(text.slice(0, 200));
|
|
653
|
+
return {
|
|
654
|
+
text: text.slice(0, 1500),
|
|
655
|
+
isError: isError || void 0
|
|
656
|
+
};
|
|
657
|
+
}
|
|
603
658
|
function listSessionsForProject2(cwd) {
|
|
604
659
|
if (!fs4.existsSync(CODEX_SESSIONS_DIR)) {
|
|
605
660
|
return [];
|
|
@@ -680,12 +735,8 @@ function parseSession2(sessionPath) {
|
|
|
680
735
|
}
|
|
681
736
|
}
|
|
682
737
|
for (const line of lines) {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
record = JSON.parse(line);
|
|
686
|
-
} catch {
|
|
687
|
-
continue;
|
|
688
|
-
}
|
|
738
|
+
const record = parseJsonRecord(line);
|
|
739
|
+
if (!record) continue;
|
|
689
740
|
if (record.type === "session_meta") {
|
|
690
741
|
const payload2 = record.payload;
|
|
691
742
|
if (payload2.cwd) meta.cwd = payload2.cwd;
|
|
@@ -693,6 +744,21 @@ function parseSession2(sessionPath) {
|
|
|
693
744
|
if (payload2.id) meta.sessionId = payload2.id;
|
|
694
745
|
continue;
|
|
695
746
|
}
|
|
747
|
+
if (record.type === "event_msg" && record.payload.type === "exec_command_end" && record.payload.call_id) {
|
|
748
|
+
const tc = pendingCalls.get(record.payload.call_id);
|
|
749
|
+
if (tc) {
|
|
750
|
+
if (Array.isArray(record.payload.parsed_cmd)) {
|
|
751
|
+
tc.input.parsed_cmd = record.payload.parsed_cmd;
|
|
752
|
+
}
|
|
753
|
+
if (typeof record.payload.aggregated_output === "string" && !tc.result) {
|
|
754
|
+
tc.result = record.payload.aggregated_output.slice(0, 1500);
|
|
755
|
+
}
|
|
756
|
+
if (typeof record.payload.exit_code === "number") {
|
|
757
|
+
tc.isError = record.payload.exit_code !== 0;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
696
762
|
if (record.type !== "response_item") continue;
|
|
697
763
|
const payload = record.payload;
|
|
698
764
|
const payloadType = payload.type;
|
|
@@ -718,11 +784,7 @@ function parseSession2(sessionPath) {
|
|
|
718
784
|
continue;
|
|
719
785
|
}
|
|
720
786
|
if (payloadType === "function_call" && payload.name) {
|
|
721
|
-
|
|
722
|
-
try {
|
|
723
|
-
input = JSON.parse(payload.arguments || "{}");
|
|
724
|
-
} catch {
|
|
725
|
-
}
|
|
787
|
+
const input = parseFunctionArguments(payload.arguments);
|
|
726
788
|
const tc = {
|
|
727
789
|
id: payload.call_id || null,
|
|
728
790
|
tool: payload.name,
|
|
@@ -734,12 +796,26 @@ function parseSession2(sessionPath) {
|
|
|
734
796
|
}
|
|
735
797
|
continue;
|
|
736
798
|
}
|
|
737
|
-
if (payloadType === "
|
|
799
|
+
if (payloadType === "custom_tool_call" && payload.name) {
|
|
800
|
+
const tc = {
|
|
801
|
+
id: payload.call_id || null,
|
|
802
|
+
tool: payload.name,
|
|
803
|
+
input: parseCustomToolInput(payload.name, payload.input)
|
|
804
|
+
};
|
|
805
|
+
currentToolCalls.push(tc);
|
|
806
|
+
if (payload.call_id) {
|
|
807
|
+
pendingCalls.set(payload.call_id, tc);
|
|
808
|
+
}
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if ((payloadType === "function_call_output" || payloadType === "custom_tool_call_output") && payload.call_id) {
|
|
738
812
|
const tc = pendingCalls.get(payload.call_id);
|
|
739
813
|
if (tc) {
|
|
740
|
-
const
|
|
741
|
-
tc.result =
|
|
742
|
-
|
|
814
|
+
const parsed = parseToolOutput(payload.output);
|
|
815
|
+
tc.result = parsed.text;
|
|
816
|
+
if (parsed.isError !== void 0) {
|
|
817
|
+
tc.isError = parsed.isError;
|
|
818
|
+
}
|
|
743
819
|
}
|
|
744
820
|
continue;
|
|
745
821
|
}
|
|
@@ -950,6 +1026,20 @@ function extractCommand(input) {
|
|
|
950
1026
|
const value = input.command || input.cmd;
|
|
951
1027
|
return typeof value === "string" ? value : null;
|
|
952
1028
|
}
|
|
1029
|
+
function resolveSessionFilePath(filePath, cwd) {
|
|
1030
|
+
return path7.isAbsolute(filePath) ? filePath : path7.resolve(cwd, filePath);
|
|
1031
|
+
}
|
|
1032
|
+
function extractPatchFilePaths(input, cwd) {
|
|
1033
|
+
const patch = input.patch;
|
|
1034
|
+
if (typeof patch !== "string") return [];
|
|
1035
|
+
const matches = [...patch.matchAll(/^\*\*\* (?:Update|Add|Delete) File: (.+)$/gm)];
|
|
1036
|
+
return matches.map((match) => resolveSessionFilePath(match[1].trim(), cwd));
|
|
1037
|
+
}
|
|
1038
|
+
function extractParsedCommands(input) {
|
|
1039
|
+
const parsed = input.parsed_cmd;
|
|
1040
|
+
if (!Array.isArray(parsed)) return [];
|
|
1041
|
+
return parsed.filter((entry) => Boolean(entry) && typeof entry === "object");
|
|
1042
|
+
}
|
|
953
1043
|
function buildSessionContext({
|
|
954
1044
|
messages,
|
|
955
1045
|
meta,
|
|
@@ -967,10 +1057,19 @@ function buildSessionContext({
|
|
|
967
1057
|
const toolName = String(toolCall.tool || "").toLowerCase();
|
|
968
1058
|
const filePath = extractFilePath(toolCall.input);
|
|
969
1059
|
const command = extractCommand(toolCall.input);
|
|
1060
|
+
const patchPaths = extractPatchFilePaths(toolCall.input, cwd);
|
|
1061
|
+
const parsedCommands = extractParsedCommands(toolCall.input);
|
|
970
1062
|
if (filePath && /(edit|write|create|multi_edit)/.test(toolName)) {
|
|
971
|
-
filesModified.add(filePath);
|
|
1063
|
+
filesModified.add(resolveSessionFilePath(filePath, cwd));
|
|
972
1064
|
} else if (filePath && /(read|grep|glob|search)/.test(toolName)) {
|
|
973
|
-
filesRead.add(filePath);
|
|
1065
|
+
filesRead.add(resolveSessionFilePath(filePath, cwd));
|
|
1066
|
+
}
|
|
1067
|
+
for (const patchPath of patchPaths) {
|
|
1068
|
+
filesModified.add(patchPath);
|
|
1069
|
+
}
|
|
1070
|
+
for (const parsedCommand of parsedCommands) {
|
|
1071
|
+
if (parsedCommand.type !== "read" || typeof parsedCommand.path !== "string") continue;
|
|
1072
|
+
filesRead.add(resolveSessionFilePath(parsedCommand.path, cwd));
|
|
974
1073
|
}
|
|
975
1074
|
if (command && /(bash|command|run|exec_command)/.test(toolName)) {
|
|
976
1075
|
commands.push(command);
|
|
@@ -985,8 +1084,10 @@ function buildSessionContext({
|
|
|
985
1084
|
const toolSummary = message.toolCalls.map((toolCall) => {
|
|
986
1085
|
const filePath = extractFilePath(toolCall.input);
|
|
987
1086
|
const command = extractCommand(toolCall.input);
|
|
1087
|
+
const patchPaths = extractPatchFilePaths(toolCall.input, cwd);
|
|
988
1088
|
let summary = "";
|
|
989
|
-
if (filePath) summary = `${toolCall.tool} ${filePath}`;
|
|
1089
|
+
if (filePath) summary = `${toolCall.tool} ${resolveSessionFilePath(filePath, cwd)}`;
|
|
1090
|
+
else if (patchPaths.length > 0) summary = `${toolCall.tool} ${patchPaths.join(", ")}`;
|
|
990
1091
|
else if (command) summary = `${toolCall.tool}: ${command}`;
|
|
991
1092
|
else summary = toolCall.tool;
|
|
992
1093
|
if (toolCall.isError && toolCall.result) {
|
|
@@ -1156,8 +1257,18 @@ function compactText(text, maxChars = 800) {
|
|
|
1156
1257
|
function unique(list) {
|
|
1157
1258
|
return [...new Set(list.filter(Boolean))];
|
|
1158
1259
|
}
|
|
1260
|
+
function extractFilePath2(input) {
|
|
1261
|
+
const value = input.file_path || input.path || input.target_file || input.filePath;
|
|
1262
|
+
return typeof value === "string" ? value : null;
|
|
1263
|
+
}
|
|
1264
|
+
function extractCommand2(input) {
|
|
1265
|
+
const value = input.command || input.cmd;
|
|
1266
|
+
return typeof value === "string" ? value : null;
|
|
1267
|
+
}
|
|
1159
1268
|
function buildTargetGuidance(target) {
|
|
1160
1269
|
switch (target) {
|
|
1270
|
+
case "claude":
|
|
1271
|
+
return "The next agent is Claude Code. It should read the active files first, trust the current workspace and git diff over stale transcript details, and continue the implementation or debugging directly.";
|
|
1161
1272
|
case "codex":
|
|
1162
1273
|
return "The next agent is Codex. It should inspect the current files first, avoid redoing completed work, and finish any remaining implementation or verification.";
|
|
1163
1274
|
case "cursor":
|
|
@@ -1165,19 +1276,20 @@ function buildTargetGuidance(target) {
|
|
|
1165
1276
|
case "chatgpt":
|
|
1166
1277
|
return "The next agent is ChatGPT. It should reason from the current workspace state, explain what remains, and provide explicit next actions.";
|
|
1167
1278
|
default:
|
|
1168
|
-
return "The next agent should
|
|
1279
|
+
return "The next agent should read the active files first, trust the current workspace and git diff over stale transcript details, continue the interrupted work directly, and avoid redoing completed steps.";
|
|
1169
1280
|
}
|
|
1170
1281
|
}
|
|
1171
1282
|
function isNoiseMessage(text) {
|
|
1172
1283
|
const trimmed = text.trim().toLowerCase();
|
|
1284
|
+
const normalized = trimmed.replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
|
|
1173
1285
|
if (trimmed.length < 5) return true;
|
|
1174
1286
|
const noise = [
|
|
1175
1287
|
"yes",
|
|
1288
|
+
"yes please",
|
|
1176
1289
|
"no",
|
|
1177
1290
|
"ok",
|
|
1178
1291
|
"okay",
|
|
1179
1292
|
"try",
|
|
1180
|
-
"try?",
|
|
1181
1293
|
"sure",
|
|
1182
1294
|
"do it",
|
|
1183
1295
|
"go ahead",
|
|
@@ -1194,11 +1306,16 @@ function isNoiseMessage(text) {
|
|
|
1194
1306
|
"try turn off thinking",
|
|
1195
1307
|
"try without timeout"
|
|
1196
1308
|
];
|
|
1197
|
-
if (noise.includes(
|
|
1198
|
-
if (/^(try|yes|ok|sure|test|run)\s/i.test(
|
|
1309
|
+
if (noise.includes(normalized)) return true;
|
|
1310
|
+
if (/^(try|yes|ok|sure|test|run)\s/i.test(normalized) && normalized.length < 40) return true;
|
|
1199
1311
|
if (trimmed.startsWith("[request interrupted")) return true;
|
|
1200
1312
|
return false;
|
|
1201
1313
|
}
|
|
1314
|
+
function isReferentialMessage(text) {
|
|
1315
|
+
const normalized = text.trim().toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
|
|
1316
|
+
if (!normalized || normalized.length > 220) return false;
|
|
1317
|
+
return /^(ok|okay|alright|now|so)\b/.test(normalized) || /\b(it|that|again|better|same|continue|still|also|another|more)\b/.test(normalized);
|
|
1318
|
+
}
|
|
1202
1319
|
function filterUserMessages(messages) {
|
|
1203
1320
|
const all = messages.filter((m) => m.role === "user" && m.content).map((m) => m.content.trim());
|
|
1204
1321
|
if (all.length <= 2) return all;
|
|
@@ -1228,30 +1345,140 @@ function extractKeyDecisions(messages) {
|
|
|
1228
1345
|
for (const msg of messages) {
|
|
1229
1346
|
if (msg.role !== "assistant" || !msg.content) continue;
|
|
1230
1347
|
const lower = msg.content.toLowerCase();
|
|
1231
|
-
if (
|
|
1348
|
+
if (/\b(handoff|prompt)\b/.test(lower) && /\b(good|bad|better|worse|quality)\b/.test(lower)) {
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
if (/\b(root cause|the issue is|the problem is|caused by|failed because|failing because|need to)\b/.test(lower) || /\b(exposed|revealed|showed)\b.*\b(gap|issue|problem|bug)\b/.test(lower) || /\bmissing\b/.test(lower)) {
|
|
1232
1352
|
decisions.push(compactText(msg.content, 300));
|
|
1233
1353
|
}
|
|
1234
1354
|
}
|
|
1235
1355
|
return decisions.slice(-5);
|
|
1236
1356
|
}
|
|
1357
|
+
function findFocusedWindow(messages) {
|
|
1358
|
+
if (messages.length === 0) {
|
|
1359
|
+
return { messages, sessionAppearsComplete: false };
|
|
1360
|
+
}
|
|
1361
|
+
const substantiveUserIndexes = messages.map((message, index) => ({ message, index })).filter(({ message }) => message.role === "user" && message.content && !isNoiseMessage(message.content)).map(({ index }) => index);
|
|
1362
|
+
if (substantiveUserIndexes.length === 0) {
|
|
1363
|
+
return { messages, sessionAppearsComplete: false };
|
|
1364
|
+
}
|
|
1365
|
+
const lastToolIndex = messages.reduce(
|
|
1366
|
+
(last, message, index) => message.role === "assistant" && message.toolCalls.length > 0 ? index : last,
|
|
1367
|
+
-1
|
|
1368
|
+
);
|
|
1369
|
+
const postToolUsers = substantiveUserIndexes.filter((index) => index > lastToolIndex);
|
|
1370
|
+
let startIndex = 0;
|
|
1371
|
+
if (postToolUsers.length > 0) {
|
|
1372
|
+
startIndex = postToolUsers[0];
|
|
1373
|
+
} else if (lastToolIndex >= 0) {
|
|
1374
|
+
startIndex = substantiveUserIndexes.filter((index) => index <= lastToolIndex).at(-1) ?? 0;
|
|
1375
|
+
} else {
|
|
1376
|
+
startIndex = substantiveUserIndexes.at(-1) ?? 0;
|
|
1377
|
+
}
|
|
1378
|
+
const startMessage = messages[startIndex];
|
|
1379
|
+
if (startMessage?.role === "user" && isReferentialMessage(startMessage.content)) {
|
|
1380
|
+
const previousSubstantive = substantiveUserIndexes.filter((index) => index < startIndex).at(-1);
|
|
1381
|
+
if (typeof previousSubstantive === "number") {
|
|
1382
|
+
startIndex = previousSubstantive;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
const focused = messages.slice(startIndex);
|
|
1386
|
+
const hasToolActivity = focused.some((message) => message.role === "assistant" && message.toolCalls.length > 0);
|
|
1387
|
+
const lastMessage = focused.at(-1);
|
|
1388
|
+
const sessionAppearsComplete = Boolean(lastMessage) && lastMessage.role === "assistant" && lastMessage.toolCalls.length === 0 && !hasToolActivity;
|
|
1389
|
+
return { messages: focused, sessionAppearsComplete };
|
|
1390
|
+
}
|
|
1391
|
+
function extractWorkSummary(messages) {
|
|
1392
|
+
const filesModified = /* @__PURE__ */ new Set();
|
|
1393
|
+
const commands = [];
|
|
1394
|
+
for (const message of messages) {
|
|
1395
|
+
if (message.role !== "assistant" || message.toolCalls.length === 0) continue;
|
|
1396
|
+
for (const toolCall of message.toolCalls) {
|
|
1397
|
+
const toolName = String(toolCall.tool || "").toLowerCase();
|
|
1398
|
+
const filePath = extractFilePath2(toolCall.input);
|
|
1399
|
+
const command = extractCommand2(toolCall.input);
|
|
1400
|
+
if (filePath && /(edit|write|create|multi_edit)/.test(toolName)) {
|
|
1401
|
+
filesModified.add(filePath);
|
|
1402
|
+
}
|
|
1403
|
+
if (command && /(bash|command|run|exec_command)/.test(toolName)) {
|
|
1404
|
+
commands.push(command);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
return {
|
|
1409
|
+
filesModified: [...filesModified],
|
|
1410
|
+
commands
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
function extractLastAssistantAnswer(messages) {
|
|
1414
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1415
|
+
const message = messages[i];
|
|
1416
|
+
if (message.role === "assistant" && message.content.trim()) {
|
|
1417
|
+
return compactText(message.content, 500);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
return null;
|
|
1421
|
+
}
|
|
1422
|
+
function summarizeCommand(command) {
|
|
1423
|
+
return compactText(command.replace(/\s+/g, " ").trim(), 140);
|
|
1424
|
+
}
|
|
1425
|
+
function extractRecentCommands(commands) {
|
|
1426
|
+
return unique(commands.map(summarizeCommand)).slice(-6);
|
|
1427
|
+
}
|
|
1428
|
+
function extractStatusPaths(status) {
|
|
1429
|
+
if (!status) return [];
|
|
1430
|
+
return status.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
1431
|
+
const renamed = line.includes(" -> ") ? line.split(" -> ").at(-1)?.trim() || "" : "";
|
|
1432
|
+
if (renamed) {
|
|
1433
|
+
return renamed.replace(/^(?:\?\?|[A-Z?!]{1,2})\s+/, "");
|
|
1434
|
+
}
|
|
1435
|
+
const match = line.match(/^(?:\?\?|[A-Z?!]{1,2})\s+(.*)$/);
|
|
1436
|
+
return match?.[1]?.trim() || line;
|
|
1437
|
+
}).filter(Boolean);
|
|
1438
|
+
}
|
|
1439
|
+
function extractFocusFiles(ctx, work) {
|
|
1440
|
+
return unique([
|
|
1441
|
+
...work.filesModified,
|
|
1442
|
+
...extractStatusPaths(ctx.gitContext.status),
|
|
1443
|
+
...ctx.gitContext.untracked.map((file) => file.path)
|
|
1444
|
+
]).slice(0, 6);
|
|
1445
|
+
}
|
|
1237
1446
|
function buildRawPrompt(ctx, options = {}) {
|
|
1238
|
-
const
|
|
1239
|
-
const
|
|
1240
|
-
const
|
|
1447
|
+
const focused = findFocusedWindow(ctx.messages);
|
|
1448
|
+
const userMessages = filterUserMessages(focused.messages);
|
|
1449
|
+
const errors = extractUnresolvedErrors(focused.messages);
|
|
1450
|
+
const decisions = extractKeyDecisions(focused.messages);
|
|
1451
|
+
const work = extractWorkSummary(focused.messages);
|
|
1452
|
+
const focusFiles = extractFocusFiles(ctx, work);
|
|
1453
|
+
const recentCommands = extractRecentCommands(work.commands);
|
|
1454
|
+
const lastAssistantAnswer = extractLastAssistantAnswer(focused.messages);
|
|
1241
1455
|
let prompt = "";
|
|
1242
1456
|
prompt += "# Task\n\n";
|
|
1243
1457
|
prompt += `Project: \`${ctx.sessionCwd}\`
|
|
1244
1458
|
`;
|
|
1245
1459
|
if (ctx.branch) prompt += `Branch: \`${ctx.branch}\`
|
|
1246
1460
|
`;
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1461
|
+
if (focused.sessionAppearsComplete) {
|
|
1462
|
+
prompt += "\nThe latest exchange in this session appears complete. ";
|
|
1463
|
+
prompt += "Use the focused context below only if the user wants to continue from that point.\n\n";
|
|
1464
|
+
} else {
|
|
1465
|
+
prompt += "\nThis is a continuation of an interrupted AI coding session. ";
|
|
1466
|
+
prompt += "The previous agent was working on the task below. Pick up where it left off.\n\n";
|
|
1467
|
+
}
|
|
1468
|
+
prompt += `## What The User Asked (${focused.sessionAppearsComplete ? "recent focus" : "chronological"})
|
|
1469
|
+
|
|
1470
|
+
`;
|
|
1250
1471
|
for (const msg of userMessages) {
|
|
1251
1472
|
prompt += `- ${compactText(msg, 500)}
|
|
1252
1473
|
`;
|
|
1253
1474
|
}
|
|
1254
1475
|
prompt += "\n";
|
|
1476
|
+
if (focused.sessionAppearsComplete && lastAssistantAnswer) {
|
|
1477
|
+
prompt += "## Last Answer Already Given\n\n";
|
|
1478
|
+
prompt += `- ${lastAssistantAnswer}
|
|
1479
|
+
|
|
1480
|
+
`;
|
|
1481
|
+
}
|
|
1255
1482
|
if (errors.length > 0) {
|
|
1256
1483
|
prompt += "## DO NOT REPEAT \u2014 Unresolved Errors\n\n";
|
|
1257
1484
|
prompt += "These errors occurred and were NOT fixed. Avoid the same approaches.\n\n";
|
|
@@ -1269,27 +1496,45 @@ function buildRawPrompt(ctx, options = {}) {
|
|
|
1269
1496
|
}
|
|
1270
1497
|
prompt += "\n";
|
|
1271
1498
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1499
|
+
if (work.filesModified.length > 0) {
|
|
1500
|
+
prompt += "## Work Already Completed\n\n";
|
|
1501
|
+
}
|
|
1502
|
+
if (work.filesModified.length > 0) {
|
|
1274
1503
|
prompt += "**Files modified:**\n";
|
|
1275
|
-
for (const filePath of unique(
|
|
1504
|
+
for (const filePath of unique(work.filesModified)) {
|
|
1276
1505
|
prompt += `- \`${filePath}\`
|
|
1277
1506
|
`;
|
|
1278
1507
|
}
|
|
1279
1508
|
prompt += "\n";
|
|
1280
1509
|
}
|
|
1281
|
-
if (ctx.gitContext.recentCommits) {
|
|
1510
|
+
if (!focused.sessionAppearsComplete && work.filesModified.length > 0 && ctx.gitContext.recentCommits) {
|
|
1282
1511
|
prompt += "**Recent commits:**\n```\n";
|
|
1283
1512
|
prompt += `${ctx.gitContext.recentCommits}
|
|
1284
1513
|
`;
|
|
1285
1514
|
prompt += "```\n\n";
|
|
1286
1515
|
}
|
|
1287
|
-
if (ctx.gitContext.committedDiff) {
|
|
1516
|
+
if (!focused.sessionAppearsComplete && work.filesModified.length > 0 && ctx.gitContext.committedDiff) {
|
|
1288
1517
|
prompt += "**Files changed in recent commits:**\n```\n";
|
|
1289
1518
|
prompt += `${ctx.gitContext.committedDiff}
|
|
1290
1519
|
`;
|
|
1291
1520
|
prompt += "```\n\n";
|
|
1292
1521
|
}
|
|
1522
|
+
if (recentCommands.length > 0) {
|
|
1523
|
+
prompt += "## Recent Commands / Checks\n\n";
|
|
1524
|
+
for (const command of recentCommands) {
|
|
1525
|
+
prompt += `- \`${command}\`
|
|
1526
|
+
`;
|
|
1527
|
+
}
|
|
1528
|
+
prompt += "\n";
|
|
1529
|
+
}
|
|
1530
|
+
if (focusFiles.length > 0) {
|
|
1531
|
+
prompt += "## Read These Files First\n\n";
|
|
1532
|
+
for (const filePath of focusFiles) {
|
|
1533
|
+
prompt += `- \`${filePath}\`
|
|
1534
|
+
`;
|
|
1535
|
+
}
|
|
1536
|
+
prompt += "\n";
|
|
1537
|
+
}
|
|
1293
1538
|
const git = ctx.gitContext;
|
|
1294
1539
|
if (git.isGitRepo && git.hasChanges) {
|
|
1295
1540
|
prompt += "## Uncommitted Changes\n\n";
|
|
@@ -1317,19 +1562,28 @@ function buildRawPrompt(ctx, options = {}) {
|
|
|
1317
1562
|
}
|
|
1318
1563
|
}
|
|
1319
1564
|
prompt += "## Your Instructions\n\n";
|
|
1320
|
-
|
|
1565
|
+
if (focused.sessionAppearsComplete) {
|
|
1566
|
+
prompt += "The latest thread appears finished. Do not resume older tasks unless the user explicitly asks for them.\n\n";
|
|
1567
|
+
prompt += "1. **Start from the recent focus above** \u2014 ignore stale history unless the user points back to it.\n";
|
|
1568
|
+
prompt += "2. **Use the last answer as prior context** \u2014 avoid restating or redoing already completed work.\n";
|
|
1569
|
+
prompt += `3. **Inspect the workspace only as needed** \u2014 respond to follow-up questions or new work from the current repo state${focusFiles.length > 0 ? `, starting with ${focusFiles.map((filePath) => `\`${filePath}\``).join(", ")}` : ""}.
|
|
1570
|
+
`;
|
|
1571
|
+
} else {
|
|
1572
|
+
prompt += `${buildTargetGuidance(options.target)}
|
|
1321
1573
|
|
|
1322
1574
|
`;
|
|
1323
|
-
|
|
1324
|
-
if (errors.length > 0) {
|
|
1325
|
-
prompt += "2. **Check the errors above** \u2014 do NOT repeat failed approaches. Try a different strategy.\n";
|
|
1326
|
-
}
|
|
1327
|
-
prompt += `${errors.length > 0 ? "3" : "2"}. **Identify what's done vs what remains** \u2014 the commits and modified files above show completed work.
|
|
1575
|
+
prompt += `1. **Read the active files first** \u2014 verify their current state before changing anything${focusFiles.length > 0 ? `: ${focusFiles.map((filePath) => `\`${filePath}\``).join(", ")}` : ""}.
|
|
1328
1576
|
`;
|
|
1329
|
-
|
|
1577
|
+
if (errors.length > 0) {
|
|
1578
|
+
prompt += "2. **Check the errors above** \u2014 do NOT repeat failed approaches. Try a different strategy.\n";
|
|
1579
|
+
}
|
|
1580
|
+
prompt += `${errors.length > 0 ? "3" : "2"}. **Identify what's done vs what remains** \u2014 use the recent commands, active files, and git state above as the source of truth for the current thread.
|
|
1581
|
+
`;
|
|
1582
|
+
prompt += `${errors.length > 0 ? "4" : "3"}. **Do the remaining work** \u2014 pick up exactly where the previous agent stopped.
|
|
1330
1583
|
`;
|
|
1331
|
-
|
|
1584
|
+
prompt += `${errors.length > 0 ? "5" : "4"}. **Verify** \u2014 rerun or extend the relevant commands/checks above to confirm everything works.
|
|
1332
1585
|
`;
|
|
1586
|
+
}
|
|
1333
1587
|
return prompt;
|
|
1334
1588
|
}
|
|
1335
1589
|
function buildRefinementDump(ctx, options = {}) {
|
|
@@ -1828,7 +2082,7 @@ async function promptForSource() {
|
|
|
1828
2082
|
}
|
|
1829
2083
|
async function main(argv = process.argv.slice(2)) {
|
|
1830
2084
|
const options = parseArgs(argv);
|
|
1831
|
-
const pkgInfo = { name: "ctx-switch", version: "2.0.
|
|
2085
|
+
const pkgInfo = { name: "ctx-switch", version: "2.0.5" };
|
|
1832
2086
|
const ui = createTheme(process.stderr);
|
|
1833
2087
|
if (options.help) {
|
|
1834
2088
|
process.stdout.write(`${getHelpText(pkgInfo)}
|
package/package.json
CHANGED