@inixiative/archive 0.11.1 → 0.12.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/README.md +5 -3
- package/package.json +2 -2
- package/src/cli.ts +8 -18
- package/src/client.ts +10 -3
- package/src/import.ts +49 -8
- package/src/remote.ts +19 -1
- package/src/server.ts +11 -0
package/README.md
CHANGED
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inixiative/archive",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
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.`,
|
|
@@ -312,24 +311,15 @@ export async function runCli(args = Bun.argv.slice(2)) {
|
|
|
312
311
|
if (results.some((r) => 'error' in r)) process.exitCode = 1;
|
|
313
312
|
return;
|
|
314
313
|
}
|
|
315
|
-
if (command === 'sync' || command === 'routes') {
|
|
316
|
-
const store = new ArchiveStore(databaseUrl());
|
|
317
|
-
try {
|
|
318
|
-
if (command === 'routes') output(await routingPreview(store, readDestinations(config)));
|
|
319
|
-
else {
|
|
320
|
-
const results = await syncArchives(store, readDestinations(config));
|
|
321
|
-
output(results);
|
|
322
|
-
if (results.some((r) => r.status.startsWith('failed'))) process.exitCode = 1;
|
|
323
|
-
}
|
|
324
|
-
} finally {
|
|
325
|
-
await store.close();
|
|
326
|
-
}
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
314
|
const token = process.env.ARCHIVE_TOKEN ?? readToken();
|
|
330
315
|
if (!token) throw new Error('No Archive token: run `archive init` or set ARCHIVE_TOKEN');
|
|
331
316
|
const archive = new ArchiveClient({ url: v.url ?? DEFAULT_URL, token });
|
|
332
|
-
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') {
|
|
333
323
|
if (
|
|
334
324
|
!v.file ||
|
|
335
325
|
!['codex', 'claude-code', 'chatgpt'].includes(v.source ?? '') ||
|
package/src/client.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
destinationToken,
|
|
7
7
|
destinationUrl,
|
|
8
8
|
} from './config';
|
|
9
|
+
import type { RoutingPreview, SyncResult } from './remote';
|
|
9
10
|
import type { ArchiveStore } from './store';
|
|
10
11
|
|
|
11
12
|
export { type ArchiveDestination, archiveDestinationSchema } from './config';
|
|
@@ -106,7 +107,10 @@ export async function publishArchive(
|
|
|
106
107
|
throw new Error('Archive changed repeatedly during publication; retry sync');
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
export async function routingPreview(
|
|
110
|
+
export async function routingPreview(
|
|
111
|
+
store: ArchiveStore,
|
|
112
|
+
destinations: ArchiveDestination[],
|
|
113
|
+
): Promise<RoutingPreview> {
|
|
110
114
|
return (await store.list()).map((archive) => ({
|
|
111
115
|
id: archive.id,
|
|
112
116
|
projectId: archive.projectId,
|
|
@@ -122,8 +126,11 @@ export async function routingPreview(store: ArchiveStore, destinations: ArchiveD
|
|
|
122
126
|
}));
|
|
123
127
|
}
|
|
124
128
|
|
|
125
|
-
export async function syncArchives(
|
|
126
|
-
|
|
129
|
+
export async function syncArchives(
|
|
130
|
+
store: ArchiveStore,
|
|
131
|
+
destinations: ArchiveDestination[],
|
|
132
|
+
): Promise<SyncResult[]> {
|
|
133
|
+
const results: SyncResult[] = [];
|
|
127
134
|
for (const archive of await store.list())
|
|
128
135
|
for (const destination of destinations.filter((d) => d.projectId === archive.projectId)) {
|
|
129
136
|
try {
|
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,7 +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 { DescribedDestination } from './config';
|
|
4
|
+
import type { ArchiveDestination, DescribedDestination } from './config';
|
|
5
5
|
import type { ArchiveChunk, ArchiveSnapshot } from './index';
|
|
6
6
|
import { archiveKey, archiveSnapshotSchema } from './index';
|
|
7
7
|
import type { ArchiveFilter, ArchiveSettings, TagDefinition } from './schemas';
|
|
@@ -22,6 +22,16 @@ export type DestinationStatus = DescribedDestination & { delivered: number; pend
|
|
|
22
22
|
/** A hosted Archive library a held Signet may write to. */
|
|
23
23
|
export type ArchiveLibrary = { integrationId: string; resourceId: string; name: string };
|
|
24
24
|
export type ArchiveLibraries = { paired: boolean; libraries: ArchiveLibrary[] };
|
|
25
|
+
export type RoutedDestination = Pick<ArchiveDestination, 'kind' | 'url'> & {
|
|
26
|
+
integrationId?: string;
|
|
27
|
+
};
|
|
28
|
+
export type RoutingPreview = (Pick<
|
|
29
|
+
ArchiveListing,
|
|
30
|
+
'id' | 'projectId' | 'tags' | 'suggestedTags'
|
|
31
|
+
> & {
|
|
32
|
+
destinations: RoutedDestination[];
|
|
33
|
+
})[];
|
|
34
|
+
export type SyncResult = { id: string; destination: string; status: string };
|
|
25
35
|
/** A project routed to a hosted Archive library through Kingdom. */
|
|
26
36
|
export type KingdomRoute = { projectId: string; integrationId: string; resourceId: string };
|
|
27
37
|
|
|
@@ -162,6 +172,14 @@ export class ArchiveClient {
|
|
|
162
172
|
libraries() {
|
|
163
173
|
return this.request<ArchiveLibraries>('destinations/libraries');
|
|
164
174
|
}
|
|
175
|
+
/** Each archive and the destinations its project routes to; nothing is sent. */
|
|
176
|
+
routes() {
|
|
177
|
+
return this.request<RoutingPreview>('destinations/routes');
|
|
178
|
+
}
|
|
179
|
+
/** Publishes every archive to its project's destinations once. */
|
|
180
|
+
sync() {
|
|
181
|
+
return this.request<SyncResult[]>('destinations/sync', {}, { timeoutMs: 600_000 });
|
|
182
|
+
}
|
|
165
183
|
/** Routes a project to a library; the server verifies its Signet grants sessions.write first. */
|
|
166
184
|
connectDestination(route: KingdomRoute) {
|
|
167
185
|
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) });
|