@jive-sh/skill-sync 0.3.0-latest-darwin-arm64 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/bin/package.json +1 -0
- package/bin/skill-sync +579 -0
- package/bin/skill-sync-daemon +579 -0
- package/dist/daemons/skill-sync-daemon.js +401 -0
- package/dist/daemons/skill-sync-daemon.js.map +1 -0
- package/dist/entrypoints/skill-sync.js +114 -0
- package/dist/entrypoints/skill-sync.js.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/config.js +134 -0
- package/dist/internal/config.js.map +1 -0
- package/dist/internal/control.js +101 -0
- package/dist/internal/control.js.map +1 -0
- package/dist/internal/folders.js +65 -0
- package/dist/internal/folders.js.map +1 -0
- package/dist/internal/notify.js +50 -0
- package/dist/internal/notify.js.map +1 -0
- package/dist/internal/path.js +36 -0
- package/dist/internal/path.js.map +1 -0
- package/dist/internal/reconcile.js +124 -0
- package/dist/internal/reconcile.js.map +1 -0
- package/dist/internal/sync.js +241 -0
- package/dist/internal/sync.js.map +1 -0
- package/dist/internal/wire.js +13 -0
- package/dist/internal/wire.js.map +1 -0
- package/install.cjs +579 -0
- package/package.json +39 -11
- package/src/daemons/skill-sync-daemon.ts +397 -0
- package/src/entrypoints/skill-sync.ts +116 -0
- package/src/index.ts +15 -0
- package/src/internal/config.ts +171 -0
- package/src/internal/control.ts +123 -0
- package/src/internal/folders.ts +69 -0
- package/src/internal/notify.ts +53 -0
- package/src/internal/path.ts +32 -0
- package/src/internal/reconcile.ts +166 -0
- package/src/internal/sync.ts +304 -0
- package/src/internal/wire.ts +162 -0
- package/multi-call-binary +0 -4
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir, hostname, userInfo } from "node:os";
|
|
4
|
+
import { join, relative, sep } from "node:path";
|
|
5
|
+
/**
|
|
6
|
+
* Per-machine settings and sync state, both under `~/.jive/`. The sync token is not here: it lives only in
|
|
7
|
+
* the running daemon's memory.
|
|
8
|
+
*
|
|
9
|
+
* A file rather than environment variables: the endpoint and which machine this is differ per computer
|
|
10
|
+
* and have to survive a login, which is exactly what a config file is for and exactly what an environment
|
|
11
|
+
* variable is not. Which folders to sync is not in here: they are detected, and the choice to leave one
|
|
12
|
+
* out is kept on the server so the dashboard can change it and a reinstalled daemon still remembers it.
|
|
13
|
+
*/
|
|
14
|
+
const JIVE_HOME = join(homedir(), ".jive");
|
|
15
|
+
export const CONFIG_PATH = join(JIVE_HOME, "skill-sync.json");
|
|
16
|
+
const STATE_PATH = join(JIVE_HOME, "skill-sync-state.json");
|
|
17
|
+
/**
|
|
18
|
+
* Where versions before 0.3 kept the sync token. The token now lives only in the running daemon's memory,
|
|
19
|
+
* so the daemon deletes this file whenever it finds one: a credential left on disk is one anything that
|
|
20
|
+
* can read the home directory can copy.
|
|
21
|
+
*/
|
|
22
|
+
export const LEGACY_TOKEN_PATH = join(homedir(), ".skill-sync-token.json");
|
|
23
|
+
/**
|
|
24
|
+
* How the CLI reaches the running daemon: a Unix socket in `~/.jive/`, or a named pipe on Windows. Only
|
|
25
|
+
* this user can open either, which is what lets the CLI hand the daemon work that involves the token.
|
|
26
|
+
*/
|
|
27
|
+
export const CONTROL_SOCKET = process.platform === "win32"
|
|
28
|
+
? `\\\\.\\pipe\\jive-skill-sync-${userInfo().username}`
|
|
29
|
+
: join(JIVE_HOME, "skill-sync.sock");
|
|
30
|
+
/** Managed by Claude's own cloud sync; see `Settings.exclude`. */
|
|
31
|
+
const DEFAULT_EXCLUDE = ["synced"];
|
|
32
|
+
/**
|
|
33
|
+
* The settings, writing a template on first run. Undefined when the file lacks an endpoint or a machine
|
|
34
|
+
* name, which only happens if someone edited it.
|
|
35
|
+
*/
|
|
36
|
+
export function loadSettings() {
|
|
37
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
38
|
+
mkdirSync(JIVE_HOME, { recursive: true });
|
|
39
|
+
writeFileSync(CONFIG_PATH, `${JSON.stringify({
|
|
40
|
+
endpoint: "https://skill-sync.jive.sh",
|
|
41
|
+
machine: machineName(),
|
|
42
|
+
exclude: DEFAULT_EXCLUDE,
|
|
43
|
+
}, null, 2)}\n`);
|
|
44
|
+
}
|
|
45
|
+
const settings = readJson(CONFIG_PATH);
|
|
46
|
+
if (!settings.endpoint || !settings.machine)
|
|
47
|
+
return undefined;
|
|
48
|
+
return { endpoint: settings.endpoint, machine: settings.machine, exclude: settings.exclude ?? DEFAULT_EXCLUDE };
|
|
49
|
+
}
|
|
50
|
+
function readJson(path) {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Every folder's sync state.
|
|
60
|
+
*
|
|
61
|
+
* Before folders were detected the file held one folder's `{ cursor, known }`, for the folder then named
|
|
62
|
+
* by `skillsDir` in the settings. That state is carried over to that folder: dropping it would make the
|
|
63
|
+
* folder look never-synced, and every file in it that differs from the server would become a conflict.
|
|
64
|
+
*/
|
|
65
|
+
export function loadStates() {
|
|
66
|
+
const stored = readJson(STATE_PATH);
|
|
67
|
+
if (stored.folders !== undefined)
|
|
68
|
+
return stored.folders;
|
|
69
|
+
const legacyFolder = readJson(CONFIG_PATH).skillsDir;
|
|
70
|
+
if (legacyFolder !== undefined && stored.cursor !== undefined && stored.known !== undefined) {
|
|
71
|
+
return { [legacyFolder]: { cursor: stored.cursor, known: stored.known } };
|
|
72
|
+
}
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
export function stateFor(states, folder) {
|
|
76
|
+
states[folder] ??= { cursor: 0, known: {} };
|
|
77
|
+
return states[folder];
|
|
78
|
+
}
|
|
79
|
+
export function saveStates(states) {
|
|
80
|
+
mkdirSync(JIVE_HOME, { recursive: true });
|
|
81
|
+
writeFileSync(STATE_PATH, `${JSON.stringify({ folders: states }, null, 2)}\n`);
|
|
82
|
+
}
|
|
83
|
+
/** `HOSTNAME` is not set on macOS, so the template was writing "unknown" on exactly this machine. */
|
|
84
|
+
function machineName() {
|
|
85
|
+
return hostname().replace(/\.local$/, "").replace(/[^A-Za-z0-9-]+/g, "-") || "unknown";
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The `skills` folder of every Claude home on this machine: `~/.claude` and each `~/.claude-<name>`.
|
|
89
|
+
*
|
|
90
|
+
* Every home is a candidate, whether or not it has a `skills` folder yet, so a Claude account added
|
|
91
|
+
* later joins without anyone listing it. Which of them actually sync is the user's choice on the
|
|
92
|
+
* dashboard; a folder that is synced and missing is created.
|
|
93
|
+
*/
|
|
94
|
+
export function detectFolders() {
|
|
95
|
+
return readdirSync(homedir())
|
|
96
|
+
.filter((entry) => entry === ".claude" || entry.startsWith(".claude-"))
|
|
97
|
+
.filter((entry) => statSync(join(homedir(), entry)).isDirectory())
|
|
98
|
+
.map((entry) => join(homedir(), entry, "skills"))
|
|
99
|
+
.sort();
|
|
100
|
+
}
|
|
101
|
+
export function hashBytes(bytes) {
|
|
102
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
103
|
+
}
|
|
104
|
+
/** Suffix for the copy kept when an incoming change would have overwritten a local edit. */
|
|
105
|
+
export const CONFLICT_SUFFIX = ".local";
|
|
106
|
+
/**
|
|
107
|
+
* Every file under a synced folder, as paths relative to it with forward slashes.
|
|
108
|
+
*
|
|
109
|
+
* Normalised because the same skill synced from Windows and macOS must be one row, not two.
|
|
110
|
+
*
|
|
111
|
+
* Conflict copies are skipped. They are this machine's evidence of a collision, not content anyone else
|
|
112
|
+
* asked for, and syncing them spreads one machine's mess to every other machine.
|
|
113
|
+
*/
|
|
114
|
+
export function listLocalFiles(folder, exclude = DEFAULT_EXCLUDE) {
|
|
115
|
+
if (!existsSync(folder))
|
|
116
|
+
return [];
|
|
117
|
+
const found = [];
|
|
118
|
+
const walk = (dir, top) => {
|
|
119
|
+
for (const entry of readdirSync(dir)) {
|
|
120
|
+
if (entry.startsWith("."))
|
|
121
|
+
continue;
|
|
122
|
+
if (top && exclude.includes(entry))
|
|
123
|
+
continue;
|
|
124
|
+
const full = join(dir, entry);
|
|
125
|
+
if (statSync(full).isDirectory())
|
|
126
|
+
walk(full, false);
|
|
127
|
+
else if (!entry.endsWith(CONFLICT_SUFFIX))
|
|
128
|
+
found.push(relative(folder, full).split(sep).join("/"));
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
walk(folder, true);
|
|
132
|
+
return found.sort();
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../../src/internal/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createConnection, createServer } from "node:net";
|
|
2
|
+
import { chmodSync, rmSync } from "node:fs";
|
|
3
|
+
import { CONTROL_SOCKET } from "./config";
|
|
4
|
+
const isFinal = (event) => event.type !== "open";
|
|
5
|
+
/** The daemon is not running, as opposed to running and refusing the request. */
|
|
6
|
+
export class DaemonUnreachable extends Error {
|
|
7
|
+
}
|
|
8
|
+
/** Sends one request and calls `onEvent` for each event until the final one, which it resolves with. */
|
|
9
|
+
export function request(message, onEvent) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
let settled = false;
|
|
12
|
+
const socket = createConnection(CONTROL_SOCKET);
|
|
13
|
+
socket.on("connect", () => socket.write(`${JSON.stringify(message)}\n`));
|
|
14
|
+
socket.on("error", (cause) => {
|
|
15
|
+
if (settled)
|
|
16
|
+
return;
|
|
17
|
+
settled = true;
|
|
18
|
+
reject(cause.code === "ENOENT" || cause.code === "ECONNREFUSED"
|
|
19
|
+
? new DaemonUnreachable(`the skill-sync daemon is not running (${cause.code} on ${CONTROL_SOCKET})`)
|
|
20
|
+
: cause);
|
|
21
|
+
});
|
|
22
|
+
readLines(socket, (line) => {
|
|
23
|
+
const event = JSON.parse(line);
|
|
24
|
+
onEvent(event);
|
|
25
|
+
if (isFinal(event) && !settled) {
|
|
26
|
+
settled = true;
|
|
27
|
+
socket.end();
|
|
28
|
+
resolve(event);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
socket.on("close", () => {
|
|
32
|
+
if (!settled) {
|
|
33
|
+
settled = true;
|
|
34
|
+
reject(new Error("the daemon closed the connection without answering"));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Listens on the control socket. Resolves false, without listening, when another live daemon already
|
|
41
|
+
* holds it: two installs of skill-sync mean two daemons, and the second stands down rather than fight.
|
|
42
|
+
*/
|
|
43
|
+
export async function serve(handle) {
|
|
44
|
+
if (await answers())
|
|
45
|
+
return false;
|
|
46
|
+
// Nothing answered, so a socket file left here is from a daemon that died; it would block the listen.
|
|
47
|
+
if (process.platform !== "win32")
|
|
48
|
+
rmSync(CONTROL_SOCKET, { force: true });
|
|
49
|
+
const server = createServer((socket) => {
|
|
50
|
+
const reply = {
|
|
51
|
+
send: (event) => {
|
|
52
|
+
if (socket.writable)
|
|
53
|
+
socket.write(`${JSON.stringify(event)}\n`);
|
|
54
|
+
if (isFinal(event))
|
|
55
|
+
socket.end();
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
readLines(socket, (line) => {
|
|
59
|
+
try {
|
|
60
|
+
handle(JSON.parse(line), reply);
|
|
61
|
+
}
|
|
62
|
+
catch (cause) {
|
|
63
|
+
reply.send({ type: "error", message: cause instanceof Error ? cause.message : String(cause) });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
socket.on("error", () => undefined); // A CLI that went away mid-request; nothing to tell it.
|
|
67
|
+
});
|
|
68
|
+
await new Promise((resolve, reject) => {
|
|
69
|
+
server.once("error", reject);
|
|
70
|
+
server.listen(CONTROL_SOCKET, () => resolve());
|
|
71
|
+
});
|
|
72
|
+
// Only this user may talk to the daemon that holds their token.
|
|
73
|
+
if (process.platform !== "win32")
|
|
74
|
+
chmodSync(CONTROL_SOCKET, 0o600);
|
|
75
|
+
return server;
|
|
76
|
+
}
|
|
77
|
+
/** Whether a live daemon is listening on the control socket. */
|
|
78
|
+
function answers() {
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
const socket = createConnection(CONTROL_SOCKET);
|
|
81
|
+
socket.on("connect", () => {
|
|
82
|
+
socket.end();
|
|
83
|
+
resolve(true);
|
|
84
|
+
});
|
|
85
|
+
socket.on("error", () => resolve(false));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function readLines(socket, onLine) {
|
|
89
|
+
let buffer = "";
|
|
90
|
+
socket.on("data", (chunk) => {
|
|
91
|
+
buffer += chunk.toString();
|
|
92
|
+
let newline;
|
|
93
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
94
|
+
const line = buffer.slice(0, newline).trim();
|
|
95
|
+
buffer = buffer.slice(newline + 1);
|
|
96
|
+
if (line !== "")
|
|
97
|
+
onLine(line);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=control.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"control.js","sourceRoot":"","sources":["../../../../src/internal/control.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { detectFolders, hashBytes, listLocalFiles } from "./config";
|
|
4
|
+
import { skillOf, } from "./wire";
|
|
5
|
+
/**
|
|
6
|
+
* What this machine tells the service about its folders, and what it hears back.
|
|
7
|
+
*
|
|
8
|
+
* The daemon and the CLI both register, so this lives in one place: they must describe a folder the same
|
|
9
|
+
* way, or the dashboard would show whichever of them spoke last.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The skills on disk in one folder, with each file's hash. Hashing every file on each check is cheap at
|
|
13
|
+
* skills' size, and it is what lets the dashboard show whether this folder's copy is the synced one.
|
|
14
|
+
*/
|
|
15
|
+
export function localSkills(folder, config, states) {
|
|
16
|
+
const known = Object.keys(states[folder]?.known ?? {});
|
|
17
|
+
const byName = new Map();
|
|
18
|
+
for (const path of listLocalFiles(folder, config.exclude)) {
|
|
19
|
+
const name = skillOf(path);
|
|
20
|
+
let hash;
|
|
21
|
+
try {
|
|
22
|
+
hash = hashBytes(readFileSync(join(folder, path)));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
continue; // Deleted between listing and reading; the next check reports the folder as it is.
|
|
26
|
+
}
|
|
27
|
+
byName.set(name, [...(byName.get(name) ?? []), { path, hash }]);
|
|
28
|
+
}
|
|
29
|
+
return [...byName].map(([name, files]) => ({
|
|
30
|
+
name,
|
|
31
|
+
files,
|
|
32
|
+
tracked: known.some((path) => skillOf(path) === name),
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
/** Everything registration sends, so a caller can tell when it has changed and is worth re-sending. */
|
|
36
|
+
export function describe(config, states) {
|
|
37
|
+
return { folders: detectFolders().map((path) => ({ path, skills: localSkills(path, config, states) })) };
|
|
38
|
+
}
|
|
39
|
+
/** Registers this machine's folders and skills; returns the folders to sync and the skills in each. */
|
|
40
|
+
export async function register(config, request) {
|
|
41
|
+
const response = await fetch(`${config.endpoint}/folders`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: authHeaders(config, { "content-type": "application/json" }),
|
|
44
|
+
body: JSON.stringify(request),
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok)
|
|
47
|
+
throw new Error(`POST /folders: ${response.status} ${await response.text()}`);
|
|
48
|
+
return (await response.json()).folders;
|
|
49
|
+
}
|
|
50
|
+
/** Sends merges to the audit log. A failure here is logged by the caller, never fatal to a sync. */
|
|
51
|
+
export async function logReconciliations(config, entries) {
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const response = await fetch(`${config.endpoint}/reconciliations`, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: authHeaders(config, { "content-type": "application/json" }),
|
|
56
|
+
body: JSON.stringify(entry),
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok)
|
|
59
|
+
throw new Error(`POST /reconciliations: ${response.status} ${await response.text()}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function authHeaders(config, extra) {
|
|
63
|
+
return { authorization: `Bearer ${config.token}`, "x-skill-sync-machine": config.machine, ...extra };
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=folders.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"folders.js","sourceRoot":"","sources":["../../../../src/internal/folders.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* A desktop notification, best effort.
|
|
4
|
+
*
|
|
5
|
+
* Best effort is the whole contract: a daemon that cannot tell you about a conflict is worse than one
|
|
6
|
+
* that cannot sync, but a daemon that *dies* because a notification helper is missing is worse than
|
|
7
|
+
* both. Every failure here is swallowed.
|
|
8
|
+
*
|
|
9
|
+
* This works because these daemons are registered per-user and run inside the logged-in session — a
|
|
10
|
+
* LaunchAgent, a `systemctl --user` unit, a scheduled task. A system-wide service would have no desktop
|
|
11
|
+
* to post to.
|
|
12
|
+
*/
|
|
13
|
+
/** No shell anywhere below: the arguments carry file paths, and a path is not a safe thing to interpolate. */
|
|
14
|
+
function run(command, args) {
|
|
15
|
+
try {
|
|
16
|
+
spawn(command, [...args], { stdio: "ignore", detached: true }).unref();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// Swallowed on purpose; see above.
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function notify(title, body) {
|
|
23
|
+
switch (process.platform) {
|
|
24
|
+
case "darwin":
|
|
25
|
+
// AppleScript string literals take double quotes and backslashes as escapes, and nothing else.
|
|
26
|
+
return run("osascript", [
|
|
27
|
+
"-e",
|
|
28
|
+
`display notification "${escapeAppleScript(body)}" with title "${escapeAppleScript(title)}"`,
|
|
29
|
+
]);
|
|
30
|
+
case "linux":
|
|
31
|
+
return run("notify-send", [title, body]);
|
|
32
|
+
case "win32":
|
|
33
|
+
return run("powershell", [
|
|
34
|
+
"-NoProfile", "-NonInteractive", "-Command",
|
|
35
|
+
"[reflection.assembly]::LoadWithPartialName('System.Windows.Forms') > $null;"
|
|
36
|
+
+ "$n = New-Object System.Windows.Forms.NotifyIcon;"
|
|
37
|
+
+ "$n.Icon = [System.Drawing.SystemIcons]::Warning; $n.Visible = $true;"
|
|
38
|
+
+ `$n.ShowBalloonTip(10000, '${escapeSingleQuotes(title)}', '${escapeSingleQuotes(body)}', 'Warning')`,
|
|
39
|
+
]);
|
|
40
|
+
default:
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function escapeAppleScript(value) {
|
|
45
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
46
|
+
}
|
|
47
|
+
function escapeSingleQuotes(value) {
|
|
48
|
+
return value.replace(/'/g, "''");
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=notify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.js","sourceRoot":"","sources":["../../../../src/internal/notify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C;;;;;;;;;;GAUG;AAEH,8GAA8G;AAC9G,SAAS,GAAG,CAAC,OAAe,EAAE,IAA2B;IACvD,IAAI,CAAC;QACH,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;IACzE,CAAC;IAAC,MAAM,CAAC;QACP,mCAAmC;IACrC,CAAC;AACH,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,KAAa,EAAE,IAAY;IAChD,QAAQ,OAAO,CAAC,QAAQ,EAAE,CAAC;QACzB,KAAK,QAAQ;YACX,+FAA+F;YAC/F,OAAO,GAAG,CAAC,WAAW,EAAE;gBACtB,IAAI;gBACJ,yBAAyB,iBAAiB,CAAC,IAAI,CAAC,iBAAiB,iBAAiB,CAAC,KAAK,CAAC,GAAG;aAC7F,CAAC,CAAC;QACL,KAAK,OAAO;YACV,OAAO,GAAG,CAAC,aAAa,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;QAC3C,KAAK,OAAO;YACV,OAAO,GAAG,CAAC,YAAY,EAAE;gBACvB,YAAY,EAAE,iBAAiB,EAAE,UAAU;gBAC3C,6EAA6E;sBAC3E,kDAAkD;sBAClD,sEAAsE;sBACtE,6BAA6B,kBAAkB,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,IAAI,CAAC,eAAe;aACvG,CAAC,CAAC;QACL;YACE,OAAO;IACX,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAa;IACtC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AAC3D,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAa;IACvC,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation for the one piece of untrusted input that becomes a filesystem path.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the Worker and the daemon on purpose, and deliberately free of any Node import so the Worker
|
|
5
|
+
* can bundle it. Both ends check: the server so one compromised client cannot poison what every machine
|
|
6
|
+
* pulls, and the daemon because a server it does not control is not a thing to take paths from either.
|
|
7
|
+
*
|
|
8
|
+
* Without this, a pushed path of `../../.ssh/authorized_keys` is joined to the skills directory and
|
|
9
|
+
* written wherever the daemon's user can write.
|
|
10
|
+
*/
|
|
11
|
+
/** Long enough for any real nesting, short enough that nothing can be used to exhaust anything. */
|
|
12
|
+
const MAX_LENGTH = 512;
|
|
13
|
+
export function invalidPathReason(path) {
|
|
14
|
+
if (path.length === 0 || path.length > MAX_LENGTH)
|
|
15
|
+
return "path is empty or too long";
|
|
16
|
+
if (path.includes("\0"))
|
|
17
|
+
return "path contains a null byte";
|
|
18
|
+
if (path.includes("\\"))
|
|
19
|
+
return "path must use forward slashes";
|
|
20
|
+
if (path.startsWith("/"))
|
|
21
|
+
return "path must be relative";
|
|
22
|
+
// A Windows drive letter is absolute on the machine that matters even though it has no leading slash.
|
|
23
|
+
if (/^[A-Za-z]:/.test(path))
|
|
24
|
+
return "path must be relative";
|
|
25
|
+
for (const segment of path.split("/")) {
|
|
26
|
+
if (segment === "")
|
|
27
|
+
return "path has an empty segment";
|
|
28
|
+
if (segment === "." || segment === "..")
|
|
29
|
+
return "path must not contain . or .. segments";
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
export function isValidPath(path) {
|
|
34
|
+
return invalidPathReason(path) === undefined;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path.js","sourceRoot":"","sources":["../../../../src/internal/path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,mGAAmG;AACnG,MAAM,UAAU,GAAG,GAAG,CAAC;AAEvB,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,UAAU;QAAE,OAAO,2BAA2B,CAAC;IACtF,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,2BAA2B,CAAC;IAC5D,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,+BAA+B,CAAC;IAChE,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,uBAAuB,CAAC;IACzD,sGAAsG;IACtG,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,uBAAuB,CAAC;IAE5D,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACtC,IAAI,OAAO,KAAK,EAAE;YAAE,OAAO,2BAA2B,CAAC;QACvD,IAAI,OAAO,KAAK,GAAG,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO,wCAAwC,CAAC;IAC3F,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,iBAAiB,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC;AAC/C,CAAC"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, delimiter, dirname, join } from "node:path";
|
|
5
|
+
const BACKUP_ROOT = join(homedir(), ".jive", "skill-sync", "reconciled");
|
|
6
|
+
/** A long skill is tens of thousands of tokens to write back out. */
|
|
7
|
+
const TIMEOUT_MS = 15 * 60 * 1000;
|
|
8
|
+
/**
|
|
9
|
+
* A merge shorter than this fraction of the longer input is refused as probably truncated: a model that
|
|
10
|
+
* stops early produces a file that looks fine and silently drops half the rules.
|
|
11
|
+
*/
|
|
12
|
+
const MIN_LENGTH_RATIO = 0.6;
|
|
13
|
+
const SYSTEM_PROMPT = `You merge two versions of one file from a user's Claude skills folder into one.
|
|
14
|
+
Answer in exactly this form and nothing else:
|
|
15
|
+
<conflict>One or two plain sentences: what differs between the versions.</conflict>
|
|
16
|
+
<resolution>One or two plain sentences: what the merge kept from each, and anything dropped or reworded.</resolution>
|
|
17
|
+
<merged>
|
|
18
|
+
The complete merged file, exactly as it should be saved. No code fences.
|
|
19
|
+
</merged>
|
|
20
|
+
Rules for the merge:
|
|
21
|
+
- Keep every rule, example and fact that either version has, unless the other version clearly replaces it.
|
|
22
|
+
- Where the two disagree, prefer the newer version's wording, and keep anything only the older one has.
|
|
23
|
+
- If a base version is given, it is what both started from: use it to tell additions from deletions, and
|
|
24
|
+
honour a deletion made on one side when the other side did not edit that part.
|
|
25
|
+
- Keep the file's frontmatter and structure.
|
|
26
|
+
In <conflict> and <resolution>, call the two versions by the names they are given (for example "the copy in
|
|
27
|
+
~/.claude-chaine/skills on MacBookPro" and "the synced copy"), never "Version A" or "Version B": these are
|
|
28
|
+
read later by a person auditing the merge, who never saw this prompt.`;
|
|
29
|
+
/**
|
|
30
|
+
* Both versions are copied out of the synced folder before anything is written, so a bad merge costs a
|
|
31
|
+
* file copy, not the user's work. Outside the folder so the copies are never synced or read as skills.
|
|
32
|
+
*/
|
|
33
|
+
export function backUp(conflict) {
|
|
34
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
35
|
+
const dir = join(BACKUP_ROOT, stamp, basename(dirname(conflict.folder)), conflict.path);
|
|
36
|
+
mkdirSync(dir, { recursive: true });
|
|
37
|
+
writeFileSync(join(dir, "local"), conflict.local.content);
|
|
38
|
+
writeFileSync(join(dir, "remote"), conflict.remote.content);
|
|
39
|
+
if (conflict.base !== null)
|
|
40
|
+
writeFileSync(join(dir, "base"), conflict.base);
|
|
41
|
+
return dir;
|
|
42
|
+
}
|
|
43
|
+
export async function reconcile(conflict) {
|
|
44
|
+
const sections = [
|
|
45
|
+
`File: ${conflict.path}`,
|
|
46
|
+
conflict.base === null ? "No base version: these two have never been in sync." : `=== the base version both started from:\n${conflict.base}`,
|
|
47
|
+
`=== ${conflict.local.label}:\n${conflict.local.content}`,
|
|
48
|
+
`=== ${conflict.remote.label}:\n${conflict.remote.content}`,
|
|
49
|
+
"Write the merged file now.",
|
|
50
|
+
];
|
|
51
|
+
const answer = await runClaude(sections.join("\n\n-----\n\n"), dirname(conflict.folder));
|
|
52
|
+
const start = answer.indexOf("<merged>");
|
|
53
|
+
const end = answer.lastIndexOf("</merged>");
|
|
54
|
+
if (start === -1 || end <= start)
|
|
55
|
+
throw new Error("Claude's answer had no <merged> section");
|
|
56
|
+
// The file is everything between the tags, minus the newline that follows the opening one.
|
|
57
|
+
const merged = answer.slice(start + "<merged>".length, end).replace(/^\r?\n/, "");
|
|
58
|
+
const conflictText = /<conflict>([\s\S]*?)<\/conflict>/.exec(answer)?.[1]?.trim() ?? "";
|
|
59
|
+
const resolutionText = /<resolution>([\s\S]*?)<\/resolution>/.exec(answer)?.[1]?.trim() ?? "";
|
|
60
|
+
const longest = Math.max(conflict.local.content.length, conflict.remote.content.length);
|
|
61
|
+
if (merged.trim().length === 0)
|
|
62
|
+
throw new Error("Claude returned an empty merge");
|
|
63
|
+
if (merged.length < longest * MIN_LENGTH_RATIO) {
|
|
64
|
+
throw new Error(`the merge is ${merged.length} characters against ${longest}; refusing it as probably truncated`);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
merged: merged.endsWith("\n") ? merged : `${merged}\n`,
|
|
68
|
+
conflict: conflictText || "Claude did not describe the conflict.",
|
|
69
|
+
resolution: resolutionText || "Claude did not describe the resolution.",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** `configDir` is the Claude home that owns the folder, e.g. `~/.claude-chaine` for `~/.claude-chaine/skills`. */
|
|
73
|
+
function runClaude(input, configDir) {
|
|
74
|
+
const claude = findClaude();
|
|
75
|
+
if (claude === undefined)
|
|
76
|
+
return Promise.reject(new Error("the claude CLI is not installed"));
|
|
77
|
+
// Always set, never inherited or left unset: unset, `claude` falls back to `~/.claude` and writes its
|
|
78
|
+
// session files, backups and caches there, recreating that home every time it is deleted.
|
|
79
|
+
const env = { ...process.env, CLAUDE_CONFIG_DIR: configDir };
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const child = spawn(claude, [
|
|
82
|
+
"-p", "--output-format", "json", "--tools", "", "--strict-mcp-config", "--setting-sources", "",
|
|
83
|
+
"--no-session-persistence", "--system-prompt", SYSTEM_PROMPT,
|
|
84
|
+
], { env, cwd: homedir(), stdio: ["pipe", "pipe", "pipe"] });
|
|
85
|
+
let stdout = "";
|
|
86
|
+
let stderr = "";
|
|
87
|
+
child.stdout.on("data", (chunk) => { stdout += chunk.toString(); });
|
|
88
|
+
child.stderr.on("data", (chunk) => { stderr += chunk.toString(); });
|
|
89
|
+
const timer = setTimeout(() => child.kill("SIGTERM"), TIMEOUT_MS);
|
|
90
|
+
child.on("error", (cause) => { clearTimeout(timer); reject(cause); });
|
|
91
|
+
child.on("close", (code) => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
try {
|
|
94
|
+
const result = JSON.parse(stdout);
|
|
95
|
+
if (code !== 0 || result.is_error || typeof result.result !== "string") {
|
|
96
|
+
reject(new Error(`claude failed (exit ${code}): ${result.result ?? stderr.trim()}`));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
resolve(result.result);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
reject(new Error(`claude failed (exit ${code}): ${stderr.trim() || stdout.slice(0, 300)}`));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
child.stdin.end(input);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The daemon starts at login with the service manager's minimal PATH, which rarely includes where the
|
|
111
|
+
* Claude installer puts `claude`, so the usual install locations are checked too.
|
|
112
|
+
*/
|
|
113
|
+
function findClaude() {
|
|
114
|
+
const name = process.platform === "win32" ? "claude.exe" : "claude";
|
|
115
|
+
const fromPath = (process.env["PATH"] ?? "").split(delimiter).filter(Boolean).map((dir) => join(dir, name));
|
|
116
|
+
const known = [
|
|
117
|
+
join(homedir(), ".local", "bin", name),
|
|
118
|
+
join(homedir(), ".claude", "local", "claude"),
|
|
119
|
+
"/opt/homebrew/bin/claude",
|
|
120
|
+
"/usr/local/bin/claude",
|
|
121
|
+
];
|
|
122
|
+
return [...fromPath, ...known].find((candidate) => existsSync(candidate));
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=reconcile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../../../../src/internal/reconcile.ts"],"names":[],"mappings":"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"}
|