@ssheleg/agent-sync 1.2.2

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.
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * agent-sync installer. Zero dependencies.
6
+ *
7
+ * One channel per agent: Claude Code gets the plugin, every other agent gets the
8
+ * skill through the vercel skills CLI, and the plain ~/.claude/skills/agent-sync
9
+ * copy that the skills CLI recreates on its own is pruned afterwards — that
10
+ * duplicate shadows the plugin and silently serves a stale skill.
11
+ */
12
+
13
+ const { spawnSync } = require('child_process');
14
+ const fs = require('fs');
15
+ const os = require('os');
16
+ const path = require('path');
17
+
18
+ const REPO = 'appvillis-com/agent-sync';
19
+ const NAME = 'agent-sync';
20
+ const SHADOW = path.join(os.homedir(), '.claude', 'skills', NAME);
21
+
22
+ const C = {
23
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
24
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
25
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
26
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
27
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
28
+ };
29
+
30
+ function run(cmd, args) {
31
+ process.stdout.write(C.dim(` $ ${cmd} ${args.join(' ')}\n`));
32
+ const r = spawnSync(cmd, args, { stdio: 'inherit' });
33
+ return r.status === 0;
34
+ }
35
+
36
+ function has(cmd) {
37
+ return spawnSync(process.platform === 'win32' ? 'where' : 'which', [cmd], {
38
+ stdio: 'ignore',
39
+ }).status === 0;
40
+ }
41
+
42
+ function usage() {
43
+ console.log(`
44
+ ${C.bold('agent-sync')} — coordination for concurrent agents
45
+
46
+ npx @ssheleg/${NAME} install install for Claude Code and other agents
47
+ npx @ssheleg/${NAME} install --claude-only Claude Code plugin only
48
+ npx @ssheleg/${NAME} install --agent a,b pick agents for the skills CLI
49
+ npx @ssheleg/${NAME} update update every channel, then prune the shadow
50
+ npx @ssheleg/${NAME} --help
51
+
52
+ After installing, initialise the project — this is the step that asks where
53
+ coordination state should live:
54
+
55
+ ${C.bold('/agent-sync init')}
56
+
57
+ It writes .claude/agent-sync.json (committed) and .env.agent-sync (gitignored,
58
+ mode 600), then tells you the one thing only you can do: create an API token in
59
+ your own knowledge-base instance and paste it into that file. The installer never
60
+ asks for a token and never stores one.
61
+ `);
62
+ }
63
+
64
+ function install(argv) {
65
+ const claudeOnly = argv.includes('--claude-only');
66
+ const noClaude = argv.includes('--no-claude');
67
+ const agentIdx = argv.indexOf('--agent');
68
+ const agents = agentIdx !== -1 && argv[agentIdx + 1] ? argv[agentIdx + 1].split(',') : null;
69
+
70
+ let ok = true;
71
+
72
+ if (!noClaude) {
73
+ console.log(C.bold('\nClaude Code — as a plugin'));
74
+ if (!has('claude')) {
75
+ console.log(C.yellow(' claude CLI not found; skipping the plugin channel'));
76
+ } else {
77
+ // The full <name>@<name> form is required; `claude plugin install agent-sync`
78
+ // answers "Plugin not found".
79
+ ok = run('claude', ['plugin', 'marketplace', 'add', REPO]) && ok;
80
+ ok = run('claude', ['plugin', 'install', `${NAME}@${NAME}`]) && ok;
81
+ }
82
+ }
83
+
84
+ if (!claudeOnly) {
85
+ console.log(C.bold('\nOther agents — via the skills CLI'));
86
+ const args = ['--yes', 'skills', 'add', REPO, '--global', '--yes'];
87
+ for (const a of agents || []) args.push('--agent', a);
88
+ ok = run('npx', args) && ok;
89
+ }
90
+
91
+ pruneShadow();
92
+
93
+ console.log(
94
+ ok
95
+ ? C.green('\n✓ installed')
96
+ : C.red('\n✗ at least one channel failed — see the output above')
97
+ );
98
+ console.log(`
99
+ ${C.bold('Next:')} restart Claude Code, then run ${C.bold('/agent-sync init')} in your project.
100
+ It will ask where coordination state should live before writing anything.
101
+ `);
102
+ return ok ? 0 : 1;
103
+ }
104
+
105
+ /**
106
+ * The shadow regrows on its own: `npx skills add|update --global` auto-detects
107
+ * Claude Code and recreates ~/.claude/skills/<name> — often as a symlink — even when
108
+ * claude-code was never named as a target. That copy shadows the plugin and serves a
109
+ * stale skill, so the prune belongs INSIDE every command that touches the skills CLI,
110
+ * not in a human's memory. lstatSync, because a symlink shadows exactly as a dir does.
111
+ */
112
+ function pruneShadow() {
113
+ let present = false;
114
+ try {
115
+ fs.lstatSync(SHADOW);
116
+ present = true;
117
+ } catch {
118
+ /* not there */
119
+ }
120
+ if (!present) return;
121
+ fs.rmSync(SHADOW, { recursive: true, force: true });
122
+ console.log(C.dim(` pruned duplicate ${SHADOW}`));
123
+ }
124
+
125
+ function update() {
126
+ console.log(C.bold('\nUpdating every channel'));
127
+ let ok = true;
128
+ if (has('claude')) {
129
+ ok = run('claude', ['plugin', 'marketplace', 'update', NAME]) && ok;
130
+ // The full <name>@<name> id is required; `claude plugin update <name>` answers
131
+ // "Plugin not found".
132
+ ok = run('claude', ['plugin', 'update', `${NAME}@${NAME}`]) && ok;
133
+ }
134
+ ok = run('npx', ['--yes', 'skills', 'update', NAME, '--global', '--yes']) && ok;
135
+ pruneShadow();
136
+ console.log(ok ? C.green('\n✓ updated') : C.red('\n✗ a channel failed — see above'));
137
+ console.log('\nRestart Claude Code so it picks the new version up.');
138
+ return ok ? 0 : 1;
139
+ }
140
+
141
+ const argv = process.argv.slice(2);
142
+ if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
143
+ usage();
144
+ process.exit(0);
145
+ }
146
+ if (argv[0] === 'install') process.exit(install(argv.slice(1)));
147
+ if (argv[0] === 'update') process.exit(update());
148
+ usage();
149
+ process.exit(1);
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@ssheleg/agent-sync",
3
+ "version": "1.2.2",
4
+ "description": "Let concurrent coding agents share one project without colliding \u2014 leases with TTL, race-free id reservation, a run journal and a generated board, over a pluggable knowledge cloud.",
5
+ "bin": {
6
+ "agent-sync": "bin/agent-sync.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "plugins/",
11
+ "agent-sync.schema.json",
12
+ "agent-sync.example.json",
13
+ "README.md",
14
+ "CHANGELOG.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "test": "python3 test/validate.py && python3 test/validate.py --self-test",
19
+ "prepublishOnly": "python3 test/validate.py"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "keywords": [
25
+ "claude-code",
26
+ "agent-skills",
27
+ "multi-agent",
28
+ "coordination",
29
+ "lease",
30
+ "knowledge-base",
31
+ "task-pipeline"
32
+ ],
33
+ "homepage": "https://github.com/appvillis-com/agent-sync",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/appvillis-com/agent-sync.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/appvillis-com/agent-sync/issues"
40
+ },
41
+ "author": "appvillis-com",
42
+ "license": "MIT",
43
+ "engines": {
44
+ "node": ">=18"
45
+ }
46
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "agent-sync",
3
+ "version": "1.2.2",
4
+ "description": "Coordination layer for multi-agent repositories — leases with TTL, race-free ID reservation, a run journal, a cross-repo signal feed and a generated board, over a pluggable knowledge cloud.",
5
+ "author": {
6
+ "name": "appvillis-com"
7
+ },
8
+ "homepage": "https://github.com/appvillis-com/agent-sync",
9
+ "repository": "https://github.com/appvillis-com/agent-sync",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "multi-agent",
13
+ "coordination",
14
+ "lease",
15
+ "knowledge-base",
16
+ "task-pipeline"
17
+ ]
18
+ }
@@ -0,0 +1,16 @@
1
+ ---
2
+ description: Coordinate concurrent agents — initialise the shared knowledge store, check status, claim a task, reserve an id, or regenerate the board.
3
+ argument-hint: "[init|status|claim <KEY>|release <KEY>|reserve <REG>|board]"
4
+ ---
5
+
6
+ Invoke the `agent-sync` skill.
7
+
8
+ Arguments: $ARGUMENTS
9
+
10
+ If no arguments were given, run the skill's default entry: check whether the
11
+ project is initialised, and if it is not, **ask the operator where coordination
12
+ state should live before doing anything else** — a knowledge cloud (and then its
13
+ instance URL) or local files. Never guess that answer.
14
+
15
+ If the project is already initialised, report status and name exactly one next
16
+ action.
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bash
2
+ # Shared helpers for the agent-sync hooks. Sourced, never executed.
3
+
4
+ # Run a command under a time limit, portably.
5
+ #
6
+ # `timeout` is GNU coreutils and is NOT on a stock macOS. Calling it directly made
7
+ # three of the four hooks die with "timeout: command not found" on every macOS
8
+ # session — which meant leases were never renewed and never released there, the exact
9
+ # abandoned-lease failure this tool exists to prevent. Homebrew installs it as
10
+ # `gtimeout`, and neither is guaranteed, so fall back to a plain-bash watchdog.
11
+ run_limited() {
12
+ local secs="$1"; shift
13
+ if command -v timeout >/dev/null 2>&1; then
14
+ timeout "$secs" "$@"
15
+ return $?
16
+ fi
17
+ if command -v gtimeout >/dev/null 2>&1; then
18
+ gtimeout "$secs" "$@"
19
+ return $?
20
+ fi
21
+
22
+ "$@" &
23
+ local pid=$!
24
+ ( sleep "$secs"; kill -TERM "$pid" 2>/dev/null ) &
25
+ local watchdog=$!
26
+ wait "$pid" 2>/dev/null
27
+ local rc=$?
28
+ kill "$watchdog" 2>/dev/null
29
+ wait "$watchdog" 2>/dev/null
30
+ return "$rc"
31
+ }
32
+
33
+ # Every hook is a no-op in a project that does not use agent-sync, so installing the
34
+ # plugin globally changes nothing elsewhere.
35
+ agent_sync_configured() {
36
+ [ -f "${CLAUDE_PROJECT_DIR:-$PWD}/.claude/agent-sync.json" ]
37
+ }
38
+
39
+ AGENT_SYNC_PY="${CLAUDE_PLUGIN_ROOT:-}/skills/agent-sync/scripts/agent_sync.py"
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env bash
2
+ # PreToolUse guard. Exit 2 blocks the call and shows stderr as the reason.
3
+ # Any other non-zero code is NON-blocking in Claude Code, so every internal
4
+ # failure must also exit 2 — a crashing guard that fails open guards nothing.
5
+ set -uo pipefail
6
+ S="${CLAUDE_PLUGIN_ROOT}/skills/agent-sync/scripts/agent_sync.py"
7
+ input=$(cat)
8
+
9
+ path=$(python3 -c '
10
+ import json,sys
11
+ try:
12
+ d=json.load(sys.stdin)
13
+ except Exception:
14
+ sys.exit(0)
15
+ ti=d.get("tool_input") or {}
16
+ print(ti.get("file_path") or ti.get("path") or ti.get("notebook_path") or "")
17
+ ' <<<"$input" 2>/dev/null)
18
+
19
+ # git commit: check every staged path instead of a single file argument.
20
+ if [ -z "$path" ]; then
21
+ cmd=$(python3 -c '
22
+ import json,sys
23
+ try:
24
+ d=json.load(sys.stdin)
25
+ except Exception:
26
+ sys.exit(0)
27
+ print((d.get("tool_input") or {}).get("command",""))
28
+ ' <<<"$input" 2>/dev/null)
29
+ case "$cmd" in
30
+ *"git commit"*)
31
+ while IFS= read -r staged; do
32
+ [ -n "$staged" ] || continue
33
+ if ! python3 "$S" guard "$staged" >/dev/null 2>&1; then
34
+ echo "agent-sync: '$staged' is staged but this run holds no lease on it. Acquire one, or unstage it." >&2
35
+ exit 2
36
+ fi
37
+ done < <(git diff --cached --name-only 2>/dev/null)
38
+ ;;
39
+ esac
40
+ exit 0
41
+ fi
42
+
43
+ [ -f "${CLAUDE_PROJECT_DIR:-$PWD}/.claude/agent-sync.json" ] || exit 0
44
+
45
+ if out=$(python3 "$S" guard "$path" 2>&1); then
46
+ exit 0
47
+ else
48
+ code=$?
49
+ if [ "$code" -eq 2 ]; then
50
+ echo "$out" >&2
51
+ exit 2
52
+ fi
53
+ echo "agent-sync guard failed to run ($code): $out" >&2
54
+ exit 2
55
+ fi
@@ -0,0 +1,69 @@
1
+ {
2
+ "description": "agent-sync — lease enforcement and run lifecycle. Every hook exits 0 immediately when the project has no .claude/agent-sync.json, so installing the plugin globally changes nothing in projects that do not use it.",
3
+ "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "matcher": "startup|resume",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "shell": "bash",
11
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh\"",
12
+ "timeout": 20
13
+ }
14
+ ]
15
+ }
16
+ ],
17
+ "PreToolUse": [
18
+ {
19
+ "matcher": "Edit|Write|MultiEdit|NotebookEdit",
20
+ "hooks": [
21
+ {
22
+ "type": "command",
23
+ "shell": "bash",
24
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/guard.sh\"",
25
+ "timeout": 20
26
+ }
27
+ ]
28
+ },
29
+ {
30
+ "matcher": "Bash",
31
+ "if": "Bash(git commit *)",
32
+ "hooks": [
33
+ {
34
+ "type": "command",
35
+ "shell": "bash",
36
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/guard.sh\"",
37
+ "timeout": 20
38
+ }
39
+ ]
40
+ }
41
+ ],
42
+ "PostToolUse": [
43
+ {
44
+ "matcher": "*",
45
+ "hooks": [
46
+ {
47
+ "type": "command",
48
+ "shell": "bash",
49
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/renew.sh\"",
50
+ "timeout": 15,
51
+ "async": true
52
+ }
53
+ ]
54
+ }
55
+ ],
56
+ "SessionEnd": [
57
+ {
58
+ "hooks": [
59
+ {
60
+ "type": "command",
61
+ "shell": "bash",
62
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/session-end.sh\"",
63
+ "timeout": 20
64
+ }
65
+ ]
66
+ }
67
+ ]
68
+ }
69
+ }
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bash
2
+ # Runs after every tool call, so it must be a no-op in the common case:
3
+ # the script itself throttles on a timestamp file and touches the network at
4
+ # most once per renewIntervalSeconds.
5
+ set -uo pipefail
6
+ . "${CLAUDE_PLUGIN_ROOT}/hooks/_lib.sh"
7
+ S="$AGENT_SYNC_PY"
8
+ agent_sync_configured || exit 0
9
+ run_limited 10 python3 "$S" renew >/dev/null 2>&1 || true
10
+ exit 0
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bash
2
+ # Release every lease this run holds. An abandoned lease looks like active work
3
+ # until its TTL expires.
4
+ set -uo pipefail
5
+ . "${CLAUDE_PLUGIN_ROOT}/hooks/_lib.sh"
6
+ S="$AGENT_SYNC_PY"
7
+ agent_sync_configured || exit 0
8
+ held=$(run_limited 10 python3 "$S" whoami 2>/dev/null | sed -n 's/^holds: //p')
9
+ [ -z "$held" ] || [ "$held" = "nothing" ] && exit 0
10
+ IFS=', ' read -r -a keys <<<"$held"
11
+ for k in "${keys[@]}"; do
12
+ [ -n "$k" ] && run_limited 10 python3 "$S" release "$k" >/dev/null 2>&1 || true
13
+ done
14
+ exit 0
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ # Register the run and print the board summary plus one next action.
3
+ set -uo pipefail
4
+ . "${CLAUDE_PLUGIN_ROOT}/hooks/_lib.sh"
5
+ S="$AGENT_SYNC_PY"
6
+ agent_sync_configured || exit 0
7
+ run_limited 10 python3 "$S" status 2>&1 || true
8
+ exit 0