trajectories 0.1.1 → 0.1.2
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/CHANGELOG.md +13 -0
- package/README.md +17 -3
- package/dist/cli.js +23148 -1480
- package/dist/deepagents_checkpoint.py +362 -0
- package/legacy/src/cli.ts +2 -2
- package/legacy/src/sync.ts +17 -3
- package/ops/README.md +31 -1
- package/ops/install-native.sh +35 -0
- package/package.json +5 -3
- package/dist/cli.d.ts +0 -2
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read a Python LangGraph SQLite checkpoint through LangGraph's public APIs."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sqlite3
|
|
9
|
+
import sys
|
|
10
|
+
from collections.abc import Iterable, Mapping
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _emit(payload: Mapping[str, Any]) -> None:
|
|
16
|
+
sys.stdout.write(json.dumps(payload, ensure_ascii=False, separators=(",", ":")))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _fail(code: str, message: str) -> None:
|
|
20
|
+
_emit({"ok": False, "code": code, "message": message})
|
|
21
|
+
raise SystemExit(0)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
|
26
|
+
from langgraph.checkpoint.sqlite import SqliteSaver
|
|
27
|
+
from langgraph.graph.message import add_messages
|
|
28
|
+
from langgraph.types import Overwrite
|
|
29
|
+
except (ImportError, ModuleNotFoundError) as error:
|
|
30
|
+
_fail(
|
|
31
|
+
"python_dependency_missing",
|
|
32
|
+
"Reading Deep Agents checkpoints requires Python packages "
|
|
33
|
+
"langgraph and langgraph-checkpoint-sqlite; install the "
|
|
34
|
+
"agent-trajectory[deepagents] extra in this Python environment "
|
|
35
|
+
f"(import error: {error}).",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _request() -> dict[str, Any]:
|
|
40
|
+
try:
|
|
41
|
+
value = json.load(sys.stdin)
|
|
42
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
43
|
+
_fail("invalid_input", f"Deep Agents checkpoint request is invalid: {error}")
|
|
44
|
+
if not isinstance(value, dict):
|
|
45
|
+
_fail("invalid_input", "Deep Agents checkpoint request must be an object.")
|
|
46
|
+
return value
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _unwrap_snapshot(value: Any) -> Any:
|
|
50
|
+
# DeltaChannel history exposes its decoded snapshot through the public
|
|
51
|
+
# `seed` contract. In current LangGraph releases that value is a namedtuple
|
|
52
|
+
# with one `value` field; avoid importing its private concrete class.
|
|
53
|
+
if (
|
|
54
|
+
isinstance(value, tuple)
|
|
55
|
+
and getattr(value, "_fields", None) == ("value",)
|
|
56
|
+
and value.__class__.__name__.endswith("DeltaSnapshot")
|
|
57
|
+
):
|
|
58
|
+
return value.value
|
|
59
|
+
return value
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _as_messages(value: Any) -> list[Any]:
|
|
63
|
+
value = _unwrap_snapshot(value)
|
|
64
|
+
if value is None:
|
|
65
|
+
return []
|
|
66
|
+
if isinstance(value, (list, tuple)):
|
|
67
|
+
return list(value)
|
|
68
|
+
return [value]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _apply_message_writes(messages: list[Any], writes: Iterable[tuple[Any, str, Any]]) -> list[Any]:
|
|
72
|
+
current = messages
|
|
73
|
+
for _task_id, channel, value in writes:
|
|
74
|
+
if channel != "messages":
|
|
75
|
+
continue
|
|
76
|
+
if isinstance(value, Overwrite):
|
|
77
|
+
current = _as_messages(value.value)
|
|
78
|
+
else:
|
|
79
|
+
current = list(add_messages(current, value))
|
|
80
|
+
return current
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _reconstruct_messages(saver: Any, checkpoint_tuple: Any) -> list[Any]:
|
|
84
|
+
channel_values = checkpoint_tuple.checkpoint.get("channel_values") or {}
|
|
85
|
+
if not isinstance(channel_values, Mapping):
|
|
86
|
+
_fail("invalid_checkpoint_state", "Checkpoint channel_values must be a mapping.")
|
|
87
|
+
|
|
88
|
+
if "messages" in channel_values:
|
|
89
|
+
messages = _as_messages(channel_values["messages"])
|
|
90
|
+
else:
|
|
91
|
+
# This is the public LangGraph API for reconstructing a DeltaChannel:
|
|
92
|
+
# it follows only the selected checkpoint's parent chain and returns
|
|
93
|
+
# already-deserialized seed/writes in oldest-to-newest order.
|
|
94
|
+
if not hasattr(saver, "get_delta_channel_history"):
|
|
95
|
+
_fail(
|
|
96
|
+
"python_dependency_missing",
|
|
97
|
+
"This checkpoint requires a LangGraph release with "
|
|
98
|
+
"get_delta_channel_history support; upgrade the deepagents extra.",
|
|
99
|
+
)
|
|
100
|
+
histories = saver.get_delta_channel_history(
|
|
101
|
+
config=checkpoint_tuple.config,
|
|
102
|
+
channels=["messages"],
|
|
103
|
+
)
|
|
104
|
+
history = histories.get("messages", {})
|
|
105
|
+
messages = _as_messages(history.get("seed"))
|
|
106
|
+
messages = _apply_message_writes(messages, history.get("writes", ()))
|
|
107
|
+
|
|
108
|
+
return _apply_message_writes(messages, checkpoint_tuple.pending_writes or ())
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _content_parts(content: Any) -> tuple[str, list[str]]:
|
|
112
|
+
if isinstance(content, str):
|
|
113
|
+
return content, []
|
|
114
|
+
if not isinstance(content, (list, tuple)):
|
|
115
|
+
return _stringify(content), []
|
|
116
|
+
|
|
117
|
+
text: list[str] = []
|
|
118
|
+
reasoning: list[str] = []
|
|
119
|
+
for block in content:
|
|
120
|
+
if isinstance(block, str):
|
|
121
|
+
text.append(block)
|
|
122
|
+
continue
|
|
123
|
+
if not isinstance(block, Mapping):
|
|
124
|
+
continue
|
|
125
|
+
block_type = block.get("type")
|
|
126
|
+
if block_type == "reasoning":
|
|
127
|
+
value = block.get("reasoning", block.get("text"))
|
|
128
|
+
if isinstance(value, str) and value:
|
|
129
|
+
reasoning.append(value)
|
|
130
|
+
elif block_type in {"text", "input_text", "output_text"} or (
|
|
131
|
+
block_type is None and "text" in block
|
|
132
|
+
):
|
|
133
|
+
value = block.get("text")
|
|
134
|
+
if isinstance(value, str) and value:
|
|
135
|
+
text.append(value)
|
|
136
|
+
return "\n".join(text), reasoning
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _stringify(value: Any) -> str:
|
|
140
|
+
if isinstance(value, str):
|
|
141
|
+
return value
|
|
142
|
+
try:
|
|
143
|
+
return json.dumps(value, ensure_ascii=False, default=str, separators=(",", ":"))
|
|
144
|
+
except (TypeError, ValueError):
|
|
145
|
+
return str(value)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _json_safe(value: Any) -> Any:
|
|
149
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
150
|
+
return value
|
|
151
|
+
if isinstance(value, Mapping):
|
|
152
|
+
return {str(key): _json_safe(item) for key, item in value.items()}
|
|
153
|
+
if isinstance(value, (list, tuple)):
|
|
154
|
+
return [_json_safe(item) for item in value]
|
|
155
|
+
return str(value)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _timestamp(message: Any) -> str | None:
|
|
159
|
+
for container in (
|
|
160
|
+
getattr(message, "additional_kwargs", None),
|
|
161
|
+
getattr(message, "response_metadata", None),
|
|
162
|
+
):
|
|
163
|
+
if not isinstance(container, Mapping):
|
|
164
|
+
continue
|
|
165
|
+
for key in ("timestamp", "created_at", "createdAt"):
|
|
166
|
+
value = container.get(key)
|
|
167
|
+
if isinstance(value, str) and value:
|
|
168
|
+
return value
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _model(message: Any) -> str | None:
|
|
173
|
+
for container in (
|
|
174
|
+
getattr(message, "response_metadata", None),
|
|
175
|
+
getattr(message, "additional_kwargs", None),
|
|
176
|
+
):
|
|
177
|
+
if not isinstance(container, Mapping):
|
|
178
|
+
continue
|
|
179
|
+
for key in ("model_name", "model", "model_id"):
|
|
180
|
+
value = container.get(key)
|
|
181
|
+
if isinstance(value, str) and value:
|
|
182
|
+
return value
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _reasoning_from_metadata(message: Any) -> list[str]:
|
|
187
|
+
output: list[str] = []
|
|
188
|
+
for container in (
|
|
189
|
+
getattr(message, "additional_kwargs", None),
|
|
190
|
+
getattr(message, "response_metadata", None),
|
|
191
|
+
):
|
|
192
|
+
if not isinstance(container, Mapping):
|
|
193
|
+
continue
|
|
194
|
+
for key in ("reasoning", "reasoning_content"):
|
|
195
|
+
value = container.get(key)
|
|
196
|
+
if isinstance(value, str) and value and value not in output:
|
|
197
|
+
output.append(value)
|
|
198
|
+
return output
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _message_data(message: Any) -> dict[str, Any] | None:
|
|
202
|
+
timestamp = _timestamp(message)
|
|
203
|
+
if isinstance(message, HumanMessage) or getattr(message, "type", None) == "human":
|
|
204
|
+
content, _ = _content_parts(message.content)
|
|
205
|
+
return {
|
|
206
|
+
"role": "human",
|
|
207
|
+
"content": content,
|
|
208
|
+
**({"timestamp": timestamp} if timestamp else {}),
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if isinstance(message, AIMessage) or getattr(message, "type", None) == "ai":
|
|
212
|
+
content, reasoning = _content_parts(message.content)
|
|
213
|
+
reasoning.extend(
|
|
214
|
+
value for value in _reasoning_from_metadata(message) if value not in reasoning
|
|
215
|
+
)
|
|
216
|
+
calls: list[dict[str, Any]] = []
|
|
217
|
+
for call in getattr(message, "tool_calls", None) or ():
|
|
218
|
+
if not isinstance(call, Mapping):
|
|
219
|
+
continue
|
|
220
|
+
calls.append(
|
|
221
|
+
{
|
|
222
|
+
"args": _json_safe(call.get("args", {})),
|
|
223
|
+
**({"id": call["id"]} if isinstance(call.get("id"), str) else {}),
|
|
224
|
+
**(
|
|
225
|
+
{"name": call["name"]}
|
|
226
|
+
if isinstance(call.get("name"), str)
|
|
227
|
+
else {}
|
|
228
|
+
),
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
model = _model(message)
|
|
232
|
+
return {
|
|
233
|
+
"role": "ai",
|
|
234
|
+
"content": content,
|
|
235
|
+
"reasoning": reasoning,
|
|
236
|
+
"toolCalls": calls,
|
|
237
|
+
**({"model": model} if model else {}),
|
|
238
|
+
**({"timestamp": timestamp} if timestamp else {}),
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if isinstance(message, ToolMessage) or getattr(message, "type", None) == "tool":
|
|
242
|
+
content, _ = _content_parts(message.content)
|
|
243
|
+
call_id = getattr(message, "tool_call_id", None)
|
|
244
|
+
if not isinstance(call_id, str):
|
|
245
|
+
call_id = ""
|
|
246
|
+
return {
|
|
247
|
+
"role": "tool",
|
|
248
|
+
"content": content,
|
|
249
|
+
"toolCallId": call_id,
|
|
250
|
+
**({"timestamp": timestamp} if timestamp else {}),
|
|
251
|
+
}
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _context_string(
|
|
256
|
+
channel_values: Mapping[str, Any], metadata: Mapping[str, Any], keys: tuple[str, ...]
|
|
257
|
+
) -> str | None:
|
|
258
|
+
for container in (channel_values, metadata):
|
|
259
|
+
for key in keys:
|
|
260
|
+
value = container.get(key)
|
|
261
|
+
if isinstance(value, str) and value:
|
|
262
|
+
return value
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def main() -> None:
|
|
267
|
+
request = _request()
|
|
268
|
+
path_value = request.get("path")
|
|
269
|
+
thread_id = request.get("threadId")
|
|
270
|
+
namespace = request.get("checkpointNamespace", "")
|
|
271
|
+
checkpoint_id = request.get("checkpointId")
|
|
272
|
+
if not isinstance(path_value, str) or not path_value:
|
|
273
|
+
_fail("invalid_input", "Deep Agents checkpoint path is required.")
|
|
274
|
+
if not isinstance(thread_id, str) or not thread_id:
|
|
275
|
+
_fail("invalid_input", "Deep Agents threadId is required.")
|
|
276
|
+
if not isinstance(namespace, str):
|
|
277
|
+
_fail("invalid_input", "Deep Agents checkpointNamespace must be a string.")
|
|
278
|
+
if checkpoint_id is not None and (
|
|
279
|
+
not isinstance(checkpoint_id, str) or not checkpoint_id
|
|
280
|
+
):
|
|
281
|
+
_fail("invalid_input", "Deep Agents checkpointId must be a non-empty string.")
|
|
282
|
+
|
|
283
|
+
path = Path(path_value).expanduser()
|
|
284
|
+
if not path.is_file():
|
|
285
|
+
_fail(
|
|
286
|
+
"checkpoint_database_not_found",
|
|
287
|
+
f"Deep Agents checkpoint database does not exist: {path}",
|
|
288
|
+
)
|
|
289
|
+
if not os.access(path, os.R_OK):
|
|
290
|
+
_fail(
|
|
291
|
+
"checkpoint_database_unreadable",
|
|
292
|
+
f"Deep Agents checkpoint database is not readable: {path}",
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
config: dict[str, Any] = {
|
|
296
|
+
"configurable": {
|
|
297
|
+
"thread_id": thread_id,
|
|
298
|
+
"checkpoint_ns": namespace,
|
|
299
|
+
**({"checkpoint_id": checkpoint_id} if checkpoint_id else {}),
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
try:
|
|
303
|
+
with SqliteSaver.from_conn_string(str(path)) as saver:
|
|
304
|
+
checkpoint_tuple = saver.get_tuple(config)
|
|
305
|
+
if checkpoint_tuple is None:
|
|
306
|
+
label = f" checkpoint {checkpoint_id!r}" if checkpoint_id else ""
|
|
307
|
+
_fail(
|
|
308
|
+
"checkpoint_not_found",
|
|
309
|
+
f"No Deep Agents{label} checkpoint was found for thread "
|
|
310
|
+
f"{thread_id!r} in namespace {namespace!r}.",
|
|
311
|
+
)
|
|
312
|
+
messages = _reconstruct_messages(saver, checkpoint_tuple)
|
|
313
|
+
except sqlite3.DatabaseError as error:
|
|
314
|
+
_fail("checkpoint_database_unreadable", f"Could not read checkpoint database: {error}")
|
|
315
|
+
except (OSError, ValueError, TypeError, KeyError) as error:
|
|
316
|
+
_fail("checkpoint_read_failed", f"Could not reconstruct Deep Agents checkpoint: {error}")
|
|
317
|
+
|
|
318
|
+
serialized_messages = [
|
|
319
|
+
data for message in messages if (data := _message_data(message)) is not None
|
|
320
|
+
]
|
|
321
|
+
if not serialized_messages:
|
|
322
|
+
_fail(
|
|
323
|
+
"checkpoint_messages_missing",
|
|
324
|
+
"Deep Agents checkpoint did not contain canonical LangChain messages.",
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
channel_values = checkpoint_tuple.checkpoint.get("channel_values") or {}
|
|
328
|
+
metadata = checkpoint_tuple.metadata or {}
|
|
329
|
+
selected = checkpoint_tuple.config.get("configurable", {})
|
|
330
|
+
cwd = _context_string(
|
|
331
|
+
channel_values,
|
|
332
|
+
metadata,
|
|
333
|
+
("cwd", "working_directory", "current_working_directory"),
|
|
334
|
+
)
|
|
335
|
+
model = _context_string(channel_values, metadata, ("model", "model_name", "model_id"))
|
|
336
|
+
if model is None:
|
|
337
|
+
model = next(
|
|
338
|
+
(
|
|
339
|
+
item["model"]
|
|
340
|
+
for item in serialized_messages
|
|
341
|
+
if item.get("role") == "ai" and isinstance(item.get("model"), str)
|
|
342
|
+
),
|
|
343
|
+
None,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
_emit(
|
|
347
|
+
{
|
|
348
|
+
"ok": True,
|
|
349
|
+
"data": {
|
|
350
|
+
"checkpointId": str(selected.get("checkpoint_id", checkpoint_tuple.checkpoint["id"])),
|
|
351
|
+
"checkpointNamespace": str(selected.get("checkpoint_ns", namespace)),
|
|
352
|
+
"checkpointTimestamp": str(checkpoint_tuple.checkpoint["ts"]),
|
|
353
|
+
"messages": serialized_messages,
|
|
354
|
+
**({"cwd": cwd} if cwd else {}),
|
|
355
|
+
**({"model": model} if model else {}),
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
if __name__ == "__main__":
|
|
362
|
+
main()
|
package/legacy/src/cli.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import manifest from "../../package.json";
|
|
2
2
|
import { SessionNotFoundError, StoreNotInitializedError, TrajectoryDb } from "./db.js";
|
|
3
3
|
import { aggregateIngestDiagnostics } from "./diagnostics.js";
|
|
4
4
|
import {
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
WriterLeaseBusyError,
|
|
29
29
|
} from "./writer-lease.js";
|
|
30
30
|
|
|
31
|
-
const VERSION: string =
|
|
31
|
+
const VERSION: string = manifest.version;
|
|
32
32
|
|
|
33
33
|
/** The provider a `--embedding` value names; an absent flag asks for local, a bare `--embedding` asks for Gemini. */
|
|
34
34
|
function requestedEmbedding(value: string | boolean | undefined): string {
|
package/legacy/src/sync.ts
CHANGED
|
@@ -630,12 +630,26 @@ function retryAt(now: Date, attempts: number): string {
|
|
|
630
630
|
return new Date(now.getTime() + delayMs).toISOString();
|
|
631
631
|
}
|
|
632
632
|
|
|
633
|
+
declare const TRAJECTORIES_STANDALONE: boolean;
|
|
634
|
+
declare const TRAJECTORIES_BUNDLED_CLI: boolean;
|
|
635
|
+
|
|
636
|
+
function syncEntry(command: string): string[] {
|
|
637
|
+
// Compiled distributions execute the same private verbs in a fresh process.
|
|
638
|
+
if (typeof TRAJECTORIES_STANDALONE !== "undefined" && TRAJECTORIES_STANDALONE) {
|
|
639
|
+
return [process.execPath, command];
|
|
640
|
+
}
|
|
641
|
+
if (typeof TRAJECTORIES_BUNDLED_CLI !== "undefined" && TRAJECTORIES_BUNDLED_CLI) {
|
|
642
|
+
return [process.execPath, process.argv[1]!, command];
|
|
643
|
+
}
|
|
644
|
+
return [process.execPath, fileURLToPath(new URL("../bin/ledger-core.ts", import.meta.url)), command];
|
|
645
|
+
}
|
|
646
|
+
|
|
633
647
|
function workerEntry(): string[] {
|
|
634
|
-
return
|
|
648
|
+
return syncEntry("__sync-worker");
|
|
635
649
|
}
|
|
636
650
|
|
|
637
651
|
function maintenanceEntry(): string[] {
|
|
638
|
-
return
|
|
652
|
+
return syncEntry("__sync-maintenance");
|
|
639
653
|
}
|
|
640
654
|
|
|
641
655
|
export function workerReservation(
|
|
@@ -863,7 +877,7 @@ async function prepareInChild(options: SyncOptions, scanLimit: number, maxRecord
|
|
|
863
877
|
if (remaining <= 0) throw new SyncPreparationError("timed_out");
|
|
864
878
|
const result = await spawnJsonChild<SyncPreparationReply | PreparationFailure>({ protocol_version: SYNC_PROTOCOL_VERSION,
|
|
865
879
|
db_path: options.dbPath, sources: options.sources, roots: options.roots, scan_limit: scanLimit, max_record_bytes: maxRecordBytes },
|
|
866
|
-
remaining, options.preparationCommand ??
|
|
880
|
+
remaining, options.preparationCommand ?? syncEntry("__sync-prepare"),
|
|
867
881
|
parsePreparationReply, { label: "sync_preparation" });
|
|
868
882
|
if (result.status !== "ok" || !result.reply) throw new SyncPreparationError(result.status);
|
|
869
883
|
if (result.reply.type === "sync_preparation_error") {
|
package/ops/README.md
CHANGED
|
@@ -7,10 +7,40 @@ session/log store, which requires an explicit `index --store` command.
|
|
|
7
7
|
|
|
8
8
|
## Install the CLI
|
|
9
9
|
|
|
10
|
+
### Native CLI
|
|
11
|
+
|
|
12
|
+
Native releases require no Bun, Node or npm. The repository is currently private,
|
|
13
|
+
so GitHub downloads require an authenticated `gh` client with repository access.
|
|
14
|
+
On an Apple Silicon Mac:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
gh release download 'trajectories@0.1.2' --repo archeism/oh-my-ai \
|
|
18
|
+
--pattern 'trajectories-0.1.2-darwin-arm64.tar.gz*'
|
|
19
|
+
shasum -a 256 --check trajectories-0.1.2-darwin-arm64.tar.gz.sha256
|
|
20
|
+
mkdir -p "$HOME/.local/share/trajectories/native"
|
|
21
|
+
tar -xzf trajectories-0.1.2-darwin-arm64.tar.gz -C "$HOME/.local/share/trajectories/native"
|
|
22
|
+
bash "$HOME/.local/share/trajectories/native/trajectories-0.1.2-darwin-arm64/install.sh"
|
|
23
|
+
trajectories --version
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
On Linux, choose `linux-x64` or `linux-arm64`, and use `sha256sum --check`.
|
|
27
|
+
The installer refuses existing commands unless `--force` explicitly authorizes
|
|
28
|
+
replacement. It creates a symlink to the retained release directory; do not
|
|
29
|
+
delete that directory or separate the executable from `lib/`. It does not change
|
|
30
|
+
the search database or enable an indexing service. Ensure `~/.local/bin` is on PATH.
|
|
31
|
+
Local ONNX inference requires glibc 2.28+ on Linux or macOS 14+ on Apple Silicon. Deep Agents SQLite conversion
|
|
32
|
+
still uses the harness's own Python/LangGraph environment.
|
|
33
|
+
|
|
34
|
+
The service installers below are for source/npm installations and invoke Bun.
|
|
35
|
+
For a native service, its command is the installed `trajectories sync` executable
|
|
36
|
+
directly; the CLI's bounded sync and scheduler protocol are unchanged.
|
|
37
|
+
|
|
38
|
+
### npm or source CLI
|
|
39
|
+
|
|
10
40
|
For the npm distribution, use Bun 1.4.2 or newer:
|
|
11
41
|
|
|
12
42
|
```sh
|
|
13
|
-
bun add --global trajectories@0.1.
|
|
43
|
+
bun add --global trajectories@0.1.2
|
|
14
44
|
trajectories --version
|
|
15
45
|
trajectories status --robot
|
|
16
46
|
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Run from an extracted native release kept in a permanent directory.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
RELEASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
5
|
+
BIN_DIR="${TRAJECTORIES_BIN_DIR:-$HOME/.local/bin}"
|
|
6
|
+
FORCE=0
|
|
7
|
+
while [[ $# -gt 0 ]]; do
|
|
8
|
+
case "$1" in
|
|
9
|
+
--bin-dir) [[ $# -ge 2 && -n "$2" ]] || { echo '--bin-dir requires a directory.' >&2; exit 1; }; BIN_DIR="$2"; shift 2 ;;
|
|
10
|
+
--force) FORCE=1; shift ;;
|
|
11
|
+
--help|-h) echo 'Usage: install.sh [--bin-dir DIR] [--force]'; exit 0 ;;
|
|
12
|
+
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
13
|
+
esac
|
|
14
|
+
done
|
|
15
|
+
[[ -x "$RELEASE_DIR/trajectories" && -d "$RELEASE_DIR/lib" ]] || {
|
|
16
|
+
echo 'Keep install.sh, the trajectories executable, and lib/ together in the extracted release.' >&2; exit 1;
|
|
17
|
+
}
|
|
18
|
+
"$RELEASE_DIR/trajectories" --version
|
|
19
|
+
mkdir -p "$BIN_DIR"
|
|
20
|
+
TARGET="$BIN_DIR/trajectories"
|
|
21
|
+
if [[ -e "$TARGET" || -L "$TARGET" ]]; then
|
|
22
|
+
if [[ -L "$TARGET" && "$(readlink "$TARGET")" == "$RELEASE_DIR/trajectories" ]]; then
|
|
23
|
+
echo "Already installed: $TARGET"; exit 0
|
|
24
|
+
fi
|
|
25
|
+
[[ "$FORCE" == 1 && ! -d "$TARGET" ]] || {
|
|
26
|
+
echo "Refusing to overwrite $TARGET; use --force for an explicit cutover." >&2; exit 1;
|
|
27
|
+
}
|
|
28
|
+
fi
|
|
29
|
+
STAGE="$(mktemp -d "$BIN_DIR/.trajectories-install.XXXXXX")"
|
|
30
|
+
trap 'rm -rf "$STAGE"' EXIT
|
|
31
|
+
ln -s "$RELEASE_DIR/trajectories" "$STAGE/trajectories"
|
|
32
|
+
mv -f "$STAGE/trajectories" "$TARGET"
|
|
33
|
+
echo "Installed: $TARGET"
|
|
34
|
+
echo "Keep the release directory: $RELEASE_DIR"
|
|
35
|
+
case ":$PATH:" in *":$BIN_DIR:"*) ;; *) echo "Add $BIN_DIR to PATH." ;; esac
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trajectories",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Portable coding-agent sessions: read, search, convert, and continue across harnesses.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
"bin": { "trajectories": "./dist/cli.js" },
|
|
10
10
|
"files": ["dist", "docs", "legacy/src", "legacy/bin", "legacy/scripts", "legacy/NOTICE.md", "ops", "skills", "README.md", "CHANGELOG.md", "LICENSE"],
|
|
11
11
|
"scripts": {
|
|
12
|
-
"build": "tsc -p tsconfig.build.json && bun build src/index.ts --target node --format esm --outfile dist/index.js && bun build src/normalization-runtime.ts --target bun --format esm --outfile dist/normalization-runtime.js && bun build src/cli.ts --target bun --format esm --outfile dist/cli.js && bun -
|
|
12
|
+
"build": "tsc -p tsconfig.build.json && bun build src/index.ts --target node --format esm --outfile dist/index.js && bun build src/normalization-runtime.ts --target bun --format esm --outfile dist/normalization-runtime.js && bun build src/cli.ts --target bun --format esm --external onnxruntime-node --define TRAJECTORIES_BUNDLED_CLI=true --outfile dist/cli.js && bun scripts/build-assets.ts",
|
|
13
|
+
"build:native": "bun run build && bun scripts/build-native.ts",
|
|
13
14
|
"test": "bun test ./test/*.test.ts",
|
|
14
15
|
"verify": "bun run build && tsc -p tsconfig.legacy.json && bun run test && bun test ./legacy/tests",
|
|
15
|
-
"test:
|
|
16
|
+
"test:native": "bun scripts/check-native.ts",
|
|
17
|
+
"test:package": "bun run build && bun test ./smoke/package.test.ts && bun run test:native"
|
|
16
18
|
},
|
|
17
19
|
"dependencies": { "@letta-ai/trajectory": "0.4.3" },
|
|
18
20
|
"optionalDependencies": { "onnxruntime-node": "^1.29.0" },
|
package/dist/cli.d.ts
DELETED