@inixiative/archive 0.11.0 → 0.12.0
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/README.md +6 -4
- package/compose.yaml +1 -1
- package/package.json +2 -2
- package/src/cli.ts +15 -18
- package/src/import.ts +49 -8
- package/src/remote.ts +11 -0
- package/src/server.ts +11 -0
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ archive up # creates ~/.local/share/archive/server.token, then docker compo
|
|
|
21
21
|
archive down
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
`up` runs the `compose.yaml` that ships in the package (project `archive`) with `ARCHIVE_DATA_DIR` set to the archive home and the server token from its `server.token`. Postgres data lives in `$ARCHIVE_DATA_DIR/postgres` and the server's configuration (`destinations.json`, destination token files, the Kingdom pairing) in `$ARCHIVE_DATA_DIR/config`, so both survive rebuilds and volume prunes. Compose mounts the config directory at the same absolute path, so the CLI and the server read one `destinations.json` and every path in it resolves for both. Running Compose directly requires an absolute `ARCHIVE_DATA_DIR
|
|
24
|
+
`up` runs the `compose.yaml` that ships in the package (project `archive`) with `ARCHIVE_DATA_DIR` set to the archive home and the server token from its `server.token`. Postgres data lives in `$ARCHIVE_DATA_DIR/postgres` and the server's configuration (`destinations.json`, destination token files, the Kingdom pairing) in `$ARCHIVE_DATA_DIR/config`, so both survive rebuilds and volume prunes. Compose mounts the config directory at the same absolute path, so the CLI and the server read one `destinations.json` and every path in it resolves for both. `up` runs the image tagged with the CLI's own version, so a cached `latest` can never sit under a newer config layout; set `ARCHIVE_VERSION` to run another. Running Compose directly requires an absolute `ARCHIVE_DATA_DIR` and an `ARCHIVE_VERSION`; build from a checkout with `ARCHIVE_VERSION=local docker compose -f compose.yaml -f compose.build.yaml up -d --build`.
|
|
25
25
|
|
|
26
26
|
The server applies its schema migrations on start and requires bearer authentication for data endpoints; `/health` has no session data. Every other command talks to it over HTTP at `--url` (default `http://127.0.0.1:4700`) with the token in `--token-file` (default `~/.local/share/archive/server.token`) or `ARCHIVE_TOKEN`:
|
|
27
27
|
|
|
@@ -48,7 +48,7 @@ archive connect --url https://your-archive.example --project-id inixiative --tok
|
|
|
48
48
|
archive search --remote --query 'migration'
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
`setup` is an alias for `connect`. Setup verifies access before saving `<home>/config/destinations.json` and does not upload; the Compose server (`serve --sync`) reads it and publishes every 30 seconds. `sync` and `routes` run the same publication, or preview it, once
|
|
51
|
+
`setup` is an alias for `connect`. Setup verifies access before saving `<home>/config/destinations.json` and does not upload; the Compose server (`serve --sync`) reads it and publishes every 30 seconds. `sync` and `routes` ask that server to run the same publication, or preview it, once. The URL locates the server; the token grants access. Configuration stores only the token file path (`tokenFile`) or environment variable name (`--token-env`, `tokenEnv`), never the token. A token file is read on every request, so rotating it needs no restart; symlinks and files readable by group or others are refused. Redirects are refused; only HTTPS or loopback HTTP is allowed.
|
|
52
52
|
|
|
53
53
|
## Always-on local archiving
|
|
54
54
|
|
|
@@ -112,8 +112,10 @@ The server manages routes over HTTP too, so Foundry can read and set them. Only
|
|
|
112
112
|
| `destinations/libraries` | none | `paired`, and the `libraries` (`integrationId`, `resourceId`, `name`) the held Signets may write to |
|
|
113
113
|
| `destinations/connect` | `projectId`, `integrationId`, `resourceId` | the saved destination, after Kingdom confirms `sessions.write` on the library (403 otherwise) |
|
|
114
114
|
| `destinations/remove` | `projectId`, `integrationId`, `resourceId` | `removed` |
|
|
115
|
+
| `destinations/routes` | none | each archive with the destinations its project routes to; nothing is sent |
|
|
116
|
+
| `destinations/sync` | none | one publication to every destination: `id`, `destination`, `status` per archive and route |
|
|
115
117
|
|
|
116
|
-
`@inixiative/archive/remote`'s `ArchiveClient` wraps these as `destinations()`, `libraries()`, `connectDestination()` and `
|
|
118
|
+
`@inixiative/archive/remote`'s `ArchiveClient` wraps these as `destinations()`, `libraries()`, `connectDestination()`, `removeDestination()`, `routes()` and `sync()`. The sync loop picks up changes within 30 seconds.
|
|
117
119
|
|
|
118
120
|
## Tags, references, actors and retention
|
|
119
121
|
|
|
@@ -144,7 +146,7 @@ All data endpoints are `POST /api/v1/archive/<action>` with a JSON body and `Aut
|
|
|
144
146
|
| `delete` | `archiveId` | deleted |
|
|
145
147
|
| `settings/read`, `settings/update` | `integrations?`, `retentionDays?` | settings |
|
|
146
148
|
| `tags/list`, `tags/define`, `tags/remove` | `tag`, `actorId?`, `description?` | offered tags with usage counts |
|
|
147
|
-
| `destinations/list`, `destinations/libraries`, `destinations/connect`, `destinations/remove` | see [Routing](#routing) | routes |
|
|
149
|
+
| `destinations/list`, `destinations/libraries`, `destinations/connect`, `destinations/remove`, `destinations/routes`, `destinations/sync` | see [Routing](#routing) | routes |
|
|
148
150
|
|
|
149
151
|
Filters are `projectId`, `source`, `tag`, `actorId`, `model`, `effort` and `reference` (`{integration, ref}`). Jev integration is not enabled. Session content must not be sent to Jev without selecting that service for the relevant ownership boundary.
|
|
150
152
|
|
package/compose.yaml
CHANGED
|
@@ -19,7 +19,7 @@ services:
|
|
|
19
19
|
timeout: 5s
|
|
20
20
|
retries: 20
|
|
21
21
|
archive:
|
|
22
|
-
image: ghcr.io/inixiative/archive:${ARCHIVE_VERSION
|
|
22
|
+
image: ghcr.io/inixiative/archive:${ARCHIVE_VERSION:?archive up sets ARCHIVE_VERSION to the CLI's own version}
|
|
23
23
|
command: ["bun", "run", "src/cli.ts", "serve", "--home", "${ARCHIVE_DATA_DIR}", "--hostname", "0.0.0.0", "--sync"]
|
|
24
24
|
depends_on:
|
|
25
25
|
postgres:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inixiative/archive",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@biomejs/biome": "2.5.2",
|
|
46
|
-
"@inixiative/config": "^0.5.
|
|
46
|
+
"@inixiative/config": "^0.5.66",
|
|
47
47
|
"@types/bun": "1.4.2",
|
|
48
48
|
"lefthook": "2.1.9",
|
|
49
49
|
"typescript": "6.0.3"
|
package/src/cli.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
writeAgents,
|
|
17
17
|
} from './agents';
|
|
18
18
|
import { importChatGPTThread } from './chatgpt';
|
|
19
|
-
import { archiveRequest,
|
|
19
|
+
import { archiveRequest, searchRemotes, syncArchives } from './client';
|
|
20
20
|
import { collectSessions } from './collector';
|
|
21
21
|
import {
|
|
22
22
|
archiveDestinationSchema,
|
|
@@ -30,7 +30,6 @@ import { pairWithKingdom, verifyKingdomDestination } from './kingdom';
|
|
|
30
30
|
import { previewImports } from './preview';
|
|
31
31
|
import { ArchiveClient, DEFAULT_URL } from './remote';
|
|
32
32
|
import { startArchiveServer } from './server';
|
|
33
|
-
import { ArchiveStore } from './store';
|
|
34
33
|
|
|
35
34
|
/** Applies pending schema migrations to the archive database. */
|
|
36
35
|
export function migrate(databaseUrl: string) {
|
|
@@ -97,7 +96,7 @@ export async function runCli(args = Bun.argv.slice(2)) {
|
|
|
97
96
|
'pair --kingdom KINGDOM_API_URL [--name NAME] [--yes] (register this Archive with Kingdom; claim the review code there, then confirm the owner)\n' +
|
|
98
97
|
'connect --url HTTPS_URL --project-id ID (--token-env ENV | --token-file PATH) (a remote Archive, for serve --sync)\n' +
|
|
99
98
|
'connect --kind kingdom --url KINGDOM_API_URL --credential-file PATH --integration-id UUID --resource-id UUID --project-id ID (a hosted Archive through Kingdom)\n' +
|
|
100
|
-
'sync | routes (
|
|
99
|
+
'sync | routes (the server publishes to, or previews, its destinations once)\n' +
|
|
101
100
|
'agents add-collector --name N --source codex|claude-code --project-id ID --project-root DIR [--project-root ...] [--directory HISTORY] [--worktrees] [--atlas]\n' +
|
|
102
101
|
'agents remove-collector --name N | server --url URL | install | uninstall | status\n' +
|
|
103
102
|
`Data commands talk to the Archive server at --url (default ${DEFAULT_URL}) with the token in --token-file (default <home>/server.token) or ARCHIVE_TOKEN.`,
|
|
@@ -117,6 +116,12 @@ export async function runCli(args = Bun.argv.slice(2)) {
|
|
|
117
116
|
output({ initialized: true, home, tokenFile });
|
|
118
117
|
return;
|
|
119
118
|
}
|
|
119
|
+
const packageVersion = () =>
|
|
120
|
+
(
|
|
121
|
+
JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as {
|
|
122
|
+
version: string;
|
|
123
|
+
}
|
|
124
|
+
).version;
|
|
120
125
|
if (command === 'up' || command === 'down') {
|
|
121
126
|
// The machine's one local Archive: the bundled compose file, with its data in the home.
|
|
122
127
|
if (command === 'up') init();
|
|
@@ -135,6 +140,7 @@ export async function runCli(args = Bun.argv.slice(2)) {
|
|
|
135
140
|
env: {
|
|
136
141
|
...process.env,
|
|
137
142
|
ARCHIVE_DATA_DIR: home,
|
|
143
|
+
ARCHIVE_VERSION: process.env.ARCHIVE_VERSION ?? packageVersion(),
|
|
138
144
|
// Compose requires the variable even to stop; down never needs the real token.
|
|
139
145
|
ARCHIVE_SERVER_TOKEN: readToken() ?? 'x'.repeat(32),
|
|
140
146
|
},
|
|
@@ -305,24 +311,15 @@ export async function runCli(args = Bun.argv.slice(2)) {
|
|
|
305
311
|
if (results.some((r) => 'error' in r)) process.exitCode = 1;
|
|
306
312
|
return;
|
|
307
313
|
}
|
|
308
|
-
if (command === 'sync' || command === 'routes') {
|
|
309
|
-
const store = new ArchiveStore(databaseUrl());
|
|
310
|
-
try {
|
|
311
|
-
if (command === 'routes') output(await routingPreview(store, readDestinations(config)));
|
|
312
|
-
else {
|
|
313
|
-
const results = await syncArchives(store, readDestinations(config));
|
|
314
|
-
output(results);
|
|
315
|
-
if (results.some((r) => r.status.startsWith('failed'))) process.exitCode = 1;
|
|
316
|
-
}
|
|
317
|
-
} finally {
|
|
318
|
-
await store.close();
|
|
319
|
-
}
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
314
|
const token = process.env.ARCHIVE_TOKEN ?? readToken();
|
|
323
315
|
if (!token) throw new Error('No Archive token: run `archive init` or set ARCHIVE_TOKEN');
|
|
324
316
|
const archive = new ArchiveClient({ url: v.url ?? DEFAULT_URL, token });
|
|
325
|
-
if (command === '
|
|
317
|
+
if (command === 'routes') output(await archive.routes());
|
|
318
|
+
else if (command === 'sync') {
|
|
319
|
+
const results = await archive.sync();
|
|
320
|
+
output(results);
|
|
321
|
+
if (results.some((r) => r.status.startsWith('failed'))) process.exitCode = 1;
|
|
322
|
+
} else if (command === 'import') {
|
|
326
323
|
if (
|
|
327
324
|
!v.file ||
|
|
328
325
|
!['codex', 'claude-code', 'chatgpt'].includes(v.source ?? '') ||
|
package/src/import.ts
CHANGED
|
@@ -7,6 +7,47 @@ export interface ImportOptions {
|
|
|
7
7
|
title?: string;
|
|
8
8
|
projectId?: string;
|
|
9
9
|
}
|
|
10
|
+
/** Content the harness wraps in or adds to a user message; the person never typed it. */
|
|
11
|
+
const injectedTags = [
|
|
12
|
+
'system-reminder',
|
|
13
|
+
'local-command-caveat',
|
|
14
|
+
'local-command-stdout',
|
|
15
|
+
'local-command-stderr',
|
|
16
|
+
'command-name',
|
|
17
|
+
'command-message',
|
|
18
|
+
'command-args',
|
|
19
|
+
'bash-input',
|
|
20
|
+
'bash-stdout',
|
|
21
|
+
'bash-stderr',
|
|
22
|
+
'task-notification',
|
|
23
|
+
'user-prompt-submit-hook',
|
|
24
|
+
'fork-boilerplate',
|
|
25
|
+
'recommended_plugins',
|
|
26
|
+
'environment_context',
|
|
27
|
+
'user_instructions',
|
|
28
|
+
'permissions',
|
|
29
|
+
'INSTRUCTIONS',
|
|
30
|
+
].join('|');
|
|
31
|
+
const injectedBlock = new RegExp(`<(${injectedTags})(?:\\s[^>]*)?>[\\s\\S]*?</\\1>`, 'g');
|
|
32
|
+
const injectedOpening = new RegExp(`^<(?:${injectedTags})(?:>|\\s)`);
|
|
33
|
+
/** Foundry's native-session envelope; the person's text is the first User Message section. */
|
|
34
|
+
const foundryUserMessage =
|
|
35
|
+
/^# User Message[ \t]*\n([\s\S]*?)(?=\n# (?:User Message|Assistant Message|Foundry Injection)[ \t]*\n|(?![\s\S]))/m;
|
|
36
|
+
|
|
37
|
+
/** What the person typed in a user message, without injected content; undefined when nothing is left. */
|
|
38
|
+
function typedPrompt(text: string) {
|
|
39
|
+
let prompt = text
|
|
40
|
+
.replace(injectedBlock, '')
|
|
41
|
+
.replace(/<\/?pasted_content(?:\s[^>]*)?>/g, '')
|
|
42
|
+
.replace(/^\s*# AGENTS\.md instructions for [^\n]*/, '')
|
|
43
|
+
.trim();
|
|
44
|
+
if (/^# (?:System Context|User Message)[ \t]*\n/.test(prompt))
|
|
45
|
+
prompt = prompt.match(foundryUserMessage)?.[1].trim() ?? '';
|
|
46
|
+
if (!prompt || injectedOpening.test(prompt) || /^\[Request interrupted by user/.test(prompt))
|
|
47
|
+
return undefined;
|
|
48
|
+
return prompt;
|
|
49
|
+
}
|
|
50
|
+
|
|
10
51
|
export function importTranscript(text: string, options: ImportOptions): ArchiveSnapshot {
|
|
11
52
|
return importTranscriptLines(text.split('\n'), options);
|
|
12
53
|
}
|
|
@@ -19,6 +60,8 @@ export function importTranscriptLines(
|
|
|
19
60
|
let sessionId = options.sessionId;
|
|
20
61
|
let reasoning = false;
|
|
21
62
|
const seen = new Map<string, string>();
|
|
63
|
+
let prompt: string | undefined;
|
|
64
|
+
let metaPrompt: string | undefined;
|
|
22
65
|
let index = -1;
|
|
23
66
|
let contentBytes = 0;
|
|
24
67
|
// Codex states the model per turn; Claude Code per assistant record.
|
|
@@ -75,6 +118,11 @@ export function importTranscriptLines(
|
|
|
75
118
|
'Supported transcript content exceeds the archive size limit; no archive imported',
|
|
76
119
|
);
|
|
77
120
|
seen.set(id, identity);
|
|
121
|
+
// Claude Code marks expanded commands and caveats as meta: a title only when nothing was typed.
|
|
122
|
+
if (kind === 'user' && !prompt && !row.isCompactSummary) {
|
|
123
|
+
if (!row.isMeta) prompt = typedPrompt(content);
|
|
124
|
+
else metaPrompt ??= typedPrompt(content);
|
|
125
|
+
}
|
|
78
126
|
entries.push({
|
|
79
127
|
id,
|
|
80
128
|
kind,
|
|
@@ -165,14 +213,7 @@ export function importTranscriptLines(
|
|
|
165
213
|
}
|
|
166
214
|
if (!sessionId || !entries.length)
|
|
167
215
|
throw new Error('Transcript must identify a session and contain supported records');
|
|
168
|
-
const
|
|
169
|
-
(entry) =>
|
|
170
|
-
entry.kind === 'user' &&
|
|
171
|
-
!/^<(?:recommended_plugins|environment_context|user_instructions|permissions)(?:>|\s)/.test(
|
|
172
|
-
entry.text.trimStart(),
|
|
173
|
-
),
|
|
174
|
-
);
|
|
175
|
-
const title = options.title ?? firstPrompt?.text.slice(0, 160) ?? sessionId;
|
|
216
|
+
const title = options.title ?? (prompt ?? metaPrompt)?.slice(0, 160) ?? sessionId;
|
|
176
217
|
return archiveSnapshotSchema.parse({
|
|
177
218
|
schemaVersion: 1,
|
|
178
219
|
sourceId: options.sourceId,
|
package/src/remote.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { defaultHome } from './agents';
|
|
4
|
+
import type { routingPreview, syncArchives } from './client';
|
|
4
5
|
import type { DescribedDestination } from './config';
|
|
5
6
|
import type { ArchiveChunk, ArchiveSnapshot } from './index';
|
|
6
7
|
import { archiveKey, archiveSnapshotSchema } from './index';
|
|
@@ -22,6 +23,8 @@ export type DestinationStatus = DescribedDestination & { delivered: number; pend
|
|
|
22
23
|
/** A hosted Archive library a held Signet may write to. */
|
|
23
24
|
export type ArchiveLibrary = { integrationId: string; resourceId: string; name: string };
|
|
24
25
|
export type ArchiveLibraries = { paired: boolean; libraries: ArchiveLibrary[] };
|
|
26
|
+
export type RoutingPreview = Awaited<ReturnType<typeof routingPreview>>;
|
|
27
|
+
export type SyncResult = Awaited<ReturnType<typeof syncArchives>>[number];
|
|
25
28
|
/** A project routed to a hosted Archive library through Kingdom. */
|
|
26
29
|
export type KingdomRoute = { projectId: string; integrationId: string; resourceId: string };
|
|
27
30
|
|
|
@@ -162,6 +165,14 @@ export class ArchiveClient {
|
|
|
162
165
|
libraries() {
|
|
163
166
|
return this.request<ArchiveLibraries>('destinations/libraries');
|
|
164
167
|
}
|
|
168
|
+
/** Each archive and the destinations its project routes to; nothing is sent. */
|
|
169
|
+
routes() {
|
|
170
|
+
return this.request<RoutingPreview>('destinations/routes');
|
|
171
|
+
}
|
|
172
|
+
/** Publishes every archive to its project's destinations once. */
|
|
173
|
+
sync() {
|
|
174
|
+
return this.request<SyncResult[]>('destinations/sync', {}, { timeoutMs: 600_000 });
|
|
175
|
+
}
|
|
165
176
|
/** Routes a project to a library; the server verifies its Signet grants sessions.write first. */
|
|
166
177
|
connectDestination(route: KingdomRoute) {
|
|
167
178
|
return this.request<DescribedDestination>('destinations/connect', route);
|
package/src/server.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { routingPreview, syncArchives } from './client';
|
|
3
4
|
import {
|
|
4
5
|
connectDestination,
|
|
5
6
|
describeDestination,
|
|
@@ -195,6 +196,16 @@ export function createArchiveHandler(
|
|
|
195
196
|
});
|
|
196
197
|
return json({ data: { destinations } });
|
|
197
198
|
}
|
|
199
|
+
if (config && path === '/api/v1/archive/destinations/routes') {
|
|
200
|
+
z.strictObject({}).parse(body);
|
|
201
|
+
return json({
|
|
202
|
+
data: await routingPreview(store, readDestinations(config.destinationsFile)),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (config && path === '/api/v1/archive/destinations/sync') {
|
|
206
|
+
z.strictObject({}).parse(body);
|
|
207
|
+
return json({ data: await syncArchives(store, readDestinations(config.destinationsFile)) });
|
|
208
|
+
}
|
|
198
209
|
if (config && path === '/api/v1/archive/destinations/libraries') {
|
|
199
210
|
z.strictObject({}).parse(body);
|
|
200
211
|
return json({ data: await pairedLibraries(config.kingdomDirectory) });
|