agent-trajectories 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -9,8 +9,9 @@ import {
9
9
  exportToJSON,
10
10
  exportToMarkdown,
11
11
  exportToTimeline,
12
+ generateRandomId,
12
13
  getSearchPaths
13
- } from "../chunk-2NBMLFIW.js";
14
+ } from "../chunk-DX4OPGH7.js";
14
15
 
15
16
  // src/cli/index.ts
16
17
  import { program } from "commander";
@@ -35,6 +36,184 @@ function registerAbandonCommand(program2) {
35
36
  }
36
37
 
37
38
  // src/cli/commands/complete.ts
39
+ import { existsSync } from "fs";
40
+ import { mkdir, writeFile } from "fs/promises";
41
+ import { join } from "path";
42
+
43
+ // src/core/trace.ts
44
+ import { execSync } from "child_process";
45
+ import { createHash } from "crypto";
46
+ function isValidGitRef(ref) {
47
+ const validRefPattern = /^[a-zA-Z0-9_\-./]+$/;
48
+ const commitHashPattern = /^[a-fA-F0-9]{7,40}$/;
49
+ if (ref === "HEAD" || ref === "working") {
50
+ return true;
51
+ }
52
+ if (commitHashPattern.test(ref)) {
53
+ return true;
54
+ }
55
+ if (validRefPattern.test(ref) && ref.length <= 255) {
56
+ if (!ref.includes("..") || ref.split("..").every((p) => p.length > 0)) {
57
+ return true;
58
+ }
59
+ }
60
+ return false;
61
+ }
62
+ function isGitRepo() {
63
+ try {
64
+ execSync("git rev-parse --is-inside-work-tree", {
65
+ encoding: "utf-8",
66
+ stdio: ["pipe", "pipe", "pipe"]
67
+ });
68
+ return true;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+ function getGitHead() {
74
+ if (!isGitRepo()) {
75
+ return null;
76
+ }
77
+ try {
78
+ const head = execSync("git rev-parse HEAD", {
79
+ encoding: "utf-8",
80
+ stdio: ["pipe", "pipe", "pipe"]
81
+ }).trim();
82
+ return head;
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+ function captureGitState() {
88
+ return getGitHead();
89
+ }
90
+ function parseDiffOutput(diffOutput) {
91
+ const files = [];
92
+ const lines = diffOutput.split("\n");
93
+ let currentFile = null;
94
+ let currentRanges = [];
95
+ for (const line of lines) {
96
+ const diffHeaderMatch = line.match(/^diff --git a\/.+ b\/(.+)$/);
97
+ if (diffHeaderMatch) {
98
+ if (currentFile) {
99
+ files.push({ path: currentFile, ranges: currentRanges });
100
+ }
101
+ currentFile = diffHeaderMatch[1];
102
+ currentRanges = [];
103
+ continue;
104
+ }
105
+ const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
106
+ if (hunkMatch && currentFile) {
107
+ const startLine = Number.parseInt(hunkMatch[1], 10);
108
+ const lineCount = hunkMatch[2] ? Number.parseInt(hunkMatch[2], 10) : 1;
109
+ if (lineCount > 0) {
110
+ currentRanges.push({
111
+ start_line: startLine,
112
+ end_line: startLine + lineCount - 1
113
+ });
114
+ }
115
+ }
116
+ }
117
+ if (currentFile) {
118
+ files.push({ path: currentFile, ranges: currentRanges });
119
+ }
120
+ return files;
121
+ }
122
+ function getChangedFiles(startRef, endRef = "HEAD") {
123
+ if (!isGitRepo()) {
124
+ return [];
125
+ }
126
+ if (!isValidGitRef(startRef) || !isValidGitRef(endRef)) {
127
+ return [];
128
+ }
129
+ try {
130
+ const diffOutput = execSync(`git diff ${startRef}..${endRef}`, {
131
+ encoding: "utf-8",
132
+ stdio: ["pipe", "pipe", "pipe"],
133
+ maxBuffer: 10 * 1024 * 1024
134
+ // 10MB buffer for large diffs
135
+ });
136
+ return parseDiffOutput(diffOutput);
137
+ } catch {
138
+ return [];
139
+ }
140
+ }
141
+ function detectModel() {
142
+ if (process.env.TRAIL_TRACE_MODEL) {
143
+ return process.env.TRAIL_TRACE_MODEL;
144
+ }
145
+ if (process.env.ANTHROPIC_MODEL) {
146
+ return process.env.ANTHROPIC_MODEL;
147
+ }
148
+ if (process.env.OPENAI_MODEL) {
149
+ return process.env.OPENAI_MODEL;
150
+ }
151
+ return "unknown";
152
+ }
153
+ function generateTraceId() {
154
+ return `trace_${generateRandomId()}`;
155
+ }
156
+ function generateTrace(trajectory, startRef) {
157
+ if (!isGitRepo()) {
158
+ return null;
159
+ }
160
+ const endRef = getGitHead();
161
+ if (!endRef) {
162
+ return null;
163
+ }
164
+ const changedFiles = getChangedFiles(startRef, endRef);
165
+ if (changedFiles.length === 0) {
166
+ return null;
167
+ }
168
+ const model = detectModel();
169
+ const traceFiles = changedFiles.map(({ path, ranges }) => ({
170
+ path,
171
+ conversations: [
172
+ {
173
+ contributor: {
174
+ type: "agent",
175
+ model
176
+ },
177
+ ranges: ranges.map((range) => ({
178
+ ...range,
179
+ revision: endRef
180
+ }))
181
+ }
182
+ ]
183
+ }));
184
+ return {
185
+ version: 1,
186
+ id: generateTraceId(),
187
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
188
+ trajectory: trajectory.id,
189
+ files: traceFiles
190
+ };
191
+ }
192
+ function createTraceRef(startRef, traceId) {
193
+ const endRef = getGitHead();
194
+ return {
195
+ startRef,
196
+ endRef: endRef ?? void 0,
197
+ traceId
198
+ };
199
+ }
200
+
201
+ // src/cli/commands/complete.ts
202
+ async function saveTraceFile(trajectory, trace) {
203
+ const dataDir = process.env.TRAJECTORIES_DATA_DIR;
204
+ const baseDir = dataDir ? dataDir : join(process.cwd(), ".trajectories");
205
+ const completedDir = join(baseDir, "completed");
206
+ const date = new Date(trajectory.completedAt ?? trajectory.startedAt);
207
+ const monthDir = join(
208
+ completedDir,
209
+ `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`
210
+ );
211
+ if (!existsSync(monthDir)) {
212
+ await mkdir(monthDir, { recursive: true });
213
+ }
214
+ const tracePath = join(monthDir, `${trajectory.id}.trace.json`);
215
+ await writeFile(tracePath, JSON.stringify(trace, null, 2), "utf-8");
216
+ }
38
217
  function registerCompleteCommand(program2) {
39
218
  program2.command("complete").description("Complete the active trajectory with retrospective").option("--summary <text>", "Summary of what was accomplished").option("--approach <text>", "How the work was approached").option("--confidence <number>", "Confidence level 0-1", Number.parseFloat).action(async (options) => {
40
219
  const storage = new FileStorage();
@@ -54,15 +233,37 @@ function registerCompleteCommand(program2) {
54
233
  console.error("Error: --confidence must be between 0 and 1");
55
234
  throw new Error("Invalid confidence");
56
235
  }
57
- const completed = completeTrajectory(active, {
236
+ let completed = completeTrajectory(active, {
58
237
  summary: options.summary,
59
238
  approach: options.approach || "Standard approach",
60
239
  confidence
61
240
  });
241
+ let trace = null;
242
+ if (active._trace?.startRef) {
243
+ trace = generateTrace(completed, active._trace.startRef);
244
+ if (trace) {
245
+ const endRef = getGitHead();
246
+ completed = {
247
+ ...completed,
248
+ _trace: {
249
+ ...completed._trace,
250
+ startRef: active._trace.startRef,
251
+ endRef: endRef ?? void 0,
252
+ traceId: trace.id
253
+ }
254
+ };
255
+ }
256
+ }
62
257
  await storage.save(completed);
258
+ if (trace) {
259
+ await saveTraceFile(completed, trace);
260
+ }
63
261
  console.log(`\u2713 Trajectory completed: ${completed.id}`);
64
262
  console.log(` Summary: ${options.summary}`);
65
263
  console.log(` Confidence: ${Math.round(confidence * 100)}%`);
264
+ if (trace) {
265
+ console.log(` Trace: ${trace.id} (${trace.files.length} files)`);
266
+ }
66
267
  });
67
268
  }
68
269
 
@@ -83,7 +284,7 @@ function registerDecisionCommand(program2) {
83
284
  console.error('Start one with: trail start "Task description"');
84
285
  throw new Error("No active trajectory");
85
286
  }
86
- const alternatives = options.alternatives ? options.alternatives.split(",").map((s) => s.trim()) : [];
287
+ const alternatives = options.alternatives ? options.alternatives.split(",").map((s) => ({ option: s.trim(), reason: "" })) : [];
87
288
  const reasoning = options.reasoning || "";
88
289
  const updated = addDecision(active, {
89
290
  question: choice,
@@ -97,15 +298,18 @@ function registerDecisionCommand(program2) {
97
298
  console.log(` Reasoning: ${reasoning}`);
98
299
  }
99
300
  if (alternatives.length > 0) {
100
- console.log(` Alternatives: ${alternatives.join(", ")}`);
301
+ const altStrings = alternatives.map(
302
+ (a) => a.option
303
+ );
304
+ console.log(` Alternatives: ${altStrings.join(", ")}`);
101
305
  }
102
306
  });
103
307
  }
104
308
 
105
309
  // src/cli/commands/export.ts
106
310
  import { exec } from "child_process";
107
- import { mkdir, writeFile } from "fs/promises";
108
- import { join } from "path";
311
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
312
+ import { join as join2 } from "path";
109
313
 
110
314
  // src/web/styles.ts
111
315
  var styles = `
@@ -502,7 +706,7 @@ function getStatusClass(status) {
502
706
  function renderDecision(decision) {
503
707
  const alternatives = decision.alternatives?.length ? `<div class="alternatives">
504
708
  <span class="alternatives-label">Considered:</span>
505
- ${decision.alternatives.map((a) => escapeHtml(a)).join(", ")}
709
+ ${decision.alternatives.map((a) => escapeHtml(typeof a === "string" ? a : a.option)).join(", ")}
506
710
  </div>` : "";
507
711
  return `
508
712
  <div class="decision">
@@ -741,16 +945,16 @@ function registerExportCommand(program2) {
741
945
  break;
742
946
  }
743
947
  if (options.output) {
744
- await writeFile(options.output, output, "utf-8");
948
+ await writeFile2(options.output, output, "utf-8");
745
949
  console.log(`\u2713 Exported to ${options.output}`);
746
950
  if (options.open && options.format === "html") {
747
951
  openInBrowser(options.output);
748
952
  }
749
953
  } else if (options.open && options.format === "html") {
750
- const outputDir = join(process.cwd(), ".trajectories", "html");
751
- await mkdir(outputDir, { recursive: true });
752
- const filePath = join(outputDir, `${trajectory.id}.html`);
753
- await writeFile(filePath, output, "utf-8");
954
+ const outputDir = join2(process.cwd(), ".trajectories", "html");
955
+ await mkdir2(outputDir, { recursive: true });
956
+ const filePath = join2(outputDir, `${trajectory.id}.html`);
957
+ await writeFile2(filePath, output, "utf-8");
754
958
  console.log(`\u2713 Generated: ${filePath}`);
755
959
  openInBrowser(filePath);
756
960
  } else {
@@ -776,7 +980,7 @@ function openInBrowser(path) {
776
980
  }
777
981
 
778
982
  // src/cli/commands/list.ts
779
- import { existsSync } from "fs";
983
+ import { existsSync as existsSync2 } from "fs";
780
984
  function registerListCommand(program2) {
781
985
  program2.command("list").description("List and search trajectories").option(
782
986
  "-s, --status <status>",
@@ -786,7 +990,7 @@ function registerListCommand(program2) {
786
990
  let allTrajectories = [];
787
991
  const seenIds = /* @__PURE__ */ new Set();
788
992
  for (const searchPath of searchPaths) {
789
- if (!existsSync(searchPath)) {
993
+ if (!existsSync2(searchPath)) {
790
994
  continue;
791
995
  }
792
996
  const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
@@ -874,11 +1078,13 @@ function formatDate2(isoString) {
874
1078
  }
875
1079
 
876
1080
  // src/cli/commands/show.ts
877
- import { existsSync as existsSync2 } from "fs";
1081
+ import { existsSync as existsSync3 } from "fs";
1082
+ import { readFile } from "fs/promises";
1083
+ import { join as join3 } from "path";
878
1084
  async function findTrajectory(id) {
879
1085
  const searchPaths = getSearchPaths();
880
1086
  for (const searchPath of searchPaths) {
881
- if (!existsSync2(searchPath)) {
1087
+ if (!existsSync3(searchPath)) {
882
1088
  continue;
883
1089
  }
884
1090
  const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
@@ -900,13 +1106,79 @@ async function findTrajectory(id) {
900
1106
  }
901
1107
  return null;
902
1108
  }
1109
+ async function findTraceFile(id) {
1110
+ const searchPaths = getSearchPaths();
1111
+ for (const searchPath of searchPaths) {
1112
+ if (!existsSync3(searchPath)) {
1113
+ continue;
1114
+ }
1115
+ const completedDir = join3(searchPath, "completed");
1116
+ if (!existsSync3(completedDir)) {
1117
+ continue;
1118
+ }
1119
+ try {
1120
+ const { readdirSync } = await import("fs");
1121
+ const months = readdirSync(completedDir);
1122
+ for (const month of months) {
1123
+ const tracePath = join3(completedDir, month, `${id}.trace.json`);
1124
+ if (existsSync3(tracePath)) {
1125
+ const content = await readFile(tracePath, "utf-8");
1126
+ return JSON.parse(content);
1127
+ }
1128
+ }
1129
+ } catch {
1130
+ }
1131
+ }
1132
+ return null;
1133
+ }
903
1134
  function registerShowCommand(program2) {
904
- program2.command("show <id>").description("Show trajectory details").option("-d, --decisions", "Show decisions only").action(async (id, options) => {
1135
+ program2.command("show <id>").description("Show trajectory details").option("-d, --decisions", "Show decisions only").option("-t, --trace", "Show trace information").action(async (id, options) => {
905
1136
  const trajectory = await findTrajectory(id);
906
1137
  if (!trajectory) {
907
1138
  console.error(`Error: Trajectory not found: ${id}`);
908
1139
  throw new Error("Trajectory not found");
909
1140
  }
1141
+ if (options.trace) {
1142
+ console.log(`Trace for ${trajectory.task.title}:
1143
+ `);
1144
+ if (trajectory._trace) {
1145
+ console.log("Trace Reference:");
1146
+ console.log(` Start Ref: ${trajectory._trace.startRef}`);
1147
+ if (trajectory._trace.endRef) {
1148
+ console.log(` End Ref: ${trajectory._trace.endRef}`);
1149
+ }
1150
+ if (trajectory._trace.traceId) {
1151
+ console.log(` Trace ID: ${trajectory._trace.traceId}`);
1152
+ }
1153
+ console.log("");
1154
+ }
1155
+ const trace = await findTraceFile(id);
1156
+ if (trace) {
1157
+ console.log("Trace Details:");
1158
+ console.log(` ID: ${trace.id}`);
1159
+ console.log(` Timestamp: ${trace.timestamp}`);
1160
+ console.log(` Files: ${trace.files.length}`);
1161
+ console.log("");
1162
+ if (trace.files.length > 0) {
1163
+ console.log("Modified Files:");
1164
+ for (const file of trace.files) {
1165
+ const rangeCount = file.conversations.reduce(
1166
+ (sum, conv) => sum + conv.ranges.length,
1167
+ 0
1168
+ );
1169
+ const model = file.conversations[0]?.contributor.model ?? "unknown";
1170
+ console.log(` \u2022 ${file.path}`);
1171
+ console.log(` Ranges: ${rangeCount}, Model: ${model}`);
1172
+ }
1173
+ }
1174
+ } else if (!trajectory._trace) {
1175
+ console.log("No trace information available");
1176
+ console.log(
1177
+ "Trace is captured when starting a trajectory in a git repo"
1178
+ );
1179
+ }
1180
+ return;
1181
+ }
910
1182
  if (options.decisions) {
911
1183
  const decisions = extractDecisions(trajectory);
912
1184
  if (decisions.length === 0) {
@@ -920,7 +1192,10 @@ function registerShowCommand(program2) {
920
1192
  console.log(` Chose: ${decision.chosen}`);
921
1193
  console.log(` Reasoning: ${decision.reasoning}`);
922
1194
  if (decision.alternatives.length > 0) {
923
- console.log(` Alternatives: ${decision.alternatives.join(", ")}`);
1195
+ const altStrings = decision.alternatives.map(
1196
+ (a) => typeof a === "string" ? a : a.option
1197
+ );
1198
+ console.log(` Alternatives: ${altStrings.join(", ")}`);
924
1199
  }
925
1200
  console.log("");
926
1201
  }
@@ -1002,11 +1277,18 @@ function registerStartCommand(program2) {
1002
1277
  }
1003
1278
  const agentName = options.agent ?? process.env.TRAJECTORIES_AGENT ?? void 0;
1004
1279
  const projectId = options.project ?? process.env.TRAJECTORIES_PROJECT ?? void 0;
1280
+ const startRef = captureGitState();
1005
1281
  let trajectory = createTrajectory({
1006
1282
  title,
1007
1283
  source,
1008
1284
  projectId
1009
1285
  });
1286
+ if (startRef) {
1287
+ trajectory = {
1288
+ ...trajectory,
1289
+ _trace: createTraceRef(startRef)
1290
+ };
1291
+ }
1010
1292
  if (agentName) {
1011
1293
  trajectory = addChapter(trajectory, {
1012
1294
  title: "Initial work",