@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
package/LICENSE.md ADDED
@@ -0,0 +1,44 @@
1
+ # Vesper Link End User License Agreement (EULA)
2
+
3
+ **Copyright (c) 2026 VSWork. All Rights Reserved.**
4
+
5
+ ## 1. Grant of License
6
+
7
+ This End User License Agreement ("Agreement") grants you ("User") a limited, non-exclusive, non-transferable, revocable license to install and use Vesper Link ("Software") for personal or internal business purposes, subject to the terms below.
8
+
9
+ ## 2. Restrictions
10
+
11
+ You may NOT:
12
+ - Copy, modify, adapt, translate, or create derivative works of the Software
13
+ - Reverse engineer, decompile, disassemble, or attempt to derive the source code
14
+ - Redistribute, sublicense, rent, lease, or lend the Software to any third party
15
+ - Remove or alter any proprietary notices, labels, or marks on the Software
16
+ - Use the Software for any unlawful purpose or in violation of any applicable laws
17
+
18
+ ## 3. Intellectual Property
19
+
20
+ The Software, including all copies, modifications, and derivative works, is the exclusive property of VSWork and is protected by copyright and other intellectual property laws. This Agreement does not grant you any ownership rights to the Software.
21
+
22
+ ## 4. No Warranty
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU.
25
+
26
+ ## 5. Limitation of Liability
27
+
28
+ IN NO EVENT SHALL VSWORK BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, LOSS OF DATA, BUSINESS INTERRUPTION, OR LOSS OF PROFITS) ARISING OUT OF OR IN CONNECTION WITH THIS AGREEMENT OR THE USE OF THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
29
+
30
+ ## 6. Termination
31
+
32
+ This license is effective until terminated. Your rights under this license will terminate automatically without notice if you fail to comply with any of its terms. Upon termination, you must destroy all copies of the Software in your possession.
33
+
34
+ ## 7. Governing Law
35
+
36
+ This Agreement shall be governed by and construed in accordance with the laws of the jurisdiction in which VSWork operates, without regard to its conflict of law provisions.
37
+
38
+ ## 8. Entire Agreement
39
+
40
+ This Agreement constitutes the entire agreement between you and VSWork regarding the Software and supersedes all prior agreements and understandings, whether written or oral.
41
+
42
+ ---
43
+
44
+ By installing or using this Software, you acknowledge that you have read, understood, and agree to be bound by the terms of this Agreement.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # vesper-link
2
+
3
+ **Vesper Memory Link — One Memory across all your devices.**
4
+
5
+ `vesper-link` connects a supported Windows or Linux device to Vesper Memory. It can:
6
+
7
+ - Sync completed Claude Code, GitHub Copilot CLI, and OpenAI Codex sessions to your Vesper Host.
8
+ - Let full-access devices search and add Vesper Memory through MCP.
9
+
10
+ ## Connect a Device
11
+
12
+ On the Windows device running Vesper, open **Memory Links** and copy a connection command. Run it on the device you want to connect; no global installation is required.
13
+
14
+ Node.js 20 or later is required. V1 supports Windows x64 and Linux x64.
15
+
16
+ ## Manage the Connection
17
+
18
+ ```bash
19
+ npx --yes @msvesper/vesper-link@0.1.4 status
20
+ npx --yes @msvesper/vesper-link@0.1.4 pause
21
+ npx --yes @msvesper/vesper-link@0.1.4 resume
22
+ npx --yes @msvesper/vesper-link@0.1.4 doctor
23
+ npx --yes @msvesper/vesper-link@0.1.4 uninstall
24
+ ```
25
+
26
+ Session transcripts are sent to the Vesper Host selected during setup and retained there. They are not uploaded to a Vesper-operated cloud service. Uninstalling the connection does not delete the original transcripts or Memory already saved on the Host.
@@ -0,0 +1,3 @@
1
+ export declare function readJsonFile<T>(filePath: string): Promise<T | null>;
2
+ export declare function writeJsonAtomic(filePath: string, value: unknown): Promise<void>;
3
+ export declare function listJsonFiles(directory: string): Promise<string[]>;
@@ -0,0 +1,47 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { randomBytes } from 'node:crypto';
4
+ export async function readJsonFile(filePath) {
5
+ try {
6
+ const raw = await fs.readFile(filePath, 'utf8');
7
+ return JSON.parse(raw);
8
+ }
9
+ catch (error) {
10
+ if (error.code === 'ENOENT') {
11
+ return null;
12
+ }
13
+ throw error;
14
+ }
15
+ }
16
+ export async function writeJsonAtomic(filePath, value) {
17
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
18
+ const tempPath = `${filePath}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
19
+ try {
20
+ const handle = await fs.open(tempPath, 'wx');
21
+ try {
22
+ await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, 'utf8');
23
+ await handle.sync();
24
+ }
25
+ finally {
26
+ await handle.close();
27
+ }
28
+ await fs.rename(tempPath, filePath);
29
+ }
30
+ finally {
31
+ await fs.rm(tempPath, { force: true }).catch(() => undefined);
32
+ }
33
+ }
34
+ export async function listJsonFiles(directory) {
35
+ try {
36
+ const entries = await fs.readdir(directory, { withFileTypes: true });
37
+ return entries
38
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
39
+ .map((entry) => path.join(directory, entry.name));
40
+ }
41
+ catch (error) {
42
+ if (error.code === 'ENOENT') {
43
+ return [];
44
+ }
45
+ throw error;
46
+ }
47
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ import { writeConfig } from './config.js';
3
+ import { getDoctorReport, getStatusReport } from './diagnostics.js';
4
+ import { resolveHookReport } from './hook-report.js';
5
+ import { pauseIntegrations, resetRemoteIdentity, resumeIntegrations, uninstallIntegrations } from './installation.js';
6
+ import { parseInstallArguments } from './install-args.js';
7
+ import { installVesperLink } from './installer.js';
8
+ import { runMemoryMcp } from './memory-mcp.js';
9
+ import { appendSyncLog } from './operational-log.js';
10
+ import { writePendingMarker } from './pending.js';
11
+ import { resolveLinkPaths } from './paths.js';
12
+ import { runSyncOnce } from './sync.js';
13
+ import { VESPER_LINK_VERSION } from './version.js';
14
+ const USAGE = 'Usage: vesper-link <configure|report-session|sync-once|scheduled-sync|mcp|install|pause|resume|uninstall|reset-identity|status|doctor>';
15
+ function flags(args) {
16
+ const result = new Map();
17
+ for (let index = 0; index < args.length; index += 1) {
18
+ const current = args[index];
19
+ if (!current.startsWith('--') || index + 1 >= args.length) {
20
+ throw new Error(`Invalid argument: ${current}`);
21
+ }
22
+ result.set(current.slice(2), args[++index]);
23
+ }
24
+ return result;
25
+ }
26
+ async function readStdinJson() {
27
+ if (process.stdin.isTTY) {
28
+ return null;
29
+ }
30
+ const chunks = [];
31
+ for await (const chunk of process.stdin) {
32
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
33
+ }
34
+ const text = Buffer.concat(chunks).toString('utf8').trim();
35
+ return text ? JSON.parse(text) : null;
36
+ }
37
+ async function main() {
38
+ const [command, ...args] = process.argv.slice(2);
39
+ if (command === '--help' || command === '-h') {
40
+ process.stdout.write(USAGE + String.fromCharCode(10));
41
+ return;
42
+ }
43
+ if (command === '--version' || command === '-v') {
44
+ process.stdout.write(VESPER_LINK_VERSION + String.fromCharCode(10));
45
+ return;
46
+ }
47
+ const paths = resolveLinkPaths();
48
+ if (command === 'configure') {
49
+ const values = flags(args);
50
+ const host = values.get('host');
51
+ if (!host) {
52
+ throw new Error('Usage: vesper-link configure --host <url>');
53
+ }
54
+ const config = await writeConfig(paths.configPath, host);
55
+ process.stdout.write(`${JSON.stringify(config)}\n`);
56
+ return;
57
+ }
58
+ if (command === 'report-session') {
59
+ const values = flags(args);
60
+ const stdin = await readStdinJson();
61
+ const report = resolveHookReport(values, stdin, paths);
62
+ const markerPath = await writePendingMarker(paths.pendingDir, {
63
+ schemaVersion: 1,
64
+ provider: report.provider,
65
+ providerSessionId: report.providerSessionId,
66
+ transcriptPath: report.transcriptPath,
67
+ queuedAt: new Date().toISOString(),
68
+ });
69
+ process.stdout.write(`${JSON.stringify({ queued: true, markerPath })}\n`);
70
+ return;
71
+ }
72
+ if (command === 'sync-once') {
73
+ const summary = await runSyncOnce({ paths });
74
+ process.stdout.write(`${JSON.stringify(summary)}\n`);
75
+ return;
76
+ }
77
+ if (command === 'mcp') {
78
+ if (args.length > 0) {
79
+ throw new Error('Usage: vesper-link mcp');
80
+ }
81
+ await runMemoryMcp(paths);
82
+ return;
83
+ }
84
+ if (command === 'scheduled-sync') {
85
+ flags(args);
86
+ try {
87
+ const summary = await runSyncOnce({ paths });
88
+ await appendSyncLog(paths.logsDir, {
89
+ timestamp: new Date().toISOString(), event: 'sync_completed',
90
+ discovered: summary.discovered, queued: summary.queued, uploaded: summary.uploaded,
91
+ unchanged: summary.unchanged, failed: summary.failed,
92
+ collectorIssueCount: summary.collectorIssues.length, stoppedBy: summary.stoppedBy,
93
+ });
94
+ process.stdout.write(`${JSON.stringify(summary)}\n`);
95
+ }
96
+ catch (error) {
97
+ await appendSyncLog(paths.logsDir, {
98
+ timestamp: new Date().toISOString(), event: 'sync_failed',
99
+ errorCode: typeof error.code === 'string' ? error.code : undefined,
100
+ errorType: error instanceof Error ? error.name : typeof error,
101
+ });
102
+ throw error;
103
+ }
104
+ return;
105
+ }
106
+ if (command === 'install') {
107
+ const { host, expectedRole, bootstrapAuthKey } = parseInstallArguments(args);
108
+ const result = await installVesperLink({
109
+ paths, host, expectedRole, bootstrapAuthKey,
110
+ onNeedsLogin: (url) => process.stderr.write(`Authorize vesper-link: ${url}\n`),
111
+ onWaitingForTag: (progress) => process.stderr.write(`Waiting for ${progress.expectedRole ? `tag:vesper-${progress.expectedRole}` : 'one Vesper capability tag'} on ${progress.nodeName ?? 'this node'} (${progress.tailnetName ?? 'unknown tailnet'})...\n`),
112
+ });
113
+ process.stdout.write(`${JSON.stringify({
114
+ installed: true, minuteOffset: result.state.minuteOffset,
115
+ sourceTrust: result.state.sourceTrust, firstSyncStarted: result.firstSyncStarted,
116
+ })}\n`);
117
+ return;
118
+ }
119
+ if (command === 'uninstall') {
120
+ process.stdout.write(`${JSON.stringify({ removed: await uninstallIntegrations(paths) })}\n`);
121
+ return;
122
+ }
123
+ if (command === 'pause') {
124
+ if (args.length > 0) {
125
+ throw new Error('Usage: vesper-link pause');
126
+ }
127
+ process.stdout.write(JSON.stringify({ paused: await pauseIntegrations(paths) }) + String.fromCharCode(10));
128
+ return;
129
+ }
130
+ if (command === 'resume') {
131
+ if (args.length > 0) {
132
+ throw new Error('Usage: vesper-link resume');
133
+ }
134
+ const resumed = await resumeIntegrations(paths);
135
+ if (resumed) {
136
+ const summary = await runSyncOnce({ paths });
137
+ process.stdout.write(JSON.stringify({ resumed: true, sync: summary }) + String.fromCharCode(10));
138
+ }
139
+ else {
140
+ process.stdout.write(JSON.stringify({ resumed: false }) + String.fromCharCode(10));
141
+ }
142
+ return;
143
+ }
144
+ if (command === 'reset-identity') {
145
+ const values = flags(args);
146
+ if (values.get('confirm') !== 'RESET') {
147
+ throw new Error('Usage: vesper-link reset-identity --confirm RESET');
148
+ }
149
+ await resetRemoteIdentity(paths, true);
150
+ process.stdout.write(`${JSON.stringify({ reset: true })}\n`);
151
+ return;
152
+ }
153
+ if (command === 'status') {
154
+ process.stdout.write(`${JSON.stringify(await getStatusReport(paths))}\n`);
155
+ return;
156
+ }
157
+ if (command === 'doctor') {
158
+ const report = await getDoctorReport(paths);
159
+ process.stdout.write(`${JSON.stringify(report)}\n`);
160
+ if (!report.healthy) {
161
+ process.exitCode = 1;
162
+ }
163
+ return;
164
+ }
165
+ throw new Error(USAGE);
166
+ }
167
+ main().catch((error) => {
168
+ process.stderr.write(`vesper-link: ${error instanceof Error ? error.message : String(error)}\n`);
169
+ process.exitCode = 1;
170
+ });
@@ -0,0 +1,5 @@
1
+ import type { SessionCandidate } from '../session.js';
2
+ import type { CollectorContext, SessionCollector } from './types.js';
3
+ export declare class ClaudeCollector implements SessionCollector {
4
+ collect(context: CollectorContext): Promise<SessionCandidate[]>;
5
+ }
@@ -0,0 +1,42 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { normalizeProjectKey, pathToOpaqueFileUri, readFirstLine, walkFiles } from '../fs-utils.js';
4
+ export class ClaudeCollector {
5
+ async collect(context) {
6
+ const files = await walkFiles(context.claudeProjectsDir, (filePath) => filePath.endsWith('.jsonl'));
7
+ const candidates = [];
8
+ for (const transcriptPath of files) {
9
+ try {
10
+ const stat = await fs.stat(transcriptPath);
11
+ let cwdUri;
12
+ try {
13
+ const firstRecord = JSON.parse(await readFirstLine(transcriptPath));
14
+ if (typeof firstRecord.cwd === 'string' && firstRecord.cwd) {
15
+ cwdUri = pathToOpaqueFileUri(firstRecord.cwd);
16
+ }
17
+ }
18
+ catch {
19
+ // Claude's native project key is sufficient when optional cwd metadata is unavailable.
20
+ }
21
+ candidates.push({
22
+ provider: 'claude-code',
23
+ format: 'claude-code-session-jsonl',
24
+ formatVersion: 'observed-v1',
25
+ providerSessionId: path.basename(transcriptPath, '.jsonl'),
26
+ transcriptPath,
27
+ projectKey: normalizeProjectKey(path.relative(context.claudeProjectsDir, transcriptPath).split(path.sep)[0]),
28
+ cwdUri,
29
+ sourceCreatedAt: stat.birthtime.toISOString(),
30
+ sourceUpdatedAt: stat.mtime.toISOString(),
31
+ size: stat.size,
32
+ mtimeMs: stat.mtimeMs,
33
+ discovery: 'scan',
34
+ });
35
+ }
36
+ catch (error) {
37
+ context.reportIssue?.({ provider: 'claude-code', code: 'source_unreadable', message: String(error) });
38
+ }
39
+ }
40
+ return candidates;
41
+ }
42
+ }
@@ -0,0 +1,5 @@
1
+ import { type SessionCandidate } from '../session.js';
2
+ import type { CollectorContext, SessionCollector } from './types.js';
3
+ export declare class CodexCollector implements SessionCollector {
4
+ collect(context: CollectorContext): Promise<SessionCandidate[]>;
5
+ }
@@ -0,0 +1,65 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { pathToOpaqueFileUri, pathToProjectKey, readFirstLine, walkFiles } from '../fs-utils.js';
4
+ import { stableSessionKey } from '../session.js';
5
+ async function readCandidate(transcriptPath, archived) {
6
+ const firstLine = await readFirstLine(transcriptPath);
7
+ const record = JSON.parse(firstLine);
8
+ if (record.type !== 'session_meta' || !record.payload?.id) {
9
+ throw new Error(`Codex rollout has no first-record session_meta.id: ${transcriptPath}`);
10
+ }
11
+ if (record.payload.session_id && record.payload.session_id !== record.payload.id) {
12
+ throw new Error(`Codex session_meta id/session_id mismatch: ${transcriptPath}`);
13
+ }
14
+ const stat = await fs.stat(transcriptPath);
15
+ const createdValue = record.payload.timestamp || record.timestamp;
16
+ const createdAt = createdValue && !Number.isNaN(Date.parse(createdValue))
17
+ ? new Date(createdValue).toISOString()
18
+ : stat.birthtime.toISOString();
19
+ return {
20
+ provider: 'openai-codex',
21
+ format: 'openai-codex-rollout-jsonl',
22
+ formatVersion: record.payload.cli_version || 'observed-v1',
23
+ providerSessionId: record.payload.id,
24
+ transcriptPath,
25
+ projectKey: record.payload.cwd ? pathToProjectKey(record.payload.cwd) : undefined,
26
+ cwdUri: record.payload.cwd ? pathToOpaqueFileUri(record.payload.cwd) : undefined,
27
+ sourceCreatedAt: createdAt,
28
+ sourceUpdatedAt: stat.mtime.toISOString(),
29
+ size: stat.size,
30
+ mtimeMs: stat.mtimeMs,
31
+ discovery: 'scan',
32
+ archived,
33
+ };
34
+ }
35
+ export class CodexCollector {
36
+ async collect(context) {
37
+ const roots = [
38
+ { directory: path.join(context.codexHome, 'sessions'), archived: false },
39
+ { directory: path.join(context.codexHome, 'archived_sessions'), archived: true },
40
+ ];
41
+ const fileGroups = await Promise.all(roots.map(async ({ directory, archived }) => {
42
+ const files = await walkFiles(directory, (filePath) => path.basename(filePath).startsWith('rollout-') && filePath.endsWith('.jsonl'));
43
+ return files.map((filePath) => ({ filePath, archived }));
44
+ }));
45
+ const discovered = [];
46
+ for (const { filePath, archived } of fileGroups.flat()) {
47
+ try {
48
+ discovered.push(await readCandidate(filePath, archived));
49
+ }
50
+ catch (error) {
51
+ context.reportIssue?.({ provider: 'openai-codex', code: 'invalid_session_meta', message: String(error) });
52
+ }
53
+ }
54
+ const byIdentity = new Map();
55
+ for (const candidate of discovered) {
56
+ const key = stableSessionKey(candidate.provider, candidate.providerSessionId);
57
+ const current = byIdentity.get(key);
58
+ if (!current || candidate.mtimeMs > current.mtimeMs
59
+ || (candidate.mtimeMs === current.mtimeMs && candidate.archived && !current.archived)) {
60
+ byIdentity.set(key, candidate);
61
+ }
62
+ }
63
+ return [...byIdentity.values()];
64
+ }
65
+ }
@@ -0,0 +1,5 @@
1
+ import type { SessionCandidate } from '../session.js';
2
+ import type { CollectorContext, SessionCollector } from './types.js';
3
+ export declare class CopilotCollector implements SessionCollector {
4
+ collect(context: CollectorContext): Promise<SessionCandidate[]>;
5
+ }
@@ -0,0 +1,64 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { pathToOpaqueFileUri, pathToProjectKey, walkFiles } from '../fs-utils.js';
4
+ function parseYamlScalars(text) {
5
+ const values = {};
6
+ for (const line of text.split(/\r?\n/)) {
7
+ const match = /^([A-Za-z_][A-Za-z0-9_]*):\s*(.*?)\s*$/.exec(line);
8
+ if (!match) {
9
+ continue;
10
+ }
11
+ let value = match[2];
12
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
13
+ value = value.slice(1, -1);
14
+ }
15
+ values[match[1]] = value;
16
+ }
17
+ return values;
18
+ }
19
+ export class CopilotCollector {
20
+ async collect(context) {
21
+ const files = await walkFiles(context.copilotSessionsDir, (filePath) => path.basename(filePath) === 'events.jsonl');
22
+ const candidates = [];
23
+ for (const transcriptPath of files) {
24
+ try {
25
+ const stat = await fs.stat(transcriptPath);
26
+ const directory = path.dirname(transcriptPath);
27
+ const fallbackId = path.basename(directory);
28
+ let metadata = {};
29
+ try {
30
+ metadata = parseYamlScalars(await fs.readFile(path.join(directory, 'workspace.yaml'), 'utf8'));
31
+ }
32
+ catch (error) {
33
+ if (error.code !== 'ENOENT') {
34
+ throw error;
35
+ }
36
+ }
37
+ const createdAt = metadata.created_at && !Number.isNaN(Date.parse(metadata.created_at))
38
+ ? new Date(metadata.created_at).toISOString()
39
+ : stat.birthtime.toISOString();
40
+ const updatedAt = metadata.updated_at && !Number.isNaN(Date.parse(metadata.updated_at))
41
+ ? new Date(metadata.updated_at).toISOString()
42
+ : stat.mtime.toISOString();
43
+ candidates.push({
44
+ provider: 'github-copilot',
45
+ format: 'github-copilot-events-jsonl',
46
+ formatVersion: 'observed-v1',
47
+ providerSessionId: metadata.id || fallbackId,
48
+ transcriptPath,
49
+ projectKey: metadata.cwd ? pathToProjectKey(metadata.cwd) : undefined,
50
+ cwdUri: metadata.cwd ? pathToOpaqueFileUri(metadata.cwd) : undefined,
51
+ sourceCreatedAt: createdAt,
52
+ sourceUpdatedAt: updatedAt,
53
+ size: stat.size,
54
+ mtimeMs: stat.mtimeMs,
55
+ discovery: 'scan',
56
+ });
57
+ }
58
+ catch (error) {
59
+ context.reportIssue?.({ provider: 'github-copilot', code: 'source_unreadable', message: String(error) });
60
+ }
61
+ }
62
+ return candidates;
63
+ }
64
+ }
@@ -0,0 +1,4 @@
1
+ export { ClaudeCollector } from './claude.js';
2
+ export { CopilotCollector } from './copilot.js';
3
+ export { CodexCollector } from './codex.js';
4
+ export type { CollectorContext, CollectorIssue, SessionCollector } from './types.js';
@@ -0,0 +1,3 @@
1
+ export { ClaudeCollector } from './claude.js';
2
+ export { CopilotCollector } from './copilot.js';
3
+ export { CodexCollector } from './codex.js';
@@ -0,0 +1,16 @@
1
+ import type { SessionCandidate } from '../session.js';
2
+ export interface CollectorContext {
3
+ nowMs: number;
4
+ claudeProjectsDir: string;
5
+ copilotSessionsDir: string;
6
+ codexHome: string;
7
+ reportIssue?: (issue: CollectorIssue) => void;
8
+ }
9
+ export interface CollectorIssue {
10
+ provider: 'claude-code' | 'github-copilot' | 'openai-codex';
11
+ code: string;
12
+ message: string;
13
+ }
14
+ export interface SessionCollector {
15
+ collect(context: CollectorContext): Promise<SessionCandidate[]>;
16
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ export declare const VESPER_NET_PROTOCOL_VERSION = 2;
2
+ export interface CompanionInfo {
3
+ executable: string;
4
+ packageName: string;
5
+ version: string;
6
+ }
7
+ export type CompanionProtocolRunner = (executable: string, environment: NodeJS.ProcessEnv) => Promise<{
8
+ stdout: string;
9
+ stderr: string;
10
+ }>;
11
+ export declare function assertCompanionProtocol(companion: CompanionInfo, environment: NodeJS.ProcessEnv, runner?: CompanionProtocolRunner): Promise<void>;
12
+ export declare function resolveCompanion(platform?: NodeJS.Platform, architecture?: string, resolvePackageJson?: (specifier: string) => string, mainPackageJsonPath?: string): Promise<CompanionInfo>;
@@ -0,0 +1,62 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import { constants as fsConstants } from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { createRequire } from 'node:module';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { execFile } from 'node:child_process';
7
+ export const VESPER_NET_PROTOCOL_VERSION = 2;
8
+ function defaultProtocolRunner(executable, environment) {
9
+ return new Promise((resolve, reject) => {
10
+ execFile(executable, ['--protocol-version'], {
11
+ env: environment, windowsHide: true, timeout: 5_000, maxBuffer: 16 * 1024, encoding: 'utf8',
12
+ }, (error, stdout, stderr) => {
13
+ if (error) {
14
+ reject(new Error(`vesper-net protocol probe failed: ${error.message}`));
15
+ return;
16
+ }
17
+ resolve({ stdout, stderr });
18
+ });
19
+ });
20
+ }
21
+ export async function assertCompanionProtocol(companion, environment, runner = defaultProtocolRunner) {
22
+ const result = await runner(companion.executable, environment);
23
+ if (result.stderr.trim() !== '' || result.stdout.trim() !== String(VESPER_NET_PROTOCOL_VERSION)) {
24
+ throw new Error(`vesper-net protocol mismatch: expected ${VESPER_NET_PROTOCOL_VERSION}`);
25
+ }
26
+ }
27
+ async function readPackageMetadata(filePath) {
28
+ return JSON.parse(await fs.readFile(filePath, 'utf8'));
29
+ }
30
+ export async function resolveCompanion(platform = process.platform, architecture = process.arch, resolvePackageJson = (specifier) => createRequire(import.meta.url).resolve(specifier), mainPackageJsonPath = fileURLToPath(new URL('../package.json', import.meta.url))) {
31
+ const packageName = platform === 'win32' && architecture === 'x64'
32
+ ? '@msvesper/vesper-net-win32-x64'
33
+ : platform === 'linux' && architecture === 'x64'
34
+ ? '@msvesper/vesper-net-linux-x64'
35
+ : null;
36
+ if (!packageName) {
37
+ throw new Error(`Unsupported vesper-net platform: ${platform}-${architecture}`);
38
+ }
39
+ const mainMetadata = await readPackageMetadata(mainPackageJsonPath);
40
+ if (typeof mainMetadata.version !== 'string') {
41
+ throw new Error('vesper-link package version is unavailable');
42
+ }
43
+ const optionalDependencies = mainMetadata.optionalDependencies;
44
+ if (!optionalDependencies || typeof optionalDependencies !== 'object'
45
+ || optionalDependencies[packageName] !== mainMetadata.version) {
46
+ throw new Error(`vesper-link must depend on ${packageName}@${mainMetadata.version} exactly`);
47
+ }
48
+ let packageJsonPath;
49
+ try {
50
+ packageJsonPath = resolvePackageJson(`${packageName}/package.json`);
51
+ }
52
+ catch {
53
+ throw new Error(`Missing optional companion package ${packageName}@${mainMetadata.version}`);
54
+ }
55
+ const companionMetadata = await readPackageMetadata(packageJsonPath);
56
+ if (companionMetadata.name !== packageName || companionMetadata.version !== mainMetadata.version) {
57
+ throw new Error(`Companion package mismatch: expected ${packageName}@${mainMetadata.version}`);
58
+ }
59
+ const executable = path.join(path.dirname(packageJsonPath), 'bin', platform === 'win32' ? 'vesper-net.exe' : 'vesper-net');
60
+ await fs.access(executable, platform === 'win32' ? fsConstants.F_OK : fsConstants.X_OK);
61
+ return { executable, packageName, version: mainMetadata.version };
62
+ }
@@ -0,0 +1,7 @@
1
+ export interface LinkConfig {
2
+ schemaVersion: 1;
3
+ host: string;
4
+ }
5
+ export declare function normalizeHost(value: string): string;
6
+ export declare function readConfig(configPath: string): Promise<LinkConfig>;
7
+ export declare function writeConfig(configPath: string, host: string): Promise<LinkConfig>;
package/dist/config.js ADDED
@@ -0,0 +1,41 @@
1
+ import { isIPv4 } from 'node:net';
2
+ import { readJsonFile, writeJsonAtomic } from './atomic-json.js';
3
+ function isTailscaleIpv4(hostname) {
4
+ if (!isIPv4(hostname)) {
5
+ return false;
6
+ }
7
+ const octets = hostname.split('.').map(Number);
8
+ return octets.length === 4
9
+ && octets.every((octet) => Number.isInteger(octet) && octet >= 0 && octet <= 255)
10
+ && octets[0] === 100
11
+ && octets[1] >= 64
12
+ && octets[1] <= 127;
13
+ }
14
+ export function normalizeHost(value) {
15
+ const url = new URL(value);
16
+ if (url.protocol !== 'http:') {
17
+ throw new Error('host must use http');
18
+ }
19
+ if (url.username || url.password || url.search || url.hash || (url.pathname !== '' && url.pathname !== '/')) {
20
+ throw new Error('host must not contain credentials, path, query, or fragment');
21
+ }
22
+ if (!isTailscaleIpv4(url.hostname)) {
23
+ throw new Error('host must use a Tailscale IPv4 address in 100.64.0.0/10');
24
+ }
25
+ if (url.port !== '11723') {
26
+ throw new Error('host must use port 11723');
27
+ }
28
+ return `http://${url.hostname}:11723`;
29
+ }
30
+ export async function readConfig(configPath) {
31
+ const config = await readJsonFile(configPath);
32
+ if (!config || config.schemaVersion !== 1 || typeof config.host !== 'string') {
33
+ throw new Error(`Missing or invalid config at ${configPath}; run vesper-link configure --host <url>`);
34
+ }
35
+ return { schemaVersion: 1, host: normalizeHost(config.host) };
36
+ }
37
+ export async function writeConfig(configPath, host) {
38
+ const config = { schemaVersion: 1, host: normalizeHost(host) };
39
+ await writeJsonAtomic(configPath, config);
40
+ return config;
41
+ }