@jskit-ai/jskit-cli 0.2.78 → 0.2.80

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.
@@ -0,0 +1,315 @@
1
+ import { mkdir, readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import {
4
+ SESSION_STATUS,
5
+ STEP_DEFINITION_BY_ID,
6
+ STEP_DEFINITIONS,
7
+ STEP_IDS,
8
+ STEP_LABEL_BY_ID
9
+ } from "./constants.js";
10
+ import {
11
+ fileExists,
12
+ normalizeText,
13
+ readTextIfExists,
14
+ readTrimmedFile,
15
+ timestampForReceipt,
16
+ writeTextFile
17
+ } from "./io.js";
18
+ import {
19
+ pathsForExistingSession
20
+ } from "./paths.js";
21
+
22
+ function createError({
23
+ code,
24
+ message,
25
+ repairCommand = ""
26
+ }) {
27
+ return Object.freeze({
28
+ code: normalizeText(code),
29
+ message: normalizeText(message),
30
+ repairCommand: normalizeText(repairCommand)
31
+ });
32
+ }
33
+
34
+ function createPrecondition({
35
+ id,
36
+ ok,
37
+ message
38
+ }) {
39
+ return Object.freeze({
40
+ id: normalizeText(id),
41
+ ok: ok === true,
42
+ message: normalizeText(message)
43
+ });
44
+ }
45
+
46
+ async function readCompletedSteps(sessionRoot) {
47
+ const stepsRoot = path.join(sessionRoot, "steps");
48
+ try {
49
+ const entries = await readdir(stepsRoot, { withFileTypes: true });
50
+ return entries
51
+ .filter((entry) => entry.isFile())
52
+ .map((entry) => entry.name)
53
+ .filter((entry) => STEP_IDS.includes(entry))
54
+ .sort((left, right) => STEP_IDS.indexOf(left) - STEP_IDS.indexOf(right));
55
+ } catch {
56
+ return [];
57
+ }
58
+ }
59
+
60
+ async function readReceiptSteps(paths) {
61
+ const stepsRoot = path.join(paths.sessionRoot, "steps");
62
+ try {
63
+ const entries = await readdir(stepsRoot, { withFileTypes: true });
64
+ const stepNames = entries
65
+ .filter((entry) => entry.isFile())
66
+ .map((entry) => entry.name)
67
+ .sort((left, right) => {
68
+ const leftIndex = STEP_IDS.indexOf(left);
69
+ const rightIndex = STEP_IDS.indexOf(right);
70
+ if (leftIndex >= 0 && rightIndex >= 0) {
71
+ return leftIndex - rightIndex;
72
+ }
73
+ if (leftIndex >= 0) {
74
+ return -1;
75
+ }
76
+ if (rightIndex >= 0) {
77
+ return 1;
78
+ }
79
+ return left.localeCompare(right);
80
+ });
81
+
82
+ return Promise.all(stepNames.map(async (stepId) => ({
83
+ label: STEP_LABEL_BY_ID[stepId] || stepId,
84
+ receipt: (await readTextIfExists(path.join(stepsRoot, stepId))).trim(),
85
+ stepId
86
+ })));
87
+ } catch {
88
+ return [];
89
+ }
90
+ }
91
+
92
+ function resolveNextStep(completedSteps = []) {
93
+ const completed = new Set(completedSteps);
94
+ return STEP_IDS.find((stepId) => !completed.has(stepId)) || "";
95
+ }
96
+
97
+ function cloneContractValue(value) {
98
+ if (!value || typeof value !== "object") {
99
+ return value;
100
+ }
101
+ if (Array.isArray(value)) {
102
+ return value.map((entry) => cloneContractValue(entry));
103
+ }
104
+ return Object.fromEntries(
105
+ Object.entries(value).map(([key, entry]) => [key, cloneContractValue(entry)])
106
+ );
107
+ }
108
+
109
+ function publicStepDefinition(step, index) {
110
+ return {
111
+ description: step.description,
112
+ id: step.id,
113
+ index,
114
+ input: cloneContractValue(step.input),
115
+ kind: step.kind,
116
+ label: step.label
117
+ };
118
+ }
119
+
120
+ function buildStepDefinitions() {
121
+ return STEP_DEFINITIONS.map((step, index) => publicStepDefinition(step, index));
122
+ }
123
+
124
+ function buildCurrentStepAction(stepId) {
125
+ const step = STEP_DEFINITION_BY_ID[stepId];
126
+ if (!step) {
127
+ return null;
128
+ }
129
+ return {
130
+ buttonLabel: step.buttonLabel,
131
+ description: step.description,
132
+ index: STEP_IDS.indexOf(step.id),
133
+ input: cloneContractValue(step.input),
134
+ kind: step.kind,
135
+ stepId: step.id
136
+ };
137
+ }
138
+
139
+ function buildCodexHandoff(stepId) {
140
+ const step = STEP_DEFINITION_BY_ID[stepId];
141
+ return step?.codex ? cloneContractValue(step.codex) : null;
142
+ }
143
+
144
+ async function readSessionArtifacts(paths) {
145
+ const [status, currentStep, issueUrl, prUrl, prompt, issueText, codexThreadId] = await Promise.all([
146
+ readTrimmedFile(path.join(paths.sessionRoot, "status")),
147
+ readTrimmedFile(path.join(paths.sessionRoot, "current_step")),
148
+ readTrimmedFile(path.join(paths.sessionRoot, "issue_url")),
149
+ readTrimmedFile(path.join(paths.sessionRoot, "pr_url")),
150
+ readTextIfExists(path.join(paths.sessionRoot, "prompt.md")),
151
+ readTextIfExists(path.join(paths.sessionRoot, "issue.md")),
152
+ readTrimmedFile(path.join(paths.sessionRoot, "codex_thread_id"))
153
+ ]);
154
+ const completedSteps = await readCompletedSteps(paths.sessionRoot);
155
+ const nextStep = resolveNextStep(completedSteps);
156
+
157
+ return {
158
+ codexThreadId,
159
+ completedSteps,
160
+ currentStep: currentStep || nextStep,
161
+ issueText: issueText.trim(),
162
+ issueUrl,
163
+ nextStep,
164
+ prUrl,
165
+ prompt: prompt.trim(),
166
+ status: status || SESSION_STATUS.PENDING
167
+ };
168
+ }
169
+
170
+ function buildNextCommand(sessionId, stepId) {
171
+ if (!stepId) {
172
+ return "";
173
+ }
174
+ const template = STEP_DEFINITION_BY_ID[stepId]?.nextCommandTemplate || "jskit session {{session_id}} step";
175
+ return template.replaceAll("{{session_id}}", sessionId);
176
+ }
177
+
178
+ async function buildSessionResponse(paths, {
179
+ ok = true,
180
+ errors = [],
181
+ preconditions = [],
182
+ prompt = undefined,
183
+ status = undefined
184
+ } = {}) {
185
+ const responsePaths = paths.sessionId ? await pathsForExistingSession(paths) : paths;
186
+ const artifacts = responsePaths.sessionRoot ? await readSessionArtifacts(responsePaths) : {};
187
+ const resolvedStatus = status || artifacts.status || (ok ? SESSION_STATUS.PENDING : SESSION_STATUS.BLOCKED);
188
+ const currentStep = artifacts.currentStep || artifacts.nextStep || "";
189
+ const responsePrompt = typeof prompt === "string" ? prompt : artifacts.prompt || "";
190
+
191
+ return {
192
+ ok: ok === true,
193
+ sessionId: paths.sessionId || "",
194
+ status: resolvedStatus,
195
+ currentStep,
196
+ completedSteps: artifacts.completedSteps || [],
197
+ stepDefinitions: buildStepDefinitions(),
198
+ currentStepAction: buildCurrentStepAction(currentStep),
199
+ codex: buildCodexHandoff(currentStep),
200
+ prompt: responsePrompt,
201
+ nextCommand: buildNextCommand(paths.sessionId || "", currentStep),
202
+ issueUrl: artifacts.issueUrl || "",
203
+ prUrl: artifacts.prUrl || "",
204
+ preconditions,
205
+ errors,
206
+ archive: responsePaths.archive || (resolvedStatus === SESSION_STATUS.FINISHED ? "completed" : resolvedStatus === SESSION_STATUS.ABANDONED ? "abandoned" : "active"),
207
+ sessionRoot: responsePaths.sessionRoot || "",
208
+ worktree: paths.worktree || "",
209
+ branch: paths.branch || "",
210
+ codexThreadId: artifacts.codexThreadId || ""
211
+ };
212
+ }
213
+
214
+ function buildSessionErrorResponse({
215
+ targetRoot = process.cwd(),
216
+ sessionId = "",
217
+ code,
218
+ message,
219
+ repairCommand = "",
220
+ status = SESSION_STATUS.BLOCKED,
221
+ preconditions = [],
222
+ errors = undefined
223
+ } = {}) {
224
+ const normalizedTargetRoot = path.resolve(normalizeText(targetRoot) || process.cwd());
225
+ const errorList = Array.isArray(errors)
226
+ ? errors
227
+ : [
228
+ createError({
229
+ code,
230
+ message,
231
+ repairCommand
232
+ })
233
+ ];
234
+
235
+ return {
236
+ ok: false,
237
+ sessionId: normalizeText(sessionId),
238
+ status,
239
+ currentStep: "",
240
+ completedSteps: [],
241
+ stepDefinitions: buildStepDefinitions(),
242
+ currentStepAction: null,
243
+ codex: null,
244
+ prompt: "",
245
+ nextCommand: "",
246
+ issueUrl: "",
247
+ prUrl: "",
248
+ preconditions,
249
+ errors: errorList,
250
+ archive: "",
251
+ sessionRoot: "",
252
+ worktree: "",
253
+ branch: "",
254
+ codexThreadId: "",
255
+ targetRoot: normalizedTargetRoot
256
+ };
257
+ }
258
+
259
+ async function markStatus(paths, status) {
260
+ await writeTextFile(path.join(paths.sessionRoot, "status"), status);
261
+ }
262
+
263
+ async function markCurrentStep(paths, stepId) {
264
+ await writeTextFile(path.join(paths.sessionRoot, "current_step"), stepId);
265
+ }
266
+
267
+ async function writeReceipt(paths, stepId, message) {
268
+ await mkdir(path.join(paths.sessionRoot, "steps"), { recursive: true });
269
+ await writeTextFile(
270
+ path.join(paths.sessionRoot, "steps", stepId),
271
+ `${timestampForReceipt()}\n${normalizeText(message) || STEP_LABEL_BY_ID[stepId] || stepId}`
272
+ );
273
+ const completedSteps = await readCompletedSteps(paths.sessionRoot);
274
+ await markCurrentStep(paths, resolveNextStep(completedSteps));
275
+ }
276
+
277
+ async function failSession(paths, {
278
+ code,
279
+ message,
280
+ repairCommand = "",
281
+ preconditions = [],
282
+ status = SESSION_STATUS.BLOCKED,
283
+ prompt = ""
284
+ }) {
285
+ if (paths.sessionRoot && await fileExists(paths.sessionRoot)) {
286
+ await markStatus(paths, status);
287
+ }
288
+ return buildSessionResponse(paths, {
289
+ ok: false,
290
+ status,
291
+ prompt,
292
+ preconditions,
293
+ errors: [
294
+ createError({
295
+ code,
296
+ message,
297
+ repairCommand
298
+ })
299
+ ]
300
+ });
301
+ }
302
+
303
+ export {
304
+ buildSessionErrorResponse,
305
+ buildSessionResponse,
306
+ buildStepDefinitions,
307
+ createError,
308
+ createPrecondition,
309
+ failSession,
310
+ markCurrentStep,
311
+ markStatus,
312
+ readReceiptSteps,
313
+ readSessionArtifacts,
314
+ writeReceipt
315
+ };