trajectories 0.1.2 → 0.1.4

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,221 @@
1
+ import { closeSync, constants, existsSync, fstatSync, openSync, readFileSync } from 'node:fs';
2
+ import { mkdir } from 'node:fs/promises';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { parseArgs } from 'node:util';
5
+ import { resolveCanonicalDbPath } from './canonical.js';
6
+ import { ensurePrivateDirectory } from './private-storage.js';
7
+ import { acquireWriterLease, isMemoryStorePath } from './writer-lease.js';
8
+ import { TrajectoryReader } from './reader.js';
9
+ import { TrajectoryDb } from './db.js';
10
+ import { listSessions } from '../../src/log.js';
11
+ import type { PageCursor } from '../../src/log-types.js';
12
+ import { TrajectoriesError } from '../../src/log-types.js';
13
+ import { exportManagedDelta } from '../../src/shared-managed.js';
14
+ import { safeId } from '../../src/log-io.js';
15
+ import { createSharedClient, createSharedHandler, sharedObservationId } from '../../src/shared.js';
16
+ import type { ObservedState } from '../../src/shared.js';
17
+
18
+ const help = `trajectories remote — cross-machine canonical history
19
+ remote serve --store <shared-directory> --token-file <0600-file>
20
+ [--host 127.0.0.1] [--port 8789]
21
+ remote push --url <endpoint> --token-file <0600-file> --machine <stable-id>
22
+ [--db <SQLite>] [--store <managed-store>] [--session <id>]
23
+ remote pull --url <endpoint> --token-file <0600-file> [--db <SQLite>]
24
+ [--store <managed-mirror>] [--machine <stable-origin-id>]
25
+ remote list --url <endpoint> --token-file <0600-file>
26
+ Token path fallback: TRAJECTORIES_REMOTE_TOKEN_FILE. Tokens never belong in URLs.
27
+ Push publishes observed canonical records; native files are not uploaded by this lane.
28
+ --store additionally synchronizes managed logs and their referenced objects.
29
+ Pull updates the existing SQLite lexical index; it never launches native harnesses.
30
+ Pull --machine skips that originating machine's observed rows when a local shared:// copy is already stored.
31
+ `;
32
+
33
+ function required(value: string | undefined, option: string): string {
34
+ if (!value) throw new Error(`${option} is required.`);
35
+ return value;
36
+ }
37
+
38
+ /** Read an owner-only regular file through the validated descriptor. */
39
+ function readToken(path: string): string {
40
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
41
+ try {
42
+ const stat = fstatSync(fd);
43
+ if (!stat.isFile() || stat.nlink !== 1 || (stat.mode & 0o777) !== 0o600 || (process.getuid && stat.uid !== process.getuid())) {
44
+ throw new Error('Remote token file must be an owner-owned regular file with mode 0600.');
45
+ }
46
+ if (stat.size > 4096) throw new Error('Remote token file is too large.');
47
+ const token = readFileSync(fd, 'utf8').trim();
48
+ if (!token || /[\s\x00-\x1f\x7f]/u.test(token)) throw new Error('Remote token file must contain one nonempty token.');
49
+ return token;
50
+ } finally { closeSync(fd); }
51
+ }
52
+
53
+ function project(state: ObservedState) {
54
+ const id = `shared:${state.id}`;
55
+ return {
56
+ session: { ...state.session, id, source_path: `shared://${state.machineId}/${encodeURIComponent(state.session.id)}`,
57
+ // The remote immutable head covers both record and metadata changes.
58
+ content_hash: state.head,
59
+ parent_session_id: state.session.parent_session_id
60
+ ? `shared:${sharedObservationId(state.machineId, state.session.parent_session_id)}` : null,
61
+ },
62
+ records: state.records.map(record => ({ ...record, id: `${id}:${record.id}`, session_id: id })),
63
+ };
64
+ }
65
+
66
+ function localSharedSession(dbPath: string, observationId: string) {
67
+ if (isMemoryStorePath(dbPath) || !existsSync(dbPath)) return null;
68
+ const reader = new TrajectoryReader(dbPath);
69
+ try {
70
+ const local = reader.getSession(`shared:${observationId}`);
71
+ return local?.source_path.startsWith('shared://') ? local : null;
72
+ } finally { reader.close(); }
73
+ }
74
+
75
+ export async function runSharedCli(argv: string[]): Promise<void> {
76
+ // Help must not open SQLite, managed stores, credentials or sockets.
77
+ if (!argv.length || argv.some(value => value === '--help' || value === '-h') || argv[0] === 'help') { console.log(help); return; }
78
+ const command = argv[0];
79
+ if (!['serve', 'push', 'pull', 'list'].includes(command!)) throw new Error('Expected remote serve, push, pull or list.');
80
+ const { values, positionals } = parseArgs({ args: argv.slice(1), strict: true, allowPositionals: true, options: {
81
+ store: { type: 'string' }, host: { type: 'string' }, port: { type: 'string' },
82
+ url: { type: 'string' }, 'token-file': { type: 'string' }, machine: { type: 'string' },
83
+ db: { type: 'string' }, session: { type: 'string' }, robot: { type: 'boolean' }, json: { type: 'boolean' },
84
+ }});
85
+ if (positionals.length) throw new Error('Unexpected remote positional arguments.');
86
+ const allowed: Record<string, Set<string>> = {
87
+ serve: new Set(['store', 'host', 'port', 'token-file', 'json', 'robot']),
88
+ push: new Set(['url', 'token-file', 'machine', 'db', 'store', 'session', 'json', 'robot']),
89
+ pull: new Set(['url', 'token-file', 'db', 'store', 'machine', 'json', 'robot']),
90
+ list: new Set(['url', 'token-file', 'json', 'robot']),
91
+ };
92
+ for (const option of Object.keys(values)) {
93
+ if (!allowed[command!]!.has(option)) throw new Error(`--${option} is not supported by remote ${command}.`);
94
+ }
95
+ const machine = values.machine === undefined ? undefined : safeId(values.machine);
96
+ const port = values.port === undefined ? 8789 : Number(values.port);
97
+ if (command === 'serve' && (!Number.isInteger(port) || port < 0 || port > 65535)) throw new Error('--port must be an integer from 0 through 65535.');
98
+ const token = readToken(required(values['token-file'] ?? process.env.TRAJECTORIES_REMOTE_TOKEN_FILE, '--token-file'));
99
+ const emit = (value: unknown) => console.log(JSON.stringify(value));
100
+ if (command === 'serve') {
101
+ const store = resolve(required(values.store, '--store'));
102
+ const server = Bun.serve({ hostname: values.host ?? '127.0.0.1', port,
103
+ maxRequestBodySize: 64 * 1024 * 1024, fetch: createSharedHandler({ store, token }),
104
+ });
105
+ const close = () => { server.stop(true); process.removeListener('SIGINT', close); process.removeListener('SIGTERM', close); };
106
+ process.on('SIGINT', close); process.on('SIGTERM', close);
107
+ emit({ command: 'serve', host: server.hostname, port: server.port });
108
+ return;
109
+ }
110
+ const client = createSharedClient({ url: required(values.url, '--url'), token });
111
+ if (command === 'list') {
112
+ const observed = await client.listObserved();
113
+ const managed = await client.listManaged();
114
+ emit({ observed: observed.map(item => ({ id: item.id, head: item.head, machineId: item.machineId,
115
+ source: item.session.source, records: item.session.record_count })),
116
+ managed: managed.map(item => ({ id: item.id, head: item.head })),
117
+ });
118
+ return;
119
+ }
120
+ const dbPath = values.db ?? resolveCanonicalDbPath();
121
+ if (command === 'push') {
122
+ const machineId = required(machine, '--machine');
123
+ const observed = [];
124
+ let selected = false;
125
+ // An explicit managed store without --db selects only the managed lane.
126
+ if (!values.store || values.db) {
127
+ const reader = new TrajectoryReader(dbPath);
128
+ try {
129
+ const ids = values.session ? [values.session] : reader.getAllSessions().map(session => session.id);
130
+ for (const id of ids) {
131
+ const captured = reader.readSessionForSync(id);
132
+ if (!captured) continue;
133
+ const { session, records } = captured;
134
+ // Derived mirrors and managed projections cannot become native originals.
135
+ if (session.source === 'managed' || session.id.startsWith('managed:') || session.source_path.startsWith('shared://')) continue;
136
+ selected = true;
137
+ const diagnostics = captured.diagnostics.map(item => ({ code: item.code,
138
+ occurrenceCount: item.occurrence_count, sample: item.sample, firstSeenAt: item.first_seen_at }));
139
+ observed.push(await client.pushObserved({ machineId, session: { ...session, diagnostics }, records }));
140
+ }
141
+ } finally { reader.close(); }
142
+ }
143
+ const managed = [];
144
+ if (values.store) {
145
+ const store = resolve(values.store);
146
+ let cursor: PageCursor | undefined;
147
+ do {
148
+ const page = await listSessions(store, { cursor, limit: 1000 });
149
+ for (const session of page.items) {
150
+ if (values.session && session.id !== values.session) continue;
151
+ selected = true;
152
+ managed.push(await client.pushManaged(store, session.id));
153
+ }
154
+ cursor = page.nextCursor ?? undefined;
155
+ } while (cursor);
156
+ }
157
+ if (values.session && !selected) throw new Error('Selected original session was not found; managed projections require --store.');
158
+ emit({ command, observed, managed });
159
+ return;
160
+ }
161
+ if (!isMemoryStorePath(dbPath)) {
162
+ if (!values.db && !process.env.TRAJECTORIES_DB && !process.env.LEDGER_DB) ensurePrivateDirectory(dirname(dbPath));
163
+ else await mkdir(dirname(dbPath), { recursive: true, mode: 0o700 });
164
+ }
165
+ const observed = [];
166
+ for (const item of await client.listObserved()) {
167
+ const local = localSharedSession(dbPath, item.id);
168
+ if (item.machineId === machine && local) {
169
+ observed.push({ id: item.id, head: item.head, outcome: 'skipped-origin' });
170
+ continue;
171
+ }
172
+ if (local && local.content_hash === item.head) {
173
+ observed.push({ id: item.id, head: item.head, records: local.record_count, outcome: 'unchanged' });
174
+ continue;
175
+ }
176
+ const state = await client.getObserved(item.id);
177
+ const lease = isMemoryStorePath(dbPath) ? null : acquireWriterLease(dbPath, { holder: 'trajectories remote pull' });
178
+ try {
179
+ const db = new TrajectoryDb(dbPath);
180
+ try {
181
+ const projection = project(state);
182
+ if (db.getTombstone(projection.session.id)) { observed.push({ id: item.id, head: state.head, outcome: 'deleted' }); continue; }
183
+ const saved = db.saveSession(projection.session, projection.records, state.session.diagnostics);
184
+ observed.push({ id: item.id, head: state.head, records: projection.records.length,
185
+ outcome: saved.inserted ? 'inserted' : saved.updated ? 'updated' : 'unchanged' });
186
+ } finally { db.close(); }
187
+ } finally { lease?.close(); }
188
+ }
189
+ const managed = [];
190
+ let conflicts = 0;
191
+ if (values.store) {
192
+ const store = resolve(values.store);
193
+ for (const session of await client.listManaged()) {
194
+ try { managed.push({ ...await client.pullManaged(store, session.id), outcome: 'pulled' }); }
195
+ catch (error) {
196
+ if (!(error instanceof TrajectoriesError) || !['STALE_HEAD', 'INTENT_CONFLICT'].includes(error.code)) throw error;
197
+ // The failed pull may have observed a newer branch than the original list.
198
+ const remote = (await client.listManaged()).find(item => item.id === session.id);
199
+ let ahead;
200
+ try { if (remote) ahead = await exportManagedDelta(store, session.id, { afterHead: remote.head }); }
201
+ catch (proofError) {
202
+ if (!(proofError instanceof TrajectoriesError) || !['STALE_HEAD', 'NOT_FOUND'].includes(proofError.code)) throw proofError;
203
+ }
204
+ if (remote && ahead?.frames.length && ahead.head !== remote.head) {
205
+ managed.push({ id: session.id, head: ahead.head, remoteHead: remote.head, outcome: 'ahead' });
206
+ } else {
207
+ conflicts++;
208
+ managed.push({ id: session.id, remoteHead: remote?.head, outcome: 'conflict', code: error.code });
209
+ }
210
+ }
211
+ }
212
+ // Existing indexer preserves accepted managed ordering, FTS and tombstones.
213
+ // Its summary is emitted separately and contains no record bodies.
214
+ if (managed.length) {
215
+ const { runManagedCli } = await import('./managed.js');
216
+ await runManagedCli('index', ['--store', resolve(values.store), '--db', dbPath, '--robot']);
217
+ }
218
+ }
219
+ emit({ command, observed, managed });
220
+ if (conflicts) throw new TrajectoriesError('STALE_HEAD', `${conflicts} managed session(s) conflict; other histories were pulled and indexed. Local branches were preserved.`);
221
+ }
package/package.json CHANGED
@@ -1,13 +1,35 @@
1
1
  {
2
2
  "name": "trajectories",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Portable coding-agent sessions: read, search, convert, and continue across harnesses.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
- "engines": { "node": ">=22.13.0", "bun": ">=1.4.2" },
8
- "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
9
- "bin": { "trajectories": "./dist/cli.js" },
10
- "files": ["dist", "docs", "legacy/src", "legacy/bin", "legacy/scripts", "legacy/NOTICE.md", "ops", "skills", "README.md", "CHANGELOG.md", "LICENSE"],
7
+ "engines": {
8
+ "node": ">=22.13.0",
9
+ "bun": ">=1.4.2"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "bin": {
18
+ "trajectories": "./dist/cli.js"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "docs",
23
+ "legacy/src",
24
+ "legacy/bin",
25
+ "legacy/scripts",
26
+ "legacy/NOTICE.md",
27
+ "ops",
28
+ "skills",
29
+ "README.md",
30
+ "CHANGELOG.md",
31
+ "LICENSE"
32
+ ],
11
33
  "scripts": {
12
34
  "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
35
  "build:native": "bun run build && bun scripts/build-native.ts",
@@ -16,9 +38,24 @@
16
38
  "test:native": "bun scripts/check-native.ts",
17
39
  "test:package": "bun run build && bun test ./smoke/package.test.ts && bun run test:native"
18
40
  },
19
- "dependencies": { "@letta-ai/trajectory": "0.4.3" },
20
- "optionalDependencies": { "onnxruntime-node": "^1.29.0" },
21
- "devDependencies": { "@types/node": "^22.20.1", "@types/bun": "1.3.14", "typescript": "^5.9.3" },
22
- "repository": { "type": "git", "url": "git+https://github.com/archeism/oh-my-ai.git", "directory": "packages/trajectories" },
23
- "publishConfig": { "access": "public" }
41
+ "dependencies": {
42
+ "@letta-ai/trajectory": "0.4.3"
43
+ },
44
+ "optionalDependencies": {
45
+ "onnxruntime-node": "^1.29.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^22.20.1",
49
+ "@types/bun": "1.3.14",
50
+ "typescript": "^5.9.3"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/archeism/oh-my-ai.git",
55
+ "directory": "packages/trajectories"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ },
60
+ "gitHead": "7cd6e650f748e57ef14c50b5d9067ebc52da8155"
24
61
  }