@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.
- package/LICENSE.md +44 -0
- package/README.md +26 -0
- package/dist/atomic-json.d.ts +3 -0
- package/dist/atomic-json.js +47 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +170 -0
- package/dist/collectors/claude.d.ts +5 -0
- package/dist/collectors/claude.js +42 -0
- package/dist/collectors/codex.d.ts +5 -0
- package/dist/collectors/codex.js +65 -0
- package/dist/collectors/copilot.d.ts +5 -0
- package/dist/collectors/copilot.js +64 -0
- package/dist/collectors/index.d.ts +4 -0
- package/dist/collectors/index.js +3 -0
- package/dist/collectors/types.d.ts +16 -0
- package/dist/collectors/types.js +1 -0
- package/dist/companion.d.ts +12 -0
- package/dist/companion.js +62 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +41 -0
- package/dist/contracts.d.ts +83 -0
- package/dist/contracts.js +144 -0
- package/dist/diagnostics.d.ts +37 -0
- package/dist/diagnostics.js +64 -0
- package/dist/fs-utils.d.ts +5 -0
- package/dist/fs-utils.js +69 -0
- package/dist/hook-report.d.ts +8 -0
- package/dist/hook-report.js +21 -0
- package/dist/http-client.d.ts +25 -0
- package/dist/http-client.js +228 -0
- package/dist/install-args.d.ts +7 -0
- package/dist/install-args.js +35 -0
- package/dist/installation.d.ts +59 -0
- package/dist/installation.js +524 -0
- package/dist/installer.d.ts +59 -0
- package/dist/installer.js +214 -0
- package/dist/lock.d.ts +5 -0
- package/dist/lock.js +57 -0
- package/dist/managed-runtime.d.ts +33 -0
- package/dist/managed-runtime.js +130 -0
- package/dist/memory-mcp.d.ts +27 -0
- package/dist/memory-mcp.js +155 -0
- package/dist/operational-log.d.ts +17 -0
- package/dist/operational-log.js +48 -0
- package/dist/paths.d.ts +23 -0
- package/dist/paths.js +36 -0
- package/dist/pending.d.ts +7 -0
- package/dist/pending.js +35 -0
- package/dist/process-runner.d.ts +11 -0
- package/dist/process-runner.js +21 -0
- package/dist/provider-integrations.d.ts +42 -0
- package/dist/provider-integrations.js +339 -0
- package/dist/queue.d.ts +19 -0
- package/dist/queue.js +129 -0
- package/dist/scheduler.d.ts +51 -0
- package/dist/scheduler.js +176 -0
- package/dist/session.d.ts +50 -0
- package/dist/session.js +12 -0
- package/dist/source.d.ts +11 -0
- package/dist/source.js +45 -0
- package/dist/state.d.ts +11 -0
- package/dist/state.js +37 -0
- package/dist/sync.d.ts +18 -0
- package/dist/sync.js +247 -0
- package/dist/transport.d.ts +27 -0
- package/dist/transport.js +400 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
|
4
|
+
const DEFAULT_FILE_COUNT = 5;
|
|
5
|
+
async function fileSize(filePath) {
|
|
6
|
+
try {
|
|
7
|
+
return (await fs.stat(filePath)).size;
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (error.code === 'ENOENT') {
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function rotate(logPath, fileCount) {
|
|
17
|
+
await fs.rm(`${logPath}.${fileCount - 1}`, { force: true });
|
|
18
|
+
for (let index = fileCount - 2; index >= 1; index -= 1) {
|
|
19
|
+
try {
|
|
20
|
+
await fs.rename(`${logPath}.${index}`, `${logPath}.${index + 1}`);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code !== 'ENOENT') {
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
await fs.rename(logPath, `${logPath}.1`);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error.code !== 'ENOENT') {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function appendSyncLog(logsDir, record, options = {}) {
|
|
38
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
39
|
+
const fileCount = options.fileCount ?? DEFAULT_FILE_COUNT;
|
|
40
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
41
|
+
const logPath = path.join(logsDir, 'sync.log');
|
|
42
|
+
await fs.mkdir(logsDir, { recursive: true });
|
|
43
|
+
const currentSize = await fileSize(logPath);
|
|
44
|
+
if (currentSize > 0 && currentSize + Buffer.byteLength(line) > maxBytes) {
|
|
45
|
+
await rotate(logPath, fileCount);
|
|
46
|
+
}
|
|
47
|
+
await fs.appendFile(logPath, line, 'utf8');
|
|
48
|
+
}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface LinkPaths {
|
|
2
|
+
homeDir: string;
|
|
3
|
+
stateDir: string;
|
|
4
|
+
configPath: string;
|
|
5
|
+
pendingDir: string;
|
|
6
|
+
sessionsDir: string;
|
|
7
|
+
syncStatePath: string;
|
|
8
|
+
lockPath: string;
|
|
9
|
+
tsnetDir: string;
|
|
10
|
+
transportDir: string;
|
|
11
|
+
transportRendezvousPath: string;
|
|
12
|
+
transportLockPath: string;
|
|
13
|
+
runtimeDir: string;
|
|
14
|
+
currentPath: string;
|
|
15
|
+
launcherPath: string;
|
|
16
|
+
installStatePath: string;
|
|
17
|
+
installProgressPath: string;
|
|
18
|
+
logsDir: string;
|
|
19
|
+
claudeProjectsDir: string;
|
|
20
|
+
copilotSessionsDir: string;
|
|
21
|
+
codexHome: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function resolveLinkPaths(environment?: NodeJS.ProcessEnv): LinkPaths;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as os from 'node:os';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export function resolveLinkPaths(environment = process.env) {
|
|
4
|
+
const homeDir = environment.HOME || environment.USERPROFILE || os.homedir();
|
|
5
|
+
const rawStateDir = path.normalize(environment.VESPER_LINK_HOME || path.join(homeDir, '.vesper-link'));
|
|
6
|
+
const root = path.parse(rawStateDir).root;
|
|
7
|
+
if (rawStateDir === root) {
|
|
8
|
+
throw new Error('VESPER_LINK_HOME must not be a filesystem root');
|
|
9
|
+
}
|
|
10
|
+
const stateDir = rawStateDir.replace(/[\\/]+$/, '');
|
|
11
|
+
const codexHome = environment.CODEX_HOME || path.join(homeDir, '.codex');
|
|
12
|
+
return {
|
|
13
|
+
homeDir,
|
|
14
|
+
stateDir,
|
|
15
|
+
configPath: path.join(stateDir, 'config.json'),
|
|
16
|
+
pendingDir: path.join(stateDir, 'pending'),
|
|
17
|
+
sessionsDir: path.join(stateDir, 'sessions'),
|
|
18
|
+
syncStatePath: path.join(stateDir, 'sync-state.json'),
|
|
19
|
+
// This lock must outlive stateDir so destructive uninstall cannot remove
|
|
20
|
+
// the lock while its critical section is still active.
|
|
21
|
+
lockPath: stateDir + '.lock',
|
|
22
|
+
tsnetDir: path.join(stateDir, 'tsnet'),
|
|
23
|
+
transportDir: path.join(stateDir, 'transport'),
|
|
24
|
+
transportRendezvousPath: path.join(stateDir, 'transport', 'transport.json'),
|
|
25
|
+
transportLockPath: path.join(stateDir, 'transport', 'owner.lock'),
|
|
26
|
+
runtimeDir: path.join(stateDir, 'runtime'),
|
|
27
|
+
currentPath: path.join(stateDir, 'current.json'),
|
|
28
|
+
launcherPath: path.join(stateDir, 'current', 'cli.mjs'),
|
|
29
|
+
installStatePath: path.join(stateDir, 'install-state.json'),
|
|
30
|
+
installProgressPath: path.join(stateDir, 'install-progress.json'),
|
|
31
|
+
logsDir: path.join(stateDir, 'logs'),
|
|
32
|
+
claudeProjectsDir: path.join(homeDir, '.claude', 'projects'),
|
|
33
|
+
copilotSessionsDir: path.join(homeDir, '.copilot', 'session-state'),
|
|
34
|
+
codexHome,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type PendingMarker } from './session.js';
|
|
2
|
+
export declare function writePendingMarker(pendingDir: string, marker: PendingMarker): Promise<string>;
|
|
3
|
+
export declare function readPendingMarkers(pendingDir: string): Promise<Array<{
|
|
4
|
+
path: string;
|
|
5
|
+
marker: PendingMarker;
|
|
6
|
+
}>>;
|
|
7
|
+
export declare function removePendingMarker(markerPath: string): Promise<void>;
|
package/dist/pending.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { isProvider } from './contracts.js';
|
|
4
|
+
import { listJsonFiles, readJsonFile, writeJsonAtomic } from './atomic-json.js';
|
|
5
|
+
import { stableSessionKey } from './session.js';
|
|
6
|
+
export async function writePendingMarker(pendingDir, marker) {
|
|
7
|
+
if (marker.schemaVersion !== 1 || !isProvider(marker.provider) || !marker.providerSessionId || !marker.transcriptPath) {
|
|
8
|
+
throw new Error('Invalid pending marker');
|
|
9
|
+
}
|
|
10
|
+
const markerPath = path.join(pendingDir, `${stableSessionKey(marker.provider, marker.providerSessionId)}.json`);
|
|
11
|
+
await writeJsonAtomic(markerPath, {
|
|
12
|
+
schemaVersion: 1,
|
|
13
|
+
provider: marker.provider,
|
|
14
|
+
providerSessionId: marker.providerSessionId,
|
|
15
|
+
transcriptPath: path.resolve(marker.transcriptPath),
|
|
16
|
+
queuedAt: marker.queuedAt,
|
|
17
|
+
});
|
|
18
|
+
return markerPath;
|
|
19
|
+
}
|
|
20
|
+
export async function readPendingMarkers(pendingDir) {
|
|
21
|
+
const files = await listJsonFiles(pendingDir);
|
|
22
|
+
const markers = [];
|
|
23
|
+
for (const filePath of files) {
|
|
24
|
+
const marker = await readJsonFile(filePath);
|
|
25
|
+
if (!marker || marker.schemaVersion !== 1 || !isProvider(marker.provider)
|
|
26
|
+
|| !marker.providerSessionId || !marker.transcriptPath || !marker.queuedAt) {
|
|
27
|
+
throw new Error(`Malformed pending marker: ${filePath}`);
|
|
28
|
+
}
|
|
29
|
+
markers.push({ path: filePath, marker });
|
|
30
|
+
}
|
|
31
|
+
return markers;
|
|
32
|
+
}
|
|
33
|
+
export async function removePendingMarker(markerPath) {
|
|
34
|
+
await fs.rm(markerPath, { force: true });
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ProcessResult {
|
|
2
|
+
code: number;
|
|
3
|
+
stdout: string;
|
|
4
|
+
stderr: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ProcessRunner {
|
|
7
|
+
run(command: string, args: string[], input?: string, environment?: NodeJS.ProcessEnv): Promise<ProcessResult>;
|
|
8
|
+
}
|
|
9
|
+
export declare class NodeProcessRunner implements ProcessRunner {
|
|
10
|
+
run(command: string, args: string[], input?: string, environment?: NodeJS.ProcessEnv): Promise<ProcessResult>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
export class NodeProcessRunner {
|
|
3
|
+
run(command, args, input, environment) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const child = spawn(command, args, {
|
|
6
|
+
stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, env: environment ?? process.env,
|
|
7
|
+
});
|
|
8
|
+
const stdout = [];
|
|
9
|
+
const stderr = [];
|
|
10
|
+
child.stdout.on('data', (chunk) => stdout.push(Buffer.from(chunk)));
|
|
11
|
+
child.stderr.on('data', (chunk) => stderr.push(Buffer.from(chunk)));
|
|
12
|
+
child.once('error', reject);
|
|
13
|
+
child.once('close', (code) => resolve({
|
|
14
|
+
code: code ?? 1,
|
|
15
|
+
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
16
|
+
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
17
|
+
}));
|
|
18
|
+
child.stdin.end(input);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { LinkPaths } from './paths.js';
|
|
2
|
+
export type SupportedProvider = 'claude-code' | 'github-copilot' | 'openai-codex';
|
|
3
|
+
interface JsonObject {
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface OwnedProviderIntegrations {
|
|
7
|
+
providers: SupportedProvider[];
|
|
8
|
+
claudeHook?: JsonObject;
|
|
9
|
+
copilotHook?: JsonObject;
|
|
10
|
+
claudeMcp?: JsonObject;
|
|
11
|
+
copilotMcp?: JsonObject;
|
|
12
|
+
claudeInstruction?: string;
|
|
13
|
+
copilotInstruction?: string;
|
|
14
|
+
codexMcpBlock?: string;
|
|
15
|
+
codexInstruction?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ProviderLocations {
|
|
18
|
+
claudeSettingsPath: string;
|
|
19
|
+
claudeUserConfigPath: string;
|
|
20
|
+
claudeInstructionPath: string;
|
|
21
|
+
copilotSettingsPath: string;
|
|
22
|
+
copilotMcpPath: string;
|
|
23
|
+
copilotInstructionPath: string;
|
|
24
|
+
codexConfigPath: string;
|
|
25
|
+
codexInstructionPath: string;
|
|
26
|
+
}
|
|
27
|
+
export type ProviderMutation = 'claude-hook' | 'claude-mcp' | 'claude-instruction' | 'copilot-hook' | 'copilot-mcp' | 'copilot-instruction' | 'codex-mcp' | 'codex-instruction';
|
|
28
|
+
export declare function providerLocations(paths: LinkPaths): ProviderLocations;
|
|
29
|
+
export declare function detectInstalledProviders(paths: LinkPaths): Promise<SupportedProvider[]>;
|
|
30
|
+
export declare function providerTargetPaths(paths: LinkPaths, providers: SupportedProvider[]): string[];
|
|
31
|
+
export declare function applyProviderIntegrations(options: {
|
|
32
|
+
paths: LinkPaths;
|
|
33
|
+
providers: SupportedProvider[];
|
|
34
|
+
trusted: boolean;
|
|
35
|
+
nodePath: string;
|
|
36
|
+
launcherPath: string;
|
|
37
|
+
previous?: OwnedProviderIntegrations;
|
|
38
|
+
installHooks?: boolean;
|
|
39
|
+
afterMutation?: (mutation: ProviderMutation) => Promise<void> | void;
|
|
40
|
+
}): Promise<OwnedProviderIntegrations>;
|
|
41
|
+
export declare function removeProviderIntegrations(paths: LinkPaths, owned: OwnedProviderIntegrations): Promise<void>;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { readJsonFile, writeJsonAtomic } from './atomic-json.js';
|
|
4
|
+
const CODEX_MCP_BEGIN = '# BEGIN vesper-link:vesper-memory MCP';
|
|
5
|
+
const CODEX_MCP_END = '# END vesper-link:vesper-memory MCP';
|
|
6
|
+
const INSTRUCTION_BEGIN = '<!-- BEGIN vesper-link:vesper-memory -->';
|
|
7
|
+
const INSTRUCTION_END = '<!-- END vesper-link:vesper-memory -->';
|
|
8
|
+
const MEMORY_INSTRUCTION = `Use the vesper-memory search_memory tool when prior preferences, decisions, facts, or context may matter. Use add_fact only when the user explicitly asks you to remember something.`;
|
|
9
|
+
export function providerLocations(paths) {
|
|
10
|
+
return {
|
|
11
|
+
claudeSettingsPath: path.join(paths.homeDir, '.claude', 'settings.json'),
|
|
12
|
+
claudeUserConfigPath: path.join(paths.homeDir, '.claude.json'),
|
|
13
|
+
claudeInstructionPath: path.join(paths.homeDir, '.claude', 'CLAUDE.md'),
|
|
14
|
+
copilotSettingsPath: path.join(paths.homeDir, '.copilot', 'settings.json'),
|
|
15
|
+
copilotMcpPath: path.join(paths.homeDir, '.copilot', 'mcp-config.json'),
|
|
16
|
+
copilotInstructionPath: path.join(paths.homeDir, '.copilot', 'instructions', 'vesper-memory.instructions.md'),
|
|
17
|
+
codexConfigPath: path.join(paths.codexHome, 'config.toml'),
|
|
18
|
+
codexInstructionPath: path.join(paths.codexHome, 'AGENTS.md'),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function objectValue(value) {
|
|
22
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
23
|
+
}
|
|
24
|
+
function arrayValue(value) {
|
|
25
|
+
return Array.isArray(value) ? value : [];
|
|
26
|
+
}
|
|
27
|
+
function equalJson(left, right) {
|
|
28
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
29
|
+
}
|
|
30
|
+
async function exists(filePath) {
|
|
31
|
+
try {
|
|
32
|
+
await fs.access(filePath);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code === 'ENOENT') {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function readJsonObject(filePath) {
|
|
43
|
+
const value = await readJsonFile(filePath);
|
|
44
|
+
if (value === null) {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
48
|
+
throw new Error(`Configuration file must contain a JSON object: ${filePath}`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
async function readText(filePath) {
|
|
53
|
+
try {
|
|
54
|
+
return await fs.readFile(filePath, 'utf8');
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error.code === 'ENOENT') {
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function writeTextAtomic(filePath, content) {
|
|
64
|
+
if (!content) {
|
|
65
|
+
await fs.rm(filePath, { force: true });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
69
|
+
const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
70
|
+
try {
|
|
71
|
+
await fs.writeFile(temporary, content, 'utf8');
|
|
72
|
+
await fs.rename(temporary, filePath);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
await fs.rm(temporary, { force: true }).catch(() => undefined);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function markedInstruction() {
|
|
79
|
+
return `${INSTRUCTION_BEGIN}\n${MEMORY_INSTRUCTION}\n${INSTRUCTION_END}`;
|
|
80
|
+
}
|
|
81
|
+
function replaceMarkedBlock(content, begin, end, replacement) {
|
|
82
|
+
const beginIndex = content.indexOf(begin);
|
|
83
|
+
const endIndex = content.indexOf(end);
|
|
84
|
+
if ((beginIndex >= 0) !== (endIndex >= 0) || (beginIndex >= 0 && endIndex < beginIndex)) {
|
|
85
|
+
throw new Error(`Malformed managed block: ${begin}`);
|
|
86
|
+
}
|
|
87
|
+
let without = content;
|
|
88
|
+
if (beginIndex >= 0) {
|
|
89
|
+
without = `${content.slice(0, beginIndex)}${content.slice(endIndex + end.length)}`;
|
|
90
|
+
}
|
|
91
|
+
without = without.replace(/\s+$/, '');
|
|
92
|
+
if (!replacement) {
|
|
93
|
+
return without ? `${without}\n` : '';
|
|
94
|
+
}
|
|
95
|
+
return without ? `${without}\n\n${replacement}\n` : `${replacement}\n`;
|
|
96
|
+
}
|
|
97
|
+
function assertMarkedBlockOwned(content, begin, end, previous) {
|
|
98
|
+
const beginIndex = content.indexOf(begin);
|
|
99
|
+
const endIndex = content.indexOf(end);
|
|
100
|
+
if ((beginIndex >= 0) !== (endIndex >= 0) || (beginIndex >= 0 && endIndex < beginIndex)) {
|
|
101
|
+
throw new Error('Malformed managed block: ' + begin);
|
|
102
|
+
}
|
|
103
|
+
const current = beginIndex >= 0 ? content.slice(beginIndex, endIndex + end.length) : undefined;
|
|
104
|
+
if (previous !== undefined && current !== undefined && current !== previous) {
|
|
105
|
+
throw new Error('Managed block changed ownership: ' + begin);
|
|
106
|
+
}
|
|
107
|
+
if (previous === undefined && current !== undefined) {
|
|
108
|
+
throw new Error('Managed block already exists without owned install state: ' + begin);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function quoteToml(value) {
|
|
112
|
+
return JSON.stringify(value);
|
|
113
|
+
}
|
|
114
|
+
function codexMcpBlock(nodePath, launcherPath) {
|
|
115
|
+
return `${CODEX_MCP_BEGIN}\n[mcp_servers.vesper-memory]\ncommand = ${quoteToml(nodePath)}\nargs = [${quoteToml(launcherPath)}, "mcp"]\n${CODEX_MCP_END}`;
|
|
116
|
+
}
|
|
117
|
+
function quoteCommand(value) {
|
|
118
|
+
return '"' + value.replace(/"/g, '\\"') + '"';
|
|
119
|
+
}
|
|
120
|
+
function ownedClaudeHook(nodePath, launcherPath) {
|
|
121
|
+
return {
|
|
122
|
+
hooks: [{
|
|
123
|
+
type: 'command', command: `${quoteCommand(nodePath)} ${quoteCommand(launcherPath)} report-session --provider claude-code`,
|
|
124
|
+
timeout: 10, statusMessage: 'Queue session for Vesper Memory',
|
|
125
|
+
}],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function ownedCopilotHook(nodePath, launcherPath) {
|
|
129
|
+
return { type: 'command', exec: nodePath, args: [launcherPath, 'report-session', '--provider', 'github-copilot'], timeoutSec: 10 };
|
|
130
|
+
}
|
|
131
|
+
function stdioMcp(type, nodePath, launcherPath) {
|
|
132
|
+
return type === 'stdio'
|
|
133
|
+
? { type, command: nodePath, args: [launcherPath, 'mcp'], env: {} }
|
|
134
|
+
: { type, command: nodePath, args: [launcherPath, 'mcp'], tools: ['*'] };
|
|
135
|
+
}
|
|
136
|
+
export async function detectInstalledProviders(paths) {
|
|
137
|
+
const candidates = [
|
|
138
|
+
['claude-code', path.join(paths.homeDir, '.claude')],
|
|
139
|
+
['github-copilot', path.join(paths.homeDir, '.copilot')],
|
|
140
|
+
['openai-codex', paths.codexHome],
|
|
141
|
+
];
|
|
142
|
+
const found = [];
|
|
143
|
+
for (const [provider, location] of candidates) {
|
|
144
|
+
if (await exists(location)) {
|
|
145
|
+
found.push(provider);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return found;
|
|
149
|
+
}
|
|
150
|
+
export function providerTargetPaths(paths, providers) {
|
|
151
|
+
const locations = providerLocations(paths);
|
|
152
|
+
const result = [];
|
|
153
|
+
if (providers.includes('claude-code')) {
|
|
154
|
+
result.push(locations.claudeSettingsPath, locations.claudeUserConfigPath, locations.claudeInstructionPath);
|
|
155
|
+
}
|
|
156
|
+
if (providers.includes('github-copilot')) {
|
|
157
|
+
result.push(locations.copilotSettingsPath, locations.copilotMcpPath, locations.copilotInstructionPath);
|
|
158
|
+
}
|
|
159
|
+
if (providers.includes('openai-codex')) {
|
|
160
|
+
result.push(locations.codexConfigPath, locations.codexInstructionPath);
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
export async function applyProviderIntegrations(options) {
|
|
165
|
+
const locations = providerLocations(options.paths);
|
|
166
|
+
const owned = { providers: [...options.providers] };
|
|
167
|
+
if (options.providers.includes('claude-code')) {
|
|
168
|
+
if (options.installHooks !== false) {
|
|
169
|
+
const settings = await readJsonObject(locations.claudeSettingsPath);
|
|
170
|
+
const hooks = objectValue(settings.hooks);
|
|
171
|
+
let entries = arrayValue(hooks.SessionEnd);
|
|
172
|
+
if (options.previous?.claudeHook) {
|
|
173
|
+
entries = entries.filter((entry) => !equalJson(entry, options.previous.claudeHook));
|
|
174
|
+
}
|
|
175
|
+
owned.claudeHook = ownedClaudeHook(options.nodePath, options.launcherPath);
|
|
176
|
+
if (!entries.some((entry) => equalJson(entry, owned.claudeHook))) {
|
|
177
|
+
entries.push(owned.claudeHook);
|
|
178
|
+
}
|
|
179
|
+
hooks.SessionEnd = entries;
|
|
180
|
+
settings.hooks = hooks;
|
|
181
|
+
await writeJsonAtomic(locations.claudeSettingsPath, settings);
|
|
182
|
+
await options.afterMutation?.('claude-hook');
|
|
183
|
+
}
|
|
184
|
+
const config = await readJsonObject(locations.claudeUserConfigPath);
|
|
185
|
+
const servers = objectValue(config.mcpServers);
|
|
186
|
+
if (options.previous?.claudeMcp) {
|
|
187
|
+
if (servers['vesper-memory'] !== undefined && !equalJson(servers['vesper-memory'], options.previous.claudeMcp)) {
|
|
188
|
+
throw new Error('Claude MCP vesper-memory changed ownership; refusing to modify it');
|
|
189
|
+
}
|
|
190
|
+
delete servers['vesper-memory'];
|
|
191
|
+
}
|
|
192
|
+
if (options.trusted) {
|
|
193
|
+
if (servers['vesper-memory'] !== undefined) {
|
|
194
|
+
throw new Error('Claude MCP name vesper-memory is already owned by another configuration');
|
|
195
|
+
}
|
|
196
|
+
owned.claudeMcp = stdioMcp('stdio', options.nodePath, options.launcherPath);
|
|
197
|
+
servers['vesper-memory'] = owned.claudeMcp;
|
|
198
|
+
}
|
|
199
|
+
if (Object.keys(servers).length > 0) {
|
|
200
|
+
config.mcpServers = servers;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
delete config.mcpServers;
|
|
204
|
+
}
|
|
205
|
+
if (Object.keys(config).length > 0 || await exists(locations.claudeUserConfigPath)) {
|
|
206
|
+
await writeJsonAtomic(locations.claudeUserConfigPath, config);
|
|
207
|
+
await options.afterMutation?.('claude-mcp');
|
|
208
|
+
}
|
|
209
|
+
owned.claudeInstruction = options.trusted ? markedInstruction() : undefined;
|
|
210
|
+
const claudeInstructions = await readText(locations.claudeInstructionPath);
|
|
211
|
+
assertMarkedBlockOwned(claudeInstructions, INSTRUCTION_BEGIN, INSTRUCTION_END, options.previous?.claudeInstruction);
|
|
212
|
+
await writeTextAtomic(locations.claudeInstructionPath, replaceMarkedBlock(claudeInstructions, INSTRUCTION_BEGIN, INSTRUCTION_END, owned.claudeInstruction));
|
|
213
|
+
await options.afterMutation?.('claude-instruction');
|
|
214
|
+
}
|
|
215
|
+
if (options.providers.includes('github-copilot')) {
|
|
216
|
+
if (options.installHooks !== false) {
|
|
217
|
+
const settings = await readJsonObject(locations.copilotSettingsPath);
|
|
218
|
+
const hooks = objectValue(settings.hooks);
|
|
219
|
+
let entries = arrayValue(hooks.sessionEnd);
|
|
220
|
+
if (options.previous?.copilotHook) {
|
|
221
|
+
entries = entries.filter((entry) => !equalJson(entry, options.previous.copilotHook));
|
|
222
|
+
}
|
|
223
|
+
owned.copilotHook = ownedCopilotHook(options.nodePath, options.launcherPath);
|
|
224
|
+
if (!entries.some((entry) => equalJson(entry, owned.copilotHook))) {
|
|
225
|
+
entries.push(owned.copilotHook);
|
|
226
|
+
}
|
|
227
|
+
hooks.sessionEnd = entries;
|
|
228
|
+
settings.hooks = hooks;
|
|
229
|
+
await writeJsonAtomic(locations.copilotSettingsPath, settings);
|
|
230
|
+
await options.afterMutation?.('copilot-hook');
|
|
231
|
+
}
|
|
232
|
+
const config = await readJsonObject(locations.copilotMcpPath);
|
|
233
|
+
const servers = objectValue(config.mcpServers);
|
|
234
|
+
if (options.previous?.copilotMcp) {
|
|
235
|
+
if (servers['vesper-memory'] !== undefined && !equalJson(servers['vesper-memory'], options.previous.copilotMcp)) {
|
|
236
|
+
throw new Error('Copilot MCP vesper-memory changed ownership; refusing to modify it');
|
|
237
|
+
}
|
|
238
|
+
delete servers['vesper-memory'];
|
|
239
|
+
}
|
|
240
|
+
if (options.trusted) {
|
|
241
|
+
if (servers['vesper-memory'] !== undefined) {
|
|
242
|
+
throw new Error('Copilot MCP name vesper-memory is already owned by another configuration');
|
|
243
|
+
}
|
|
244
|
+
owned.copilotMcp = stdioMcp('local', options.nodePath, options.launcherPath);
|
|
245
|
+
servers['vesper-memory'] = owned.copilotMcp;
|
|
246
|
+
}
|
|
247
|
+
if (Object.keys(servers).length > 0) {
|
|
248
|
+
config.mcpServers = servers;
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
delete config.mcpServers;
|
|
252
|
+
}
|
|
253
|
+
if (Object.keys(config).length > 0 || await exists(locations.copilotMcpPath)) {
|
|
254
|
+
await writeJsonAtomic(locations.copilotMcpPath, config);
|
|
255
|
+
await options.afterMutation?.('copilot-mcp');
|
|
256
|
+
}
|
|
257
|
+
if (options.trusted) {
|
|
258
|
+
owned.copilotInstruction = `${MEMORY_INSTRUCTION}\n`;
|
|
259
|
+
const current = await readText(locations.copilotInstructionPath);
|
|
260
|
+
if (current && current !== options.previous?.copilotInstruction && current !== owned.copilotInstruction) {
|
|
261
|
+
throw new Error('Copilot vesper-memory instruction file is not owned by vesper-link');
|
|
262
|
+
}
|
|
263
|
+
await writeTextAtomic(locations.copilotInstructionPath, owned.copilotInstruction);
|
|
264
|
+
await options.afterMutation?.('copilot-instruction');
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
const current = await readText(locations.copilotInstructionPath);
|
|
268
|
+
if (current && options.previous?.copilotInstruction !== undefined && current !== options.previous.copilotInstruction) {
|
|
269
|
+
throw new Error('Copilot vesper-memory instruction file changed ownership; refusing to modify it');
|
|
270
|
+
}
|
|
271
|
+
if (!current || current === options.previous?.copilotInstruction) {
|
|
272
|
+
await fs.rm(locations.copilotInstructionPath, { force: true });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (options.providers.includes('openai-codex')) {
|
|
277
|
+
let config = await readText(locations.codexConfigPath);
|
|
278
|
+
assertMarkedBlockOwned(config, CODEX_MCP_BEGIN, CODEX_MCP_END, options.previous?.codexMcpBlock);
|
|
279
|
+
const unmanagedTable = /^\s*\[mcp_servers\.vesper-memory\]\s*$/m.test(replaceMarkedBlock(config, CODEX_MCP_BEGIN, CODEX_MCP_END));
|
|
280
|
+
if (options.trusted && unmanagedTable) {
|
|
281
|
+
throw new Error('Codex MCP name vesper-memory is already owned by another configuration');
|
|
282
|
+
}
|
|
283
|
+
owned.codexMcpBlock = options.trusted ? codexMcpBlock(options.nodePath, options.launcherPath) : undefined;
|
|
284
|
+
config = replaceMarkedBlock(config, CODEX_MCP_BEGIN, CODEX_MCP_END, owned.codexMcpBlock);
|
|
285
|
+
await writeTextAtomic(locations.codexConfigPath, config);
|
|
286
|
+
await options.afterMutation?.('codex-mcp');
|
|
287
|
+
owned.codexInstruction = options.trusted ? markedInstruction() : undefined;
|
|
288
|
+
const codexInstructions = await readText(locations.codexInstructionPath);
|
|
289
|
+
assertMarkedBlockOwned(codexInstructions, INSTRUCTION_BEGIN, INSTRUCTION_END, options.previous?.codexInstruction);
|
|
290
|
+
await writeTextAtomic(locations.codexInstructionPath, replaceMarkedBlock(codexInstructions, INSTRUCTION_BEGIN, INSTRUCTION_END, owned.codexInstruction));
|
|
291
|
+
await options.afterMutation?.('codex-instruction');
|
|
292
|
+
}
|
|
293
|
+
return owned;
|
|
294
|
+
}
|
|
295
|
+
export async function removeProviderIntegrations(paths, owned) {
|
|
296
|
+
await applyProviderIntegrations({
|
|
297
|
+
paths, providers: owned.providers, trusted: false, nodePath: '', launcherPath: '', previous: owned,
|
|
298
|
+
installHooks: false,
|
|
299
|
+
});
|
|
300
|
+
const locations = providerLocations(paths);
|
|
301
|
+
for (const provider of owned.providers) {
|
|
302
|
+
if (provider === 'claude-code' && owned.claudeHook) {
|
|
303
|
+
const settings = await readJsonObject(locations.claudeSettingsPath);
|
|
304
|
+
const hooks = objectValue(settings.hooks);
|
|
305
|
+
const entries = arrayValue(hooks.SessionEnd).filter((entry) => !equalJson(entry, owned.claudeHook));
|
|
306
|
+
if (entries.length > 0) {
|
|
307
|
+
hooks.SessionEnd = entries;
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
delete hooks.SessionEnd;
|
|
311
|
+
}
|
|
312
|
+
if (Object.keys(hooks).length > 0) {
|
|
313
|
+
settings.hooks = hooks;
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
delete settings.hooks;
|
|
317
|
+
}
|
|
318
|
+
await writeJsonAtomic(locations.claudeSettingsPath, settings);
|
|
319
|
+
}
|
|
320
|
+
if (provider === 'github-copilot' && owned.copilotHook) {
|
|
321
|
+
const settings = await readJsonObject(locations.copilotSettingsPath);
|
|
322
|
+
const hooks = objectValue(settings.hooks);
|
|
323
|
+
const entries = arrayValue(hooks.sessionEnd).filter((entry) => !equalJson(entry, owned.copilotHook));
|
|
324
|
+
if (entries.length > 0) {
|
|
325
|
+
hooks.sessionEnd = entries;
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
delete hooks.sessionEnd;
|
|
329
|
+
}
|
|
330
|
+
if (Object.keys(hooks).length > 0) {
|
|
331
|
+
settings.hooks = hooks;
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
delete settings.hooks;
|
|
335
|
+
}
|
|
336
|
+
await writeJsonAtomic(locations.copilotSettingsPath, settings);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
package/dist/queue.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type PendingMarker, type SessionCandidate, type SessionState } from './session.js';
|
|
2
|
+
export declare const STABLE_FILE_AGE_MS: number;
|
|
3
|
+
export declare const MAX_SESSIONS_PER_RUN = 2000;
|
|
4
|
+
export interface QueueItem {
|
|
5
|
+
candidate: SessionCandidate;
|
|
6
|
+
markerPath?: string;
|
|
7
|
+
markerQueuedAt?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function isStableForUpload(candidate: Pick<SessionCandidate, 'discovery' | 'archived'>, mtimeMs: number, nowMs: number): boolean;
|
|
10
|
+
export declare function selectQueueItems(options: {
|
|
11
|
+
markers: Array<{
|
|
12
|
+
path: string;
|
|
13
|
+
marker: PendingMarker;
|
|
14
|
+
}>;
|
|
15
|
+
scanned: SessionCandidate[];
|
|
16
|
+
states: Map<string, SessionState>;
|
|
17
|
+
nowMs: number;
|
|
18
|
+
limit?: number;
|
|
19
|
+
}): Promise<QueueItem[]>;
|