@msvesper/vesper-link 0.1.4 → 0.1.5
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/LICENSE.md +44 -44
- package/README.md +45 -26
- package/dist/cli.js +24 -4
- package/dist/collectors/copilot.js +82 -1
- package/dist/collectors/types.d.ts +1 -0
- package/dist/config.d.ts +3 -1
- package/dist/config.js +24 -3
- package/dist/installation.d.ts +1 -0
- package/dist/installation.js +14 -1
- package/dist/installer.d.ts +7 -0
- package/dist/installer.js +46 -3
- package/dist/managed-runtime.js +19 -15
- package/dist/paths.d.ts +3 -1
- package/dist/paths.js +22 -1
- package/dist/scheduler.d.ts +1 -0
- package/dist/scheduler.js +25 -4
- package/dist/sync.js +5 -1
- package/dist/transport.d.ts +1 -0
- package/dist/transport.js +4 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +39 -39
package/LICENSE.md
CHANGED
|
@@ -1,44 +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.
|
|
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
CHANGED
|
@@ -1,26 +1,45 @@
|
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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, VS Code Copilot Chat, 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
|
+
The Remote must already have Node.js 20+ and npm 9+. V1 supports Windows x64 and Linux x64.
|
|
15
|
+
|
|
16
|
+
Linux also requires the `crontab` command for periodic session sync. Install it before running the copied command, then ensure the service is running:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Ubuntu/Debian
|
|
20
|
+
sudo apt-get install cron
|
|
21
|
+
sudo systemctl enable --now cron
|
|
22
|
+
|
|
23
|
+
# Fedora/RHEL
|
|
24
|
+
sudo dnf install cronie
|
|
25
|
+
sudo systemctl enable --now crond
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Vesper Link does not use `sudo` or install operating-system packages for you.
|
|
29
|
+
|
|
30
|
+
VS Code Copilot Chat transcripts are discovered from local VS Code workspace storage and from VS Code Server storage on Remote SSH hosts. On Windows this includes `%APPDATA%\Code\User\workspaceStorage` and the default `%USERPROFILE%\.vscode-server\data\User\workspaceStorage`; on Linux it includes `~/.config/Code/User/workspaceStorage` and `~/.vscode-server/data/User/workspaceStorage`. `VSCODE_AGENT_FOLDER` is honored when set. Vesper Link reads the per-session `GitHub.copilot-chat/transcripts/*.jsonl` files, not Copilot's global `session-store.db`.
|
|
31
|
+
|
|
32
|
+
## Manage the Connection
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx --yes @msvesper/vesper-link@host-v1 upgrade
|
|
36
|
+
npx --yes @msvesper/vesper-link@host-v1 status
|
|
37
|
+
npx --yes @msvesper/vesper-link@host-v1 pause
|
|
38
|
+
npx --yes @msvesper/vesper-link@host-v1 resume
|
|
39
|
+
npx --yes @msvesper/vesper-link@host-v1 doctor
|
|
40
|
+
npx --yes @msvesper/vesper-link@host-v1 uninstall
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`upgrade` reads the existing Host and access role from `~/.vesper-link/`, replaces the managed runtime and integrations transactionally, and preserves the Remote identity and queued work. It accepts no Host or auth key. If the existing installation, configuration, or Tailscale identity is missing or inconsistent, it stops without relinking. Resume a paused Link before upgrading.
|
|
44
|
+
|
|
45
|
+
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.
|
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import { readConfig, writeConfig } from './config.js';
|
|
3
4
|
import { getDoctorReport, getStatusReport } from './diagnostics.js';
|
|
4
5
|
import { resolveHookReport } from './hook-report.js';
|
|
5
6
|
import { pauseIntegrations, resetRemoteIdentity, resumeIntegrations, uninstallIntegrations } from './installation.js';
|
|
6
7
|
import { parseInstallArguments } from './install-args.js';
|
|
7
|
-
import { installVesperLink } from './installer.js';
|
|
8
|
+
import { installVesperLink, upgradeVesperLink } from './installer.js';
|
|
8
9
|
import { runMemoryMcp } from './memory-mcp.js';
|
|
9
10
|
import { appendSyncLog } from './operational-log.js';
|
|
10
11
|
import { writePendingMarker } from './pending.js';
|
|
11
12
|
import { resolveLinkPaths } from './paths.js';
|
|
12
13
|
import { runSyncOnce } from './sync.js';
|
|
13
14
|
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
|
+
const USAGE = 'Usage: vesper-link <configure|report-session|sync-once|scheduled-sync|mcp|install|upgrade|pause|resume|uninstall|reset-identity|status|doctor>';
|
|
15
16
|
function flags(args) {
|
|
16
17
|
const result = new Map();
|
|
17
18
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -51,7 +52,15 @@ async function main() {
|
|
|
51
52
|
if (!host) {
|
|
52
53
|
throw new Error('Usage: vesper-link configure --host <url>');
|
|
53
54
|
}
|
|
54
|
-
const
|
|
55
|
+
const existing = await fs.access(paths.configPath)
|
|
56
|
+
.then(() => readConfig(paths.configPath))
|
|
57
|
+
.catch((error) => {
|
|
58
|
+
if (error.code === 'ENOENT') {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
});
|
|
63
|
+
const config = await writeConfig(paths.configPath, host, process.env.VSCODE_AGENT_FOLDER ?? existing?.vscodeAgentFolder);
|
|
55
64
|
process.stdout.write(`${JSON.stringify(config)}\n`);
|
|
56
65
|
return;
|
|
57
66
|
}
|
|
@@ -116,6 +125,17 @@ async function main() {
|
|
|
116
125
|
})}\n`);
|
|
117
126
|
return;
|
|
118
127
|
}
|
|
128
|
+
if (command === 'upgrade') {
|
|
129
|
+
if (args.length > 0) {
|
|
130
|
+
throw new Error('Usage: vesper-link upgrade');
|
|
131
|
+
}
|
|
132
|
+
const result = await upgradeVesperLink({ paths });
|
|
133
|
+
process.stdout.write(`${JSON.stringify({
|
|
134
|
+
upgraded: true, runtimeVersion: result.state.runtime.version,
|
|
135
|
+
sourceTrust: result.state.sourceTrust, firstSyncStarted: result.firstSyncStarted,
|
|
136
|
+
})}\n`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
119
139
|
if (command === 'uninstall') {
|
|
120
140
|
process.stdout.write(`${JSON.stringify({ removed: await uninstallIntegrations(paths) })}\n`);
|
|
121
141
|
return;
|
|
@@ -16,6 +16,42 @@ function parseYamlScalars(text) {
|
|
|
16
16
|
}
|
|
17
17
|
return values;
|
|
18
18
|
}
|
|
19
|
+
async function readVscodeWorkspace(workspaceDir, context) {
|
|
20
|
+
try {
|
|
21
|
+
const workspace = JSON.parse(await fs.readFile(path.join(workspaceDir, 'workspace.json'), 'utf8'));
|
|
22
|
+
const cwdUri = typeof workspace.folder === 'string'
|
|
23
|
+
? workspace.folder
|
|
24
|
+
: typeof workspace.workspace === 'string' ? workspace.workspace : undefined;
|
|
25
|
+
if (!cwdUri) {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
let projectPath = cwdUri;
|
|
29
|
+
try {
|
|
30
|
+
projectPath = decodeURIComponent(new URL(cwdUri).pathname).replace(/^\/([A-Za-z]:\/)/, '$1');
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Keep the opaque location for project grouping when it is not a valid URL.
|
|
34
|
+
}
|
|
35
|
+
return { cwdUri, projectKey: pathToProjectKey(projectPath) };
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error.code !== 'ENOENT') {
|
|
39
|
+
context.reportIssue?.({ provider: 'github-copilot', code: 'workspace_metadata_unreadable', message: String(error) });
|
|
40
|
+
}
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function preferNewest(candidates) {
|
|
45
|
+
const byId = new Map();
|
|
46
|
+
for (const candidate of candidates) {
|
|
47
|
+
const current = byId.get(candidate.providerSessionId);
|
|
48
|
+
if (!current || candidate.mtimeMs > current.mtimeMs
|
|
49
|
+
|| (candidate.mtimeMs === current.mtimeMs && candidate.transcriptPath < current.transcriptPath)) {
|
|
50
|
+
byId.set(candidate.providerSessionId, candidate);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return [...byId.values()];
|
|
54
|
+
}
|
|
19
55
|
export class CopilotCollector {
|
|
20
56
|
async collect(context) {
|
|
21
57
|
const files = await walkFiles(context.copilotSessionsDir, (filePath) => path.basename(filePath) === 'events.jsonl');
|
|
@@ -59,6 +95,51 @@ export class CopilotCollector {
|
|
|
59
95
|
context.reportIssue?.({ provider: 'github-copilot', code: 'source_unreadable', message: String(error) });
|
|
60
96
|
}
|
|
61
97
|
}
|
|
62
|
-
|
|
98
|
+
for (const workspaceStorageDir of context.copilotVscodeWorkspaceStorageDirs) {
|
|
99
|
+
let workspaces;
|
|
100
|
+
try {
|
|
101
|
+
workspaces = await fs.readdir(workspaceStorageDir, { withFileTypes: true });
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (['ENOENT', 'EACCES', 'EPERM'].includes(String(error.code))) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
for (const workspace of workspaces) {
|
|
110
|
+
if (!workspace.isDirectory()) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const workspaceDir = path.join(workspaceStorageDir, workspace.name);
|
|
114
|
+
const transcriptDir = path.join(workspaceDir, 'GitHub.copilot-chat', 'transcripts');
|
|
115
|
+
const transcripts = await walkFiles(transcriptDir, (filePath) => path.extname(filePath).toLowerCase() === '.jsonl');
|
|
116
|
+
if (transcripts.length === 0) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const metadata = await readVscodeWorkspace(workspaceDir, context);
|
|
120
|
+
for (const transcriptPath of transcripts) {
|
|
121
|
+
try {
|
|
122
|
+
const stat = await fs.stat(transcriptPath);
|
|
123
|
+
candidates.push({
|
|
124
|
+
provider: 'github-copilot',
|
|
125
|
+
format: 'github-copilot-events-jsonl',
|
|
126
|
+
formatVersion: 'observed-v1',
|
|
127
|
+
providerSessionId: 'vscode:' + path.basename(transcriptPath, path.extname(transcriptPath)),
|
|
128
|
+
transcriptPath,
|
|
129
|
+
...metadata,
|
|
130
|
+
sourceCreatedAt: stat.birthtime.toISOString(),
|
|
131
|
+
sourceUpdatedAt: stat.mtime.toISOString(),
|
|
132
|
+
size: stat.size,
|
|
133
|
+
mtimeMs: stat.mtimeMs,
|
|
134
|
+
discovery: 'scan',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
context.reportIssue?.({ provider: 'github-copilot', code: 'source_unreadable', message: String(error) });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return preferNewest(candidates);
|
|
63
144
|
}
|
|
64
145
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export interface LinkConfig {
|
|
2
2
|
schemaVersion: 1;
|
|
3
3
|
host: string;
|
|
4
|
+
vscodeAgentFolder?: string;
|
|
4
5
|
}
|
|
6
|
+
export declare function normalizeVscodeAgentFolder(value: string | undefined): string | undefined;
|
|
5
7
|
export declare function normalizeHost(value: string): string;
|
|
6
8
|
export declare function readConfig(configPath: string): Promise<LinkConfig>;
|
|
7
|
-
export declare function writeConfig(configPath: string, host: string): Promise<LinkConfig>;
|
|
9
|
+
export declare function writeConfig(configPath: string, host: string, vscodeAgentFolder?: string): Promise<LinkConfig>;
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { isIPv4 } from 'node:net';
|
|
2
|
+
import * as path from 'node:path';
|
|
2
3
|
import { readJsonFile, writeJsonAtomic } from './atomic-json.js';
|
|
4
|
+
export function normalizeVscodeAgentFolder(value) {
|
|
5
|
+
if (value === undefined) {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
const normalized = path.normalize(value).replace(/[\\/]+$/, '');
|
|
9
|
+
if (!normalized || !path.isAbsolute(normalized) || normalized === path.parse(normalized).root) {
|
|
10
|
+
throw new Error('VSCODE_AGENT_FOLDER must be an absolute non-root path');
|
|
11
|
+
}
|
|
12
|
+
return normalized;
|
|
13
|
+
}
|
|
3
14
|
function isTailscaleIpv4(hostname) {
|
|
4
15
|
if (!isIPv4(hostname)) {
|
|
5
16
|
return false;
|
|
@@ -32,10 +43,20 @@ export async function readConfig(configPath) {
|
|
|
32
43
|
if (!config || config.schemaVersion !== 1 || typeof config.host !== 'string') {
|
|
33
44
|
throw new Error(`Missing or invalid config at ${configPath}; run vesper-link configure --host <url>`);
|
|
34
45
|
}
|
|
35
|
-
|
|
46
|
+
if (config.vscodeAgentFolder !== undefined && typeof config.vscodeAgentFolder !== 'string') {
|
|
47
|
+
throw new Error(`Invalid VSCODE_AGENT_FOLDER in config at ${configPath}`);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
schemaVersion: 1,
|
|
51
|
+
host: normalizeHost(config.host),
|
|
52
|
+
vscodeAgentFolder: normalizeVscodeAgentFolder(config.vscodeAgentFolder),
|
|
53
|
+
};
|
|
36
54
|
}
|
|
37
|
-
export async function writeConfig(configPath, host) {
|
|
38
|
-
const config = {
|
|
55
|
+
export async function writeConfig(configPath, host, vscodeAgentFolder) {
|
|
56
|
+
const config = {
|
|
57
|
+
schemaVersion: 1, host: normalizeHost(host),
|
|
58
|
+
vscodeAgentFolder: normalizeVscodeAgentFolder(vscodeAgentFolder),
|
|
59
|
+
};
|
|
39
60
|
await writeJsonAtomic(configPath, config);
|
|
40
61
|
return config;
|
|
41
62
|
}
|
package/dist/installation.d.ts
CHANGED
|
@@ -51,6 +51,7 @@ export declare function installManagedRuntime(options: {
|
|
|
51
51
|
now?: Date;
|
|
52
52
|
postInstallDoctor?: () => Promise<void>;
|
|
53
53
|
faults?: InstallFaults;
|
|
54
|
+
vscodeAgentFolder?: string;
|
|
54
55
|
}): Promise<InstalledState>;
|
|
55
56
|
export declare function uninstallIntegrations(paths: LinkPaths, runner?: ProcessRunner, probeProcess?: (pid: number) => 'alive' | 'absent' | 'unknown'): Promise<boolean>;
|
|
56
57
|
export declare function pauseIntegrations(paths: LinkPaths, runner?: ProcessRunner): Promise<boolean>;
|
package/dist/installation.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from 'node:fs/promises';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { readJsonFile, writeJsonAtomic } from './atomic-json.js';
|
|
5
|
+
import { normalizeVscodeAgentFolder } from './config.js';
|
|
5
6
|
import { stableLauncherSource } from './managed-runtime.js';
|
|
6
7
|
import { acquireSyncLock } from './lock.js';
|
|
7
8
|
import { NodeProcessRunner } from './process-runner.js';
|
|
@@ -150,6 +151,14 @@ async function prepareInstall(options) {
|
|
|
150
151
|
JSON.parse(snapshot.content.toString('utf8'));
|
|
151
152
|
}
|
|
152
153
|
}
|
|
154
|
+
let previousAgentFolder;
|
|
155
|
+
if (config.existed) {
|
|
156
|
+
const previousConfig = JSON.parse(config.content.toString('utf8'));
|
|
157
|
+
if (typeof previousConfig.vscodeAgentFolder === 'string') {
|
|
158
|
+
previousAgentFolder = previousConfig.vscodeAgentFolder;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const vscodeAgentFolder = normalizeVscodeAgentFolder(options.vscodeAgentFolder ?? previousAgentFolder);
|
|
153
162
|
const installationId = previous?.installationId ?? randomUUID();
|
|
154
163
|
const minuteOffset = previous?.minuteOffset ?? stableMinuteOffset(options.paths.homeDir);
|
|
155
164
|
const scheduler = await prepareScheduler({
|
|
@@ -174,6 +183,7 @@ async function prepareInstall(options) {
|
|
|
174
183
|
...(previous?.createdProviderFiles ?? []),
|
|
175
184
|
...providerFiles.filter((snapshot) => !snapshot.existed).map((snapshot) => snapshot.filePath),
|
|
176
185
|
])],
|
|
186
|
+
vscodeAgentFolder,
|
|
177
187
|
};
|
|
178
188
|
}
|
|
179
189
|
export async function installManagedRuntime(options) {
|
|
@@ -185,6 +195,7 @@ export async function installManagedRuntime(options) {
|
|
|
185
195
|
paths: options.paths, runtime: options.runtime, host: options.host,
|
|
186
196
|
sourceTrust: options.capabilities.sourceTrust, providers: options.providers,
|
|
187
197
|
nodePath, platform, runner, now,
|
|
198
|
+
vscodeAgentFolder: options.vscodeAgentFolder,
|
|
188
199
|
});
|
|
189
200
|
const installationId = prepared.installationId;
|
|
190
201
|
const pointer = {
|
|
@@ -196,7 +207,9 @@ export async function installManagedRuntime(options) {
|
|
|
196
207
|
await ensureLauncher(options.paths);
|
|
197
208
|
await writeJsonAtomic(options.paths.currentPath, pointer);
|
|
198
209
|
await options.faults?.afterStep?.('current');
|
|
199
|
-
await writeJsonAtomic(options.paths.configPath, {
|
|
210
|
+
await writeJsonAtomic(options.paths.configPath, {
|
|
211
|
+
schemaVersion: 1, host: options.host, vscodeAgentFolder: prepared.vscodeAgentFolder,
|
|
212
|
+
});
|
|
200
213
|
await options.faults?.afterStep?.('config');
|
|
201
214
|
providersApplied = true;
|
|
202
215
|
const integrations = await applyProviderIntegrations({
|
package/dist/installer.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export interface InstallerDependencies {
|
|
|
38
38
|
onNeedsLogin?: (url: string) => void;
|
|
39
39
|
requiredCompanion?: ManagedRuntime['companion'];
|
|
40
40
|
bootstrapAuthKey?: string;
|
|
41
|
+
failIfLoginRequired?: boolean;
|
|
41
42
|
}): Promise<RemoteTransport>;
|
|
42
43
|
getHealth: typeof getHealth;
|
|
43
44
|
getCapabilities: typeof getCapabilities;
|
|
@@ -56,4 +57,10 @@ export declare function installVesperLink(options: {
|
|
|
56
57
|
pollIntervalMs?: number;
|
|
57
58
|
signal?: AbortSignal;
|
|
58
59
|
dependencies?: InstallerDependencies;
|
|
60
|
+
requireExistingIdentity?: boolean;
|
|
61
|
+
}): Promise<InstallResult>;
|
|
62
|
+
export declare function upgradeVesperLink(options: {
|
|
63
|
+
paths: LinkPaths;
|
|
64
|
+
runner?: ProcessRunner;
|
|
65
|
+
dependencies?: InstallerDependencies;
|
|
59
66
|
}): Promise<InstallResult>;
|
package/dist/installer.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
-
import { normalizeHost } from './config.js';
|
|
3
|
+
import { normalizeHost, readConfig } from './config.js';
|
|
4
4
|
import { getCapabilities, getHealth, SystemicUploadError } from './http-client.js';
|
|
5
|
-
import { installManagedRuntime } from './installation.js';
|
|
5
|
+
import { installManagedRuntime, readInstalledState } from './installation.js';
|
|
6
6
|
import { acquireSyncLock } from './lock.js';
|
|
7
7
|
import { prepareManagedRuntime, validateInstallPrerequisites, validateManagedRuntime, } from './managed-runtime.js';
|
|
8
8
|
import { NodeProcessRunner } from './process-runner.js';
|
|
@@ -58,6 +58,9 @@ export async function installVesperLink(options) {
|
|
|
58
58
|
let bootstrapAuthKey = consumeBootstrapAuthKey(options.bootstrapAuthKey);
|
|
59
59
|
options.bootstrapAuthKey = undefined;
|
|
60
60
|
const automaticEnrollment = bootstrapAuthKey !== undefined;
|
|
61
|
+
if (options.requireExistingIdentity && automaticEnrollment) {
|
|
62
|
+
throw new Error('upgrade does not accept a Tailscale auth key');
|
|
63
|
+
}
|
|
61
64
|
const host = normalizeHost(options.host);
|
|
62
65
|
const companion = await dependencies.validatePrerequisites({ hostUrl: host, runner });
|
|
63
66
|
const releaseCandidateLock = await acquireSyncLock(options.paths.lockPath);
|
|
@@ -72,13 +75,16 @@ export async function installVesperLink(options) {
|
|
|
72
75
|
const progress = {
|
|
73
76
|
schemaVersion: 1, state: 'enrolling', host, expectedRole: options.expectedRole, updatedAt: new Date().toISOString(),
|
|
74
77
|
};
|
|
75
|
-
|
|
78
|
+
if (!options.requireExistingIdentity) {
|
|
79
|
+
await writeProgress(options.paths, progress);
|
|
80
|
+
}
|
|
76
81
|
let transport;
|
|
77
82
|
try {
|
|
78
83
|
transport = await dependencies.acquireTransport({
|
|
79
84
|
paths: options.paths, hostUrl: host,
|
|
80
85
|
requiredCompanion: runtime.companion,
|
|
81
86
|
bootstrapAuthKey,
|
|
87
|
+
failIfLoginRequired: options.requireExistingIdentity,
|
|
82
88
|
onNeedsLogin: (url) => {
|
|
83
89
|
options.onNeedsLogin?.(url);
|
|
84
90
|
},
|
|
@@ -102,6 +108,9 @@ export async function installVesperLink(options) {
|
|
|
102
108
|
if (!(error instanceof SystemicUploadError) || error.httpStatus !== 403) {
|
|
103
109
|
throw error;
|
|
104
110
|
}
|
|
111
|
+
if (options.requireExistingIdentity) {
|
|
112
|
+
throw new Error('The existing Tailscale identity is no longer authorized; upgrade stopped without relinking');
|
|
113
|
+
}
|
|
105
114
|
if (automaticEnrollment) {
|
|
106
115
|
throw new AutomaticEnrollmentConflictError(progress);
|
|
107
116
|
}
|
|
@@ -144,6 +153,7 @@ export async function installVesperLink(options) {
|
|
|
144
153
|
await dependencies.validateRuntime(runtime);
|
|
145
154
|
const candidateTransport = await dependencies.acquireTransport({
|
|
146
155
|
paths: options.paths, hostUrl: host, requiredCompanion: runtime.companion,
|
|
156
|
+
failIfLoginRequired: options.requireExistingIdentity,
|
|
147
157
|
});
|
|
148
158
|
try {
|
|
149
159
|
const [candidateHealth, candidateCapabilities] = await Promise.all([
|
|
@@ -160,10 +170,12 @@ export async function installVesperLink(options) {
|
|
|
160
170
|
}
|
|
161
171
|
installed = await dependencies.installManaged({
|
|
162
172
|
paths: options.paths, runtime, host, capabilities, runner,
|
|
173
|
+
vscodeAgentFolder: process.env.VSCODE_AGENT_FOLDER,
|
|
163
174
|
postInstallDoctor: async () => {
|
|
164
175
|
await dependencies.validateRuntime(runtime);
|
|
165
176
|
const postTransport = await dependencies.acquireTransport({
|
|
166
177
|
paths: options.paths, hostUrl: host, requiredCompanion: runtime.companion,
|
|
178
|
+
failIfLoginRequired: options.requireExistingIdentity,
|
|
167
179
|
});
|
|
168
180
|
try {
|
|
169
181
|
const [health, currentCapabilities] = await Promise.all([
|
|
@@ -190,6 +202,37 @@ export async function installVesperLink(options) {
|
|
|
190
202
|
await dependencies.triggerFirstSync(options.paths);
|
|
191
203
|
return { state: installed, firstSyncStarted: true };
|
|
192
204
|
}
|
|
205
|
+
export async function upgradeVesperLink(options) {
|
|
206
|
+
const inheritedAuthKey = consumeBootstrapAuthKey(undefined);
|
|
207
|
+
if (inheritedAuthKey !== undefined) {
|
|
208
|
+
throw new Error('upgrade does not accept a Tailscale auth key');
|
|
209
|
+
}
|
|
210
|
+
const installed = await readInstalledState(options.paths);
|
|
211
|
+
if (!installed) {
|
|
212
|
+
throw new Error('No existing Vesper Link installation; use a Host-generated install command first');
|
|
213
|
+
}
|
|
214
|
+
if (installed.paused) {
|
|
215
|
+
throw new Error('Vesper Link is paused; resume it before upgrading');
|
|
216
|
+
}
|
|
217
|
+
const config = await readConfig(options.paths.configPath);
|
|
218
|
+
if (installed.host !== config.host) {
|
|
219
|
+
throw new Error('Installed Host does not match Vesper Link config; refusing to guess during upgrade');
|
|
220
|
+
}
|
|
221
|
+
const identityEntries = await fs.readdir(options.paths.tsnetDir).catch((error) => {
|
|
222
|
+
if (error.code === 'ENOENT') {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
throw error;
|
|
226
|
+
});
|
|
227
|
+
if (identityEntries.length === 0) {
|
|
228
|
+
throw new Error('No existing Tailscale identity; upgrade stopped without relinking');
|
|
229
|
+
}
|
|
230
|
+
return installVesperLink({
|
|
231
|
+
paths: options.paths, host: config.host, expectedRole: installed.sourceTrust,
|
|
232
|
+
runner: options.runner, dependencies: options.dependencies, requireExistingIdentity: true,
|
|
233
|
+
pollAuthorization: false,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
193
236
|
async function waitForPoll(milliseconds, signal) {
|
|
194
237
|
if (signal?.aborted) {
|
|
195
238
|
throw signal.reason ?? new Error('Installation cancelled');
|
package/dist/managed-runtime.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createRequire } from 'node:module';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { assertCompanionProtocol, resolveCompanion } from './companion.js';
|
|
6
6
|
import { NodeProcessRunner } from './process-runner.js';
|
|
7
|
+
import { validateLinuxSchedulerPrerequisite } from './scheduler.js';
|
|
7
8
|
async function metadata(filePath) {
|
|
8
9
|
return JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
9
10
|
}
|
|
@@ -34,21 +35,21 @@ async function copyDirectory(source, destination) {
|
|
|
34
35
|
await fs.cp(source, destination, { recursive: true, force: false, errorOnExist: true });
|
|
35
36
|
}
|
|
36
37
|
export function stableLauncherSource() {
|
|
37
|
-
return `#!/usr/bin/env node
|
|
38
|
-
import fs from 'node:fs/promises';
|
|
39
|
-
import path from 'node:path';
|
|
40
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
41
|
-
const stateDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
42
|
-
const current = JSON.parse(await fs.readFile(path.join(stateDir, 'current.json'), 'utf8'));
|
|
43
|
-
if (current.schemaVersion !== 1 || typeof current.runtimePath !== 'string') {
|
|
44
|
-
throw new Error('vesper-link current runtime pointer is malformed');
|
|
45
|
-
}
|
|
46
|
-
const runtimeRoot = path.join(stateDir, 'runtime');
|
|
47
|
-
const relative = path.relative(runtimeRoot, current.runtimePath);
|
|
48
|
-
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
49
|
-
throw new Error('vesper-link current runtime escaped the managed runtime root');
|
|
50
|
-
}
|
|
51
|
-
await import(pathToFileURL(path.join(current.runtimePath, 'package', 'dist', 'cli.js')).href);
|
|
38
|
+
return `#!/usr/bin/env node
|
|
39
|
+
import fs from 'node:fs/promises';
|
|
40
|
+
import path from 'node:path';
|
|
41
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
42
|
+
const stateDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
43
|
+
const current = JSON.parse(await fs.readFile(path.join(stateDir, 'current.json'), 'utf8'));
|
|
44
|
+
if (current.schemaVersion !== 1 || typeof current.runtimePath !== 'string') {
|
|
45
|
+
throw new Error('vesper-link current runtime pointer is malformed');
|
|
46
|
+
}
|
|
47
|
+
const runtimeRoot = path.join(stateDir, 'runtime');
|
|
48
|
+
const relative = path.relative(runtimeRoot, current.runtimePath);
|
|
49
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
50
|
+
throw new Error('vesper-link current runtime escaped the managed runtime root');
|
|
51
|
+
}
|
|
52
|
+
await import(pathToFileURL(path.join(current.runtimePath, 'package', 'dist', 'cli.js')).href);
|
|
52
53
|
`;
|
|
53
54
|
}
|
|
54
55
|
export async function validateInstallPrerequisites(options) {
|
|
@@ -70,6 +71,9 @@ export async function validateInstallPrerequisites(options) {
|
|
|
70
71
|
if (npm.code !== 0 || majorVersion(npm.stdout.trim()) < 9) {
|
|
71
72
|
throw new Error(`A working npm 9 or newer is required: ${npm.stderr.trim() || npm.stdout.trim()}`);
|
|
72
73
|
}
|
|
74
|
+
if (platform === 'linux') {
|
|
75
|
+
await validateLinuxSchedulerPrerequisite(runner);
|
|
76
|
+
}
|
|
73
77
|
const companion = await (options.resolveSourceCompanion ?? (() => resolveCompanion(platform, architecture)))();
|
|
74
78
|
await (options.probeCompanion ?? assertCompanionProtocol)(companion, protocolProbeEnvironment(process.env));
|
|
75
79
|
return companion;
|
package/dist/paths.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface LinkPaths {
|
|
|
18
18
|
logsDir: string;
|
|
19
19
|
claudeProjectsDir: string;
|
|
20
20
|
copilotSessionsDir: string;
|
|
21
|
+
copilotVscodeWorkspaceStorageDirs: string[];
|
|
21
22
|
codexHome: string;
|
|
22
23
|
}
|
|
23
|
-
export declare function
|
|
24
|
+
export declare function vscodeAgentWorkspaceStorageDir(agentFolder: string): string;
|
|
25
|
+
export declare function resolveLinkPaths(environment?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): LinkPaths;
|
package/dist/paths.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as os from 'node:os';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
export function
|
|
3
|
+
export function vscodeAgentWorkspaceStorageDir(agentFolder) {
|
|
4
|
+
return path.join(agentFolder, 'data', 'User', 'workspaceStorage');
|
|
5
|
+
}
|
|
6
|
+
export function resolveLinkPaths(environment = process.env, platform = process.platform) {
|
|
4
7
|
const homeDir = environment.HOME || environment.USERPROFILE || os.homedir();
|
|
5
8
|
const rawStateDir = path.normalize(environment.VESPER_LINK_HOME || path.join(homeDir, '.vesper-link'));
|
|
6
9
|
const root = path.parse(rawStateDir).root;
|
|
@@ -9,6 +12,23 @@ export function resolveLinkPaths(environment = process.env) {
|
|
|
9
12
|
}
|
|
10
13
|
const stateDir = rawStateDir.replace(/[\\/]+$/, '');
|
|
11
14
|
const codexHome = environment.CODEX_HOME || path.join(homeDir, '.codex');
|
|
15
|
+
const appData = environment.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
|
|
16
|
+
const localWorkspaceStorageDirs = platform === 'win32'
|
|
17
|
+
? [
|
|
18
|
+
path.join(appData, 'Code', 'User', 'workspaceStorage'),
|
|
19
|
+
path.join(appData, 'Code - Insiders', 'User', 'workspaceStorage'),
|
|
20
|
+
]
|
|
21
|
+
: [
|
|
22
|
+
path.join(homeDir, '.config', 'Code', 'User', 'workspaceStorage'),
|
|
23
|
+
path.join(homeDir, '.config', 'Code - Insiders', 'User', 'workspaceStorage'),
|
|
24
|
+
];
|
|
25
|
+
const agentRoots = environment.VSCODE_AGENT_FOLDER
|
|
26
|
+
? [environment.VSCODE_AGENT_FOLDER]
|
|
27
|
+
: [path.join(homeDir, '.vscode-server'), path.join(homeDir, '.vscode-server-insiders')];
|
|
28
|
+
const copilotVscodeWorkspaceStorageDirs = [
|
|
29
|
+
...localWorkspaceStorageDirs,
|
|
30
|
+
...agentRoots.map(vscodeAgentWorkspaceStorageDir),
|
|
31
|
+
];
|
|
12
32
|
return {
|
|
13
33
|
homeDir,
|
|
14
34
|
stateDir,
|
|
@@ -31,6 +51,7 @@ export function resolveLinkPaths(environment = process.env) {
|
|
|
31
51
|
logsDir: path.join(stateDir, 'logs'),
|
|
32
52
|
claudeProjectsDir: path.join(homeDir, '.claude', 'projects'),
|
|
33
53
|
copilotSessionsDir: path.join(homeDir, '.copilot', 'session-state'),
|
|
54
|
+
copilotVscodeWorkspaceStorageDirs,
|
|
34
55
|
codexHome,
|
|
35
56
|
};
|
|
36
57
|
}
|
package/dist/scheduler.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export type SchedulerSnapshot = {
|
|
|
22
22
|
content: string;
|
|
23
23
|
};
|
|
24
24
|
export declare function stableMinuteOffset(homeDir: string, machineName?: string): number;
|
|
25
|
+
export declare function validateLinuxSchedulerPrerequisite(runner: ProcessRunner): Promise<void>;
|
|
25
26
|
export declare function prepareScheduler(options: {
|
|
26
27
|
platform?: NodeJS.Platform;
|
|
27
28
|
runner?: ProcessRunner;
|
package/dist/scheduler.js
CHANGED
|
@@ -14,12 +14,31 @@ function shellQuote(value) {
|
|
|
14
14
|
function ownerToken(installationId) {
|
|
15
15
|
return '--owner ' + installationId;
|
|
16
16
|
}
|
|
17
|
+
async function runCron(runner, args, input) {
|
|
18
|
+
const environment = { ...process.env, LC_ALL: 'C', LANG: 'C' };
|
|
19
|
+
try {
|
|
20
|
+
return await runner.run('crontab', args, input, environment);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
const code = error.code;
|
|
24
|
+
if (code === 'ENOENT') {
|
|
25
|
+
throw new Error('Linux requires the crontab command for periodic session sync.\n'
|
|
26
|
+
+ 'Ubuntu/Debian: sudo apt-get install cron\n'
|
|
27
|
+
+ 'Fedora/RHEL: sudo dnf install cronie\n'
|
|
28
|
+
+ 'Ensure the cron service is running, then rerun this command.');
|
|
29
|
+
}
|
|
30
|
+
if (code === 'EACCES') {
|
|
31
|
+
throw new Error('Cannot execute crontab. Check that the crontab executable has execute permission and is not on a filesystem '
|
|
32
|
+
+ 'mounted with noexec, then rerun this command.');
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
17
37
|
export function stableMinuteOffset(homeDir, machineName = os.hostname()) {
|
|
18
38
|
return Number.parseInt(createHash('sha256').update(machineName).update('\0').update(homeDir).digest('hex').slice(0, 8), 16) % 60;
|
|
19
39
|
}
|
|
20
40
|
async function readCron(runner) {
|
|
21
|
-
const
|
|
22
|
-
const result = await runner.run('crontab', ['-l'], undefined, environment);
|
|
41
|
+
const result = await runCron(runner, ['-l']);
|
|
23
42
|
if (result.code === 0) {
|
|
24
43
|
return result.stdout;
|
|
25
44
|
}
|
|
@@ -33,12 +52,14 @@ function withoutOwnedCron(content) {
|
|
|
33
52
|
return content.split(/\r?\n/).filter((line) => !line.trimEnd().endsWith(CRON_MARKER)).join('\n');
|
|
34
53
|
}
|
|
35
54
|
async function writeCron(runner, content) {
|
|
36
|
-
const
|
|
37
|
-
const result = await runner.run('crontab', ['-'], content, environment);
|
|
55
|
+
const result = await runCron(runner, ['-'], content);
|
|
38
56
|
if (result.code !== 0) {
|
|
39
57
|
throw new Error('Cannot update user crontab: ' + result.stderr.trim());
|
|
40
58
|
}
|
|
41
59
|
}
|
|
60
|
+
export async function validateLinuxSchedulerPrerequisite(runner) {
|
|
61
|
+
await readCron(runner);
|
|
62
|
+
}
|
|
42
63
|
async function queryWindowsTask(runner, name) {
|
|
43
64
|
return runner.run('schtasks.exe', ['/Query', '/TN', name, '/XML']);
|
|
44
65
|
}
|
package/dist/sync.js
CHANGED
|
@@ -6,7 +6,7 @@ import { getHealth, postManifest, putContent, SystemicUploadError, TaskUploadErr
|
|
|
6
6
|
import { readInstalledState } from './installation.js';
|
|
7
7
|
import { acquireSyncLock, SyncAlreadyRunningError } from './lock.js';
|
|
8
8
|
import { readPendingMarkers, removePendingMarker } from './pending.js';
|
|
9
|
-
import { resolveLinkPaths } from './paths.js';
|
|
9
|
+
import { resolveLinkPaths, vscodeAgentWorkspaceStorageDir } from './paths.js';
|
|
10
10
|
import { isStableForUpload, selectQueueItems } from './queue.js';
|
|
11
11
|
import { fingerprintStableSource, LocalTaskError, assertSourceUnchanged } from './source.js';
|
|
12
12
|
import { clearSyncState, readSessionStates, readSyncState, writeSessionState, writeSyncState } from './state.js';
|
|
@@ -101,6 +101,10 @@ export async function runSyncOnce(options = {}) {
|
|
|
101
101
|
nowMs,
|
|
102
102
|
claudeProjectsDir: paths.claudeProjectsDir,
|
|
103
103
|
copilotSessionsDir: paths.copilotSessionsDir,
|
|
104
|
+
copilotVscodeWorkspaceStorageDirs: [...new Set([
|
|
105
|
+
...paths.copilotVscodeWorkspaceStorageDirs,
|
|
106
|
+
...(config.vscodeAgentFolder ? [vscodeAgentWorkspaceStorageDir(config.vscodeAgentFolder)] : []),
|
|
107
|
+
])],
|
|
104
108
|
codexHome: paths.codexHome,
|
|
105
109
|
reportIssue: (issue) => summary.collectorIssues.push(issue),
|
|
106
110
|
};
|
package/dist/transport.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface AcquireTransportOptions {
|
|
|
20
20
|
probeCompanion?: (companion: CompanionInfo, environment: NodeJS.ProcessEnv) => Promise<void>;
|
|
21
21
|
spawnChild?: (executable: string, args: string[], env: NodeJS.ProcessEnv) => ChildProcessWithoutNullStreams;
|
|
22
22
|
onNeedsLogin?: (url: string) => void;
|
|
23
|
+
failIfLoginRequired?: boolean;
|
|
23
24
|
bootstrapAuthKey?: string;
|
|
24
25
|
startTimeoutMs?: number;
|
|
25
26
|
}
|
package/dist/transport.js
CHANGED
|
@@ -344,6 +344,10 @@ export async function acquireRemoteTransport(options) {
|
|
|
344
344
|
fatalMessage = 'automatic enrollment key was not accepted; copy a fresh Memory Link command';
|
|
345
345
|
child.kill('SIGTERM');
|
|
346
346
|
}
|
|
347
|
+
else if (options.failIfLoginRequired) {
|
|
348
|
+
fatalMessage = 'the existing Tailscale identity is unavailable; upgrade stopped without relinking';
|
|
349
|
+
child.kill('SIGTERM');
|
|
350
|
+
}
|
|
347
351
|
else {
|
|
348
352
|
options.onNeedsLogin?.(event.loginUrl);
|
|
349
353
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VESPER_LINK_VERSION = "0.1.
|
|
1
|
+
export declare const VESPER_LINK_VERSION = "0.1.5";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VESPER_LINK_VERSION = '0.1.
|
|
1
|
+
export const VESPER_LINK_VERSION = '0.1.5';
|
package/package.json
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@msvesper/vesper-link",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Connect agent sessions and tools to Vesper Memory",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"vesper-link": "dist/cli.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"dist/**/*.js",
|
|
11
|
-
"dist/**/*.d.ts",
|
|
12
|
-
"README.md",
|
|
13
|
-
"LICENSE.md"
|
|
14
|
-
],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
17
|
-
"build": "npm run clean && tsc -p tsconfig.json",
|
|
18
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
19
|
-
"test": "npm run build && vitest run",
|
|
20
|
-
"pack-check": "npm run build && npm pack --dry-run",
|
|
21
|
-
"prepack": "npm run build"
|
|
22
|
-
},
|
|
23
|
-
"engines": {
|
|
24
|
-
"node": ">=20"
|
|
25
|
-
},
|
|
26
|
-
"license": "SEE LICENSE IN LICENSE.md",
|
|
27
|
-
"publishConfig": {
|
|
28
|
-
"access": "public"
|
|
29
|
-
},
|
|
30
|
-
"optionalDependencies": {
|
|
31
|
-
"@msvesper/vesper-net-linux-x64": "0.1.
|
|
32
|
-
"@msvesper/vesper-net-win32-x64": "0.1.
|
|
33
|
-
},
|
|
34
|
-
"devDependencies": {
|
|
35
|
-
"@types/node": "^22.15.0",
|
|
36
|
-
"typescript": "^5.8.3",
|
|
37
|
-
"vitest": "^3.2.4"
|
|
38
|
-
}
|
|
39
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@msvesper/vesper-link",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Connect agent sessions and tools to Vesper Memory",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vesper-link": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/**/*.js",
|
|
11
|
+
"dist/**/*.d.ts",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
17
|
+
"build": "npm run clean && tsc -p tsconfig.json",
|
|
18
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
19
|
+
"test": "npm run build && vitest run",
|
|
20
|
+
"pack-check": "npm run build && npm pack --dry-run",
|
|
21
|
+
"prepack": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"optionalDependencies": {
|
|
31
|
+
"@msvesper/vesper-net-linux-x64": "0.1.5",
|
|
32
|
+
"@msvesper/vesper-net-win32-x64": "0.1.5"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.15.0",
|
|
36
|
+
"typescript": "^5.8.3",
|
|
37
|
+
"vitest": "^3.2.4"
|
|
38
|
+
}
|
|
39
|
+
}
|