@msvesper/vesper-link 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.
Files changed (69) hide show
  1. package/LICENSE.md +44 -0
  2. package/README.md +26 -0
  3. package/dist/atomic-json.d.ts +3 -0
  4. package/dist/atomic-json.js +47 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +170 -0
  7. package/dist/collectors/claude.d.ts +5 -0
  8. package/dist/collectors/claude.js +42 -0
  9. package/dist/collectors/codex.d.ts +5 -0
  10. package/dist/collectors/codex.js +65 -0
  11. package/dist/collectors/copilot.d.ts +5 -0
  12. package/dist/collectors/copilot.js +64 -0
  13. package/dist/collectors/index.d.ts +4 -0
  14. package/dist/collectors/index.js +3 -0
  15. package/dist/collectors/types.d.ts +16 -0
  16. package/dist/collectors/types.js +1 -0
  17. package/dist/companion.d.ts +12 -0
  18. package/dist/companion.js +62 -0
  19. package/dist/config.d.ts +7 -0
  20. package/dist/config.js +41 -0
  21. package/dist/contracts.d.ts +83 -0
  22. package/dist/contracts.js +144 -0
  23. package/dist/diagnostics.d.ts +37 -0
  24. package/dist/diagnostics.js +64 -0
  25. package/dist/fs-utils.d.ts +5 -0
  26. package/dist/fs-utils.js +69 -0
  27. package/dist/hook-report.d.ts +8 -0
  28. package/dist/hook-report.js +21 -0
  29. package/dist/http-client.d.ts +25 -0
  30. package/dist/http-client.js +228 -0
  31. package/dist/install-args.d.ts +7 -0
  32. package/dist/install-args.js +35 -0
  33. package/dist/installation.d.ts +59 -0
  34. package/dist/installation.js +524 -0
  35. package/dist/installer.d.ts +59 -0
  36. package/dist/installer.js +214 -0
  37. package/dist/lock.d.ts +5 -0
  38. package/dist/lock.js +57 -0
  39. package/dist/managed-runtime.d.ts +33 -0
  40. package/dist/managed-runtime.js +130 -0
  41. package/dist/memory-mcp.d.ts +27 -0
  42. package/dist/memory-mcp.js +155 -0
  43. package/dist/operational-log.d.ts +17 -0
  44. package/dist/operational-log.js +48 -0
  45. package/dist/paths.d.ts +23 -0
  46. package/dist/paths.js +36 -0
  47. package/dist/pending.d.ts +7 -0
  48. package/dist/pending.js +35 -0
  49. package/dist/process-runner.d.ts +11 -0
  50. package/dist/process-runner.js +21 -0
  51. package/dist/provider-integrations.d.ts +42 -0
  52. package/dist/provider-integrations.js +339 -0
  53. package/dist/queue.d.ts +19 -0
  54. package/dist/queue.js +129 -0
  55. package/dist/scheduler.d.ts +51 -0
  56. package/dist/scheduler.js +176 -0
  57. package/dist/session.d.ts +50 -0
  58. package/dist/session.js +12 -0
  59. package/dist/source.d.ts +11 -0
  60. package/dist/source.js +45 -0
  61. package/dist/state.d.ts +11 -0
  62. package/dist/state.js +37 -0
  63. package/dist/sync.d.ts +18 -0
  64. package/dist/sync.js +247 -0
  65. package/dist/transport.d.ts +27 -0
  66. package/dist/transport.js +400 -0
  67. package/dist/version.d.ts +1 -0
  68. package/dist/version.js +1 -0
  69. package/package.json +39 -0
@@ -0,0 +1,228 @@
1
+ import * as fs from 'node:fs';
2
+ import { MAX_MANIFEST_BYTES, parseAddFactResponse, parseCapabilitiesResponse, parseHealthResponse, parseMemorySearchResponse, parseTranscriptResponse, validateManifest } from './contracts.js';
3
+ import { VESPER_LINK_VERSION } from './version.js';
4
+ const CLIENT_VERSION = VESPER_LINK_VERSION;
5
+ const REQUEST_TIMEOUT_MS = 30_000;
6
+ const UPLOAD_TIMEOUT_MS = 5 * 60_000;
7
+ const TASK_LOCAL_STATUS_BY_CODE = new Map([
8
+ ['invalid_json', 400],
9
+ ['invalid_manifest', 400],
10
+ ['unsupported_provider', 400],
11
+ ['length_required', 400],
12
+ ['length_mismatch', 400],
13
+ ['digest_mismatch', 400],
14
+ ['content_encoding_unsupported', 400],
15
+ ['source_changed', 400],
16
+ ['transcript_not_found', 404],
17
+ ['upload_busy', 409],
18
+ ['manifest_changed', 409],
19
+ ['file_too_large', 413],
20
+ ['manifest_too_large', 413],
21
+ ]);
22
+ function requestHeaders() {
23
+ return {
24
+ 'user-agent': `vesper-link/${CLIENT_VERSION}`,
25
+ 'x-vesper-client-version': VESPER_LINK_VERSION,
26
+ };
27
+ }
28
+ async function readJsonResponse(response) {
29
+ const text = await response.text();
30
+ try {
31
+ return JSON.parse(text);
32
+ }
33
+ catch {
34
+ throw new SystemicUploadError('malformed_response', `Server returned malformed JSON (${response.status})`);
35
+ }
36
+ }
37
+ function parseErrorEnvelope(value) {
38
+ if (!value || typeof value !== 'object') {
39
+ return null;
40
+ }
41
+ const envelope = value;
42
+ if (typeof envelope.requestId !== 'string' || !envelope.error
43
+ || typeof envelope.error.code !== 'string' || typeof envelope.error.message !== 'string'
44
+ || typeof envelope.error.retryable !== 'boolean') {
45
+ return null;
46
+ }
47
+ return envelope;
48
+ }
49
+ function parseRetryAfter(value, nowMs) {
50
+ if (!value) {
51
+ return undefined;
52
+ }
53
+ const trimmed = value.trim();
54
+ if (/^\d+$/.test(trimmed)) {
55
+ const seconds = Number(trimmed);
56
+ const retryAt = nowMs + seconds * 1_000;
57
+ return Number.isSafeInteger(seconds) && Number.isFinite(retryAt) ? new Date(retryAt).toISOString() : undefined;
58
+ }
59
+ const parsed = Date.parse(trimmed);
60
+ return Number.isNaN(parsed) ? undefined : new Date(parsed).toISOString();
61
+ }
62
+ async function expectSuccess(response) {
63
+ const value = await readJsonResponse(response);
64
+ if (response.ok) {
65
+ return value;
66
+ }
67
+ const envelope = parseErrorEnvelope(value);
68
+ if (!envelope) {
69
+ throw new SystemicUploadError('malformed_error', `Server returned malformed error (${response.status})`);
70
+ }
71
+ if (TASK_LOCAL_STATUS_BY_CODE.get(envelope.error.code) === response.status) {
72
+ throw new TaskUploadError(envelope.error.code, envelope.error.message, response.status);
73
+ }
74
+ const retryAfterUntil = response.status === 429
75
+ ? parseRetryAfter(response.headers.get('retry-after'), Date.now())
76
+ : undefined;
77
+ throw new SystemicUploadError(envelope.error.code, envelope.error.message, response.status, retryAfterUntil);
78
+ }
79
+ function parseSuccess(value, parse) {
80
+ try {
81
+ return parse(value);
82
+ }
83
+ catch (error) {
84
+ throw new SystemicUploadError('malformed_response', error instanceof Error ? error.message : String(error));
85
+ }
86
+ }
87
+ function endpoint(host, pathname) {
88
+ return `${host.replace(/\/$/, '')}/api/remote-memory/v1${pathname}`;
89
+ }
90
+ export async function getHealth(host, fetchImpl = fetch) {
91
+ let response;
92
+ try {
93
+ response = await fetchImpl(endpoint(host, '/health'), {
94
+ headers: requestHeaders(),
95
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
96
+ });
97
+ }
98
+ catch (error) {
99
+ throw new SystemicUploadError('network_error', String(error));
100
+ }
101
+ return parseSuccess(await expectSuccess(response), parseHealthResponse);
102
+ }
103
+ export async function getCapabilities(host, fetchImpl = fetch) {
104
+ let response;
105
+ try {
106
+ response = await fetchImpl(endpoint(host, '/capabilities'), {
107
+ headers: requestHeaders(),
108
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
109
+ });
110
+ }
111
+ catch (error) {
112
+ throw new SystemicUploadError('network_error', String(error));
113
+ }
114
+ return parseSuccess(await expectSuccess(response), parseCapabilitiesResponse);
115
+ }
116
+ async function postJson(host, pathname, bodyValue, fetchImpl) {
117
+ const body = JSON.stringify(bodyValue);
118
+ let response;
119
+ try {
120
+ response = await fetchImpl(endpoint(host, pathname), {
121
+ method: 'POST',
122
+ headers: { ...requestHeaders(), 'content-type': 'application/json', 'content-length': String(Buffer.byteLength(body)) },
123
+ body,
124
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
125
+ });
126
+ }
127
+ catch (error) {
128
+ throw new SystemicUploadError('network_error', String(error));
129
+ }
130
+ return expectSuccess(response);
131
+ }
132
+ export async function searchMemory(host, query, limit = 5, fetchImpl = fetch) {
133
+ return parseSuccess(await postJson(host, '/search', { query, limit }, fetchImpl), parseMemorySearchResponse);
134
+ }
135
+ export async function addFact(host, fact, fetchImpl = fetch) {
136
+ return parseSuccess(await postJson(host, '/facts', { fact, source: 'user_instruction' }, fetchImpl), parseAddFactResponse);
137
+ }
138
+ export async function getTranscriptStatus(host, transcriptId, fetchImpl = fetch) {
139
+ let response;
140
+ try {
141
+ response = await fetchImpl(endpoint(host, `/transcripts/${encodeURIComponent(transcriptId)}/status`), {
142
+ headers: requestHeaders(),
143
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
144
+ });
145
+ }
146
+ catch (error) {
147
+ throw new SystemicUploadError('network_error', String(error));
148
+ }
149
+ return parseSuccess(await expectSuccess(response), parseTranscriptResponse);
150
+ }
151
+ export async function postManifest(host, manifest, fetchImpl = fetch) {
152
+ try {
153
+ validateManifest(manifest);
154
+ }
155
+ catch (error) {
156
+ throw new TaskUploadError('invalid_manifest', error instanceof Error ? error.message : String(error));
157
+ }
158
+ const body = JSON.stringify(manifest);
159
+ if (Buffer.byteLength(body) > MAX_MANIFEST_BYTES) {
160
+ throw new TaskUploadError('manifest_too_large', 'Manifest exceeds 16 KiB');
161
+ }
162
+ let response;
163
+ try {
164
+ response = await fetchImpl(endpoint(host, '/transcripts'), {
165
+ method: 'POST',
166
+ headers: { ...requestHeaders(), 'content-type': 'application/json', 'content-length': String(Buffer.byteLength(body)) },
167
+ body,
168
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
169
+ });
170
+ }
171
+ catch (error) {
172
+ throw new SystemicUploadError('network_error', String(error));
173
+ }
174
+ return parseSuccess(await expectSuccess(response), parseTranscriptResponse);
175
+ }
176
+ export async function putContent(options) {
177
+ const fetchImpl = options.fetchImpl ?? fetch;
178
+ const body = fs.createReadStream(options.filePath);
179
+ let response;
180
+ try {
181
+ response = await fetchImpl(endpoint(options.host, `/transcripts/${encodeURIComponent(options.transcriptId)}/content`), {
182
+ method: 'PUT',
183
+ headers: {
184
+ ...requestHeaders(),
185
+ 'content-type': 'application/octet-stream',
186
+ 'content-length': String(options.byteLength),
187
+ },
188
+ body: body,
189
+ // Node fetch requires duplex for streaming request bodies.
190
+ duplex: 'half',
191
+ signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS),
192
+ });
193
+ }
194
+ catch (error) {
195
+ const cause = error instanceof Error ? error.cause : undefined;
196
+ const fileCode = error.code ?? cause?.code;
197
+ if (fileCode === 'ENOENT' || fileCode === 'EACCES' || fileCode === 'EPERM') {
198
+ throw new TaskUploadError('source_changed', `Source became unreadable while uploading: ${String(error)}`);
199
+ }
200
+ throw new SystemicUploadError('network_error', String(error));
201
+ }
202
+ finally {
203
+ body.destroy();
204
+ }
205
+ return parseSuccess(await expectSuccess(response), parseTranscriptResponse);
206
+ }
207
+ export class TaskUploadError extends Error {
208
+ code;
209
+ httpStatus;
210
+ constructor(code, message, httpStatus) {
211
+ super(message);
212
+ this.code = code;
213
+ this.httpStatus = httpStatus;
214
+ this.name = 'TaskUploadError';
215
+ }
216
+ }
217
+ export class SystemicUploadError extends Error {
218
+ code;
219
+ httpStatus;
220
+ retryAfterUntil;
221
+ constructor(code, message, httpStatus, retryAfterUntil) {
222
+ super(message);
223
+ this.code = code;
224
+ this.httpStatus = httpStatus;
225
+ this.retryAfterUntil = retryAfterUntil;
226
+ this.name = 'SystemicUploadError';
227
+ }
228
+ }
@@ -0,0 +1,7 @@
1
+ import type { ExpectedRole } from './installer.js';
2
+ export interface InstallArguments {
3
+ host: string;
4
+ expectedRole?: ExpectedRole;
5
+ bootstrapAuthKey?: string;
6
+ }
7
+ export declare function parseInstallArguments(args: string[], processArgs?: string[]): InstallArguments;
@@ -0,0 +1,35 @@
1
+ const AUTH_KEY_PATTERN = /^tskey-auth-[A-Za-z0-9_-]{16,512}$/;
2
+ export function parseInstallArguments(args, processArgs = process.argv) {
3
+ const values = new Map();
4
+ for (let index = 0; index < args.length; index += 1) {
5
+ const current = args[index];
6
+ if (!current.startsWith('--') || index + 1 >= args.length) {
7
+ throw new Error(`Invalid argument: ${current}`);
8
+ }
9
+ const valueIndex = index + 1;
10
+ const value = args[valueIndex];
11
+ values.set(current.slice(2), value);
12
+ if (current === '--auth-key') {
13
+ args[valueIndex] = '[redacted]';
14
+ for (let processIndex = 2; processIndex < processArgs.length - 1; processIndex += 1) {
15
+ if (processArgs[processIndex] === '--auth-key' && processArgs[processIndex + 1] === value) {
16
+ processArgs[processIndex + 1] = '[redacted]';
17
+ }
18
+ }
19
+ }
20
+ index = valueIndex;
21
+ }
22
+ const host = values.get('host');
23
+ const expectedRole = values.get('expect-role');
24
+ const bootstrapAuthKey = values.get('auth-key');
25
+ values.delete('auth-key');
26
+ if (!host || (expectedRole !== undefined && expectedRole !== 'uploader' && expectedRole !== 'trusted')
27
+ || (bootstrapAuthKey !== undefined && !AUTH_KEY_PATTERN.test(bootstrapAuthKey))) {
28
+ throw new Error('Usage: vesper-link install --host http://<Tailscale IPv4>:11723 [--expect-role uploader|trusted] [--auth-key <one-use-key>]');
29
+ }
30
+ return {
31
+ host,
32
+ expectedRole: expectedRole,
33
+ bootstrapAuthKey,
34
+ };
35
+ }
@@ -0,0 +1,59 @@
1
+ import type { CapabilitiesResponse } from './contracts.js';
2
+ import { type CurrentRuntimePointer, type ManagedRuntime } from './managed-runtime.js';
3
+ import type { LinkPaths } from './paths.js';
4
+ import { type ProcessRunner } from './process-runner.js';
5
+ import { type OwnedProviderIntegrations, type ProviderMutation, type SupportedProvider } from './provider-integrations.js';
6
+ import { type SchedulerState } from './scheduler.js';
7
+ export interface InstalledState {
8
+ schemaVersion: 2;
9
+ installedAt: string;
10
+ host: string;
11
+ sourceTrust: 'trusted' | 'uploader';
12
+ installationId: string;
13
+ runtime: CurrentRuntimePointer;
14
+ nodePath: string;
15
+ launcherPath: string;
16
+ minuteOffset: number;
17
+ scheduler: SchedulerState;
18
+ integrations: OwnedProviderIntegrations;
19
+ backups: Record<string, string>;
20
+ createdProviderFiles: string[];
21
+ paused?: boolean;
22
+ }
23
+ export interface InstallationStatus {
24
+ installed: boolean;
25
+ paused: boolean;
26
+ providers: SupportedProvider[];
27
+ claudeHookInstalled: boolean;
28
+ copilotHookInstalled: boolean;
29
+ sourceTrust?: 'trusted' | 'uploader';
30
+ schedulerInstalled: boolean;
31
+ schedulerKind?: 'windows-task' | 'cron';
32
+ minuteOffset?: number;
33
+ runtimeVersion?: string;
34
+ trustedIntegrationsInstalled: boolean;
35
+ }
36
+ export interface InstallFaults {
37
+ afterStep?(step: 'current' | 'config' | 'providers' | 'scheduler' | 'state'): Promise<void> | void;
38
+ afterProviderMutation?(mutation: ProviderMutation): Promise<void> | void;
39
+ }
40
+ export declare function verifyInstalledState(paths: LinkPaths, expected: InstalledState, runner?: ProcessRunner): Promise<void>;
41
+ export declare function readInstalledState(paths: LinkPaths): Promise<InstalledState | null>;
42
+ export declare function installManagedRuntime(options: {
43
+ paths: LinkPaths;
44
+ runtime: ManagedRuntime;
45
+ host: string;
46
+ capabilities: CapabilitiesResponse;
47
+ nodePath?: string;
48
+ platform?: NodeJS.Platform;
49
+ runner?: ProcessRunner;
50
+ providers?: SupportedProvider[];
51
+ now?: Date;
52
+ postInstallDoctor?: () => Promise<void>;
53
+ faults?: InstallFaults;
54
+ }): Promise<InstalledState>;
55
+ export declare function uninstallIntegrations(paths: LinkPaths, runner?: ProcessRunner, probeProcess?: (pid: number) => 'alive' | 'absent' | 'unknown'): Promise<boolean>;
56
+ export declare function pauseIntegrations(paths: LinkPaths, runner?: ProcessRunner): Promise<boolean>;
57
+ export declare function resumeIntegrations(paths: LinkPaths, runner?: ProcessRunner): Promise<boolean>;
58
+ export declare function resetRemoteIdentity(paths: LinkPaths, confirmed: boolean): Promise<void>;
59
+ export declare function getInstallationStatus(paths: LinkPaths, runner?: ProcessRunner): Promise<InstallationStatus>;